Context
First leaf of the Fleet Manager sub-epic #13015 (pillar 1 of the Agent Harness #13012). Operator direction (2026-06-13): "agent harness biggest ROI." This is the FM MVP's start/stop verb — consumed by every later FM surface (settings-pane, NL-MCP) — and the one slice not gated by the #12984 session-id canonicalization.
The Problem
#13015's recorded operator pain: running the swarm means N manually-launched harness instances (Claude Desktop windows + terminal Codex), each started and killed by hand. The registry (sibling leaf #13031) holds the definition; nothing yet acts on it — there is no programmatic start/stop/restart/health for a defined agent's harness process.
The Architectural Reality
- New Brain-side singleton
FleetLifecycleService under the existing ai/services/fleet/ domain (sibling to FleetRegistryService) — structural-pre-flight Stage-1 fast-path.
- Process supervision via Node's
child_process (spawn / signal / reap) — Node-side only.
- Registry integration: resolves the agent definition via
FleetRegistryService.getAgent(id) and the PAT via the Brain-internal resolveCredential(id), injected into the child env.
- Launch resolution: the launch command comes from the agent's
metadata.launch = {command, args} — harnessType is classification, not a launcher, so the service never guesses a brittle per-type command. (Refined from an earlier "per-harnessType strategy" — see Evolution.)
- Stderr supervision: the child's stderr is drained into a bounded tail so a noisy harness cannot block on pipe backpressure.
The Fix
A FleetLifecycleService (singleton) under ai/services/fleet/:
start(id) — resolve def + credential, spawn the harness (launch from metadata.launch; PAT in the child env), track PID + state, drain stderr.
stop(id) — graceful SIGTERM, then SIGKILL on timeout.
restart(id) — stop→start.
status(id) / listRunning() — running / stopped / PID / uptime / last-exit / bounded recent-stderr.
Contract Ledger
| Field |
Value |
| Target Surface |
ai/services/fleet/FleetLifecycleService.mjs (NEW) — start / stop / restart / status / listRunning |
| Source of Authority |
#13015 (FM MVP: "spawn/stop one external harness instance"); #13031 (the registry it consumes) |
| Proposed Behavior |
Brain-side process supervision of registry-defined harness instances. Launch command from the agent's metadata.launch. PAT injected Node-side into the child env (inject-if-present), never surfaced, never in argv. Child stderr drained (only a non-secret byte count retained — content never stored or surfaced, since the child may echo the PAT). |
| Fallback |
Unknown agent / no metadata.launch → start refuses (throws). Spawn failure → state: failed + captured error. Null credential → start proceeds without a token (a tokenless local harness is valid); the secret never leaks. |
| Docs |
Service JSDoc (Anchor & Echo). |
| Evidence |
Unit tests with a stubbed spawn (no real binary): lifecycle round-trip + credential-injection (child env only) + refusals + stderr drain & no-leak (a secret echoed to stderr is never surfaced via status()). |
Decision Record impact
aligned-with ADR 0020 (harness concept) + #13012 / #13015 design rules. depends-on #13031 (registry — merged). The #12984 session-id canonicalization gates the identity-env + wake-subscription provisioning — out of scope here (separate provisioning leaf); this leaf does process supervision only.
Acceptance Criteria
Out of Scope
- Identity-env + wake-subscription provisioning — #12984-gated (separate provisioning leaf).
- Repo provisioning (clone/worktree) — separate FM leaf.
- The settings-pane UI (Body-side,
apps/) — separate leaf.
- The Electron build root — parent-epic leaf (#13033).
- Wedged / idle watchdog detection — enrichment beyond v1 health.
Avoided Traps
- PAT in
argv — rejected: secrets go in the child env, never argv (visible in ps).
- Browser-side process control — rejected: Brain-side only (two-hemisphere rule).
- Undrained piped stderr — rejected: a piped-but-undrained stderr blocks a noisy child on pipe backpressure; drained, counting bytes only.
- Surfacing captured child stderr — rejected: the child's stderr is untrusted and may echo the injected PAT; only a non-secret byte count is retained, never content — a
status() read carries no secret. (Caught cycle-2, @neo-gpt.)
- Per-
harnessType hardcoded launchers — rejected: the service can't reliably guess how to launch an arbitrary external harness; the operator specifies metadata.launch.
- Real external binaries in unit tests — rejected: tests stub the spawn.
Evolution
The contract was refined during implementation (PR #13054), reconciled here after @neo-gpt's cross-family review flagged ticket-vs-code drift:
- Launch via
metadata.launch, not a per-harnessType command map — harnessType classifies an agent; it does not encode a launch command, and hardcoding per-type launchers is brittle/wrong across installs.
- Credential policy = inject-if-present, not refuse-on-null — the unconditional invariant is never leak (never
argv / record / log); refusing a tokenless agent is policy, and a local harness may legitimately have no PAT. start still refuses an unknown agent / missing launch spec.
- Stderr capture → byte-count only (cycle-2) — the cycle-1 fix surfaced a bounded stderr tail through
status(); since an external harness may echo its injected PAT to stderr, that re-opened the credential boundary. Now only a non-secret byte count (stderrBytes) is retained — content is never stored or surfaced.
Related
Parent: #13015 (Fleet Manager MVP). Epic: #13012. Consumes: #13031 (registry — merged). #12984 (gates the provisioning leaf, not this one). PR: #13054. Source archaeology: Discussion #10119.
Release classification: post-release (FM MVP product line; nothing v13.x-blocking).
Origin Session ID: 7c6c8fd9-219d-4eae-9d70-ef18ec945a14
Retrieval Hint: "fleet manager instance lifecycle start stop restart health child_process spawn harness FleetLifecycleService"
Authored by Claude Opus 4.8 (Claude Code, @neo-opus-grace / Grace).
Context
First leaf of the Fleet Manager sub-epic #13015 (pillar 1 of the Agent Harness #13012). Operator direction (2026-06-13): "agent harness biggest ROI." This is the FM MVP's
start/stopverb — consumed by every later FM surface (settings-pane, NL-MCP) — and the one slice not gated by the #12984 session-id canonicalization.The Problem
#13015's recorded operator pain: running the swarm means N manually-launched harness instances (Claude Desktop windows + terminal Codex), each started and killed by hand. The registry (sibling leaf #13031) holds the definition; nothing yet acts on it — there is no programmatic start/stop/restart/health for a defined agent's harness process.
The Architectural Reality
FleetLifecycleServiceunder the existingai/services/fleet/domain (sibling toFleetRegistryService) — structural-pre-flight Stage-1 fast-path.child_process(spawn / signal / reap) — Node-side only.FleetRegistryService.getAgent(id)and the PAT via the Brain-internalresolveCredential(id), injected into the child env.metadata.launch = {command, args}—harnessTypeis classification, not a launcher, so the service never guesses a brittle per-type command. (Refined from an earlier "per-harnessType strategy" — see Evolution.)The Fix
A
FleetLifecycleService(singleton) underai/services/fleet/:start(id)— resolve def + credential, spawn the harness (launch frommetadata.launch; PAT in the child env), track PID + state, drain stderr.stop(id)— gracefulSIGTERM, thenSIGKILLon timeout.restart(id)—stop→start.status(id)/listRunning()— running / stopped / PID / uptime / last-exit / bounded recent-stderr.Contract Ledger
ai/services/fleet/FleetLifecycleService.mjs(NEW) —start/stop/restart/status/listRunningmetadata.launch. PAT injected Node-side into the child env (inject-if-present), never surfaced, never inargv. Child stderr drained (only a non-secret byte count retained — content never stored or surfaced, since the child may echo the PAT).metadata.launch→startrefuses (throws). Spawn failure →state: failed+ captured error. Null credential →startproceeds without a token (a tokenless local harness is valid); the secret never leaks.status()).Decision Record impact
aligned-withADR 0020 (harness concept) + #13012 / #13015 design rules.depends-on#13031 (registry — merged). The #12984 session-id canonicalization gates the identity-env + wake-subscription provisioning — out of scope here (separate provisioning leaf); this leaf does process supervision only.Acceptance Criteria
FleetLifecycleServiceinai/services/fleet/withstart/stop/restart/status/listRunning.start(id)resolves def + credential fromFleetRegistryService, spawns the harness (launch frommetadata.launch), injects the PAT into the child env only (neverargv).stopgraceful (SIGTERM→SIGKILLon timeout);restartstops then starts fresh.status/listRunningreport running / stopped / pid / uptime / last-exit / stderr byte-count (never stderr content).argv/ record / log).startrefuses an unknown agent or one with no launch spec.status().Out of Scope
apps/) — separate leaf.Avoided Traps
argv— rejected: secrets go in the child env, neverargv(visible inps).status()read carries no secret. (Caught cycle-2, @neo-gpt.)harnessTypehardcoded launchers — rejected: the service can't reliably guess how to launch an arbitrary external harness; the operator specifiesmetadata.launch.Evolution
The contract was refined during implementation (PR #13054), reconciled here after @neo-gpt's cross-family review flagged ticket-vs-code drift:
metadata.launch, not a per-harnessTypecommand map —harnessTypeclassifies an agent; it does not encode a launch command, and hardcoding per-type launchers is brittle/wrong across installs.argv/ record / log); refusing a tokenless agent is policy, and a local harness may legitimately have no PAT.startstill refuses an unknown agent / missing launch spec.status(); since an external harness may echo its injected PAT to stderr, that re-opened the credential boundary. Now only a non-secret byte count (stderrBytes) is retained — content is never stored or surfaced.Related
Parent: #13015 (Fleet Manager MVP). Epic: #13012. Consumes: #13031 (registry — merged). #12984 (gates the provisioning leaf, not this one). PR: #13054. Source archaeology: Discussion #10119.
Release classification: post-release (FM MVP product line; nothing v13.x-blocking).
Origin Session ID: 7c6c8fd9-219d-4eae-9d70-ef18ec945a14 Retrieval Hint: "fleet manager instance lifecycle start stop restart health child_process spawn harness FleetLifecycleService"
Authored by Claude Opus 4.8 (Claude Code, @neo-opus-grace / Grace).