LearnNewsExamplesServices
Frontmatter
id13177
titleFleet Manager: provision the agent repo at spawn, run the harness in it
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtJun 14, 2026, 4:43 AM
updatedAtJun 14, 2026, 8:43 AM
githubUrlhttps://github.com/neomjs/neo/issues/13177
authorneo-opus-ada
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 14, 2026, 8:43 AM

Fleet Manager: provision the agent repo at spawn, run the harness in it

Closed v13.1.0/archive-v13-1-0-chunk-2 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on Jun 14, 2026, 4:43 AM

Context

The Fleet Manager repo-provisioning chain is complete on devderiveAgentRepoPathinspectAgentRepoprovisionAgentRepoensureAgentRepo (#13145 / #13148 / #13155 / #13162). It is a four-leaf pipeline that, given an agent + a managed root + a clone URL, lands a stable, collision-free, traversal-safe checkout without ever clobbering an occupant.

But nothing consumes it yet. FleetLifecycleService.start(id) — the "run the agent" leaf — imports only FleetRegistryService; it never calls ensureAgentRepo. The chain I built is dead code until a caller wires it into the spawn path. This ticket is that wire.

The Problem

Two concrete gaps, both verified against origin/dev:

  1. The spawned harness runs in an undefined working directory. start() spawns with this.getSpawnFn()(command, args, {stdio, env})no cwd. The child therefore inherits the Fleet Manager's own cwd, not the agent's checkout. An FM-spawned harness that is meant to operate on its repo has no idea where that repo is. This is the in-miniature form of the epic's stated friction (#13015): "if i start 5 claude desktop instances plus codex via terminal and manage 6 agent repos… brittle" — the paths are load-bearing (auto-memory is checkout-path-keyed) and right now the launch does not even set one.

  2. Provisioning is manual. Starting an agent assumes someone already cloned its repo to the right path by hand. The whole point of the provisioning chain is to make "start an agent" turnkey: clone-or-reuse the canonical checkout, then launch the harness in it.

The Architectural Reality

  • ai/services/fleet/FleetLifecycleService.mjsstart(id) is synchronous, builds {stdio, env}, and spawns. The JSDoc explicitly scopes it to "process supervision + harness-auth provisioning" and defers repo/identity provisioning to "a separate provisioning leaf" — this ticket is that leaf. No external caller of start() exists yet (only doc-references in provisionAgentRepo.mjs), so widening its options object has zero blast radius.
  • ai/services/fleet/ensureAgentRepo.mjsasync ensureAgentRepo({managedRoot, agentId, repoSlug, cloneUrl, cloneRepo}){repoPath, state, action, cloned}. The composer feeds it from the agent's metadata + an FM-level managed root and uses the returned repoPath as the spawn cwd.
  • ai/services/fleet/FleetRegistryService.mjs — an agent definition is {id, githubUsername, harnessType, metadata, …} where metadata is documented free-form (the metadata.launch = {command, args} convention already lives there). The agent's working-repo coordinates ride the same channel: metadata.repo = {cloneUrl, repoSlug}. No registry-schema change.

The Fix

A new composer + a minimal primitive on the supervisor:

  • ai/services/fleet/startAgentProvisioned.mjs (new, standalone — sibling to ensureAgentRepo): async startAgentProvisioned({lifecycleService, managedRoot, agentId, cloneRepo, ensureRepo = ensureAgentRepo}). Reads the agent's metadata.repo; if present → await ensureRepo({managedRoot, agentId, repoSlug, cloneUrl, cloneRepo}) then lifecycleService.start(agentId, {cwd: repoPath}); if absent → lifecycleService.start(agentId) (backward-compatible plain start). A provisioning throw propagates and the harness is never spawned (fail-closed: no launch into an unprovisioned / conflicting directory). Short-circuits a redundant provision when the agent is already running.
  • FleetLifecycleService.start(id, opts = {}) gains an optional opts.cwd, passed through to the spawn options; absent → today's behavior exactly. A process supervisor letting its caller set the working directory is an obviously-correct primitive; the opinionated provisioning composition stays in the composer (and out of the registry-owned service).

ensureRepo is an injectable seam (default-real, mirroring provisionAgentRepo's cloneRepo and the service's spawnFn) so the composer is unit-testable without a git binary or the filesystem.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
FleetLifecycleService.start(id, opts) this ticket new optional opts.cwd → spawn option omitted ⇒ inherited cwd (unchanged) start() JSDoc origin/dev start() spawns {stdio, env}, no cwd (verified)
startAgentProvisioned({…}) (new) this ticket ensureRepostart(id, {cwd: repoPath}); no metadata.repo ⇒ plain start; provision-throw ⇒ no spawn fail-closed (no launch) module JSDoc + spec ensureAgentRepo signature verified on origin/dev
agent.metadata.repo = {cloneUrl, repoSlug} this ticket (convention) the agent's working-repo coordinates the composer reads absent ⇒ no provisioning composer JSDoc metadata is free-form per FleetRegistryService.defineAgent

Decision Record impact

none — no ADR governs FM spawn provisioning. Aligned-with the #13015 MVP decomposition (this is the repo-provisioning consumer leaf).

Acceptance Criteria

  • FleetLifecycleService.start(id, opts) passes opts.cwd to the spawn options when present; omitting it reproduces today's spawn exactly (covered by an extended FleetLifecycleService.spec).
  • startAgentProvisioned with a metadata.repo provisions via the injected ensureRepo and calls start with {cwd: repoPath}.
  • startAgentProvisioned with no metadata.repo calls start with no cwd (backward-compatible).
  • A throwing ensureRepo propagates and start is not called (fail-closed — no spawn into a bad dir).
  • An already-running agent short-circuits to status without a redundant provision.
  • missing lifecycleService / agentId / (repo present but) managedRoot each throw a clear error.
  • Unit spec at test/playwright/unit/ai/startAgentProvisioned.spec.mjs, green via the custom unit config; node --check on both touched files.

Out of Scope

  • Spawn-time identity-env + wake-subscription provisioning (gated on cross-harness session-id canonicalization — the other deferred leaf named in the start() JSDoc).
  • Promoting metadata.repo to a first-class registry-schema field (free-form metadata is the documented extension point; promotion is a later refinement if the convention proves load-bearing — flagged for the registry owner's input at review).
  • Wiring managedRoot to a concrete FM config source / the MCP tool / settings-pane entry point (the composer takes it as a param; the turnkey caller supplies it later).
  • Any change to provisionAgentRepo / inspectAgentRepo / clone semantics.

Avoided Traps

  • Absorbing async provisioning into start() (making it async) — rejected: it would break start()'s synchronous contract for all future callers and bury an opinionated FM convention inside the registry-owned supervisor. The composer keeps start() a pure, sync, cwd-aware process primitive.
  • A first-class repoUrl registry field — rejected for now: a schema change in a peer-owned service for a convention that metadata already carries cleanly.

Related

  • Parent epic: #13015 (Fleet Manager MVP — define, start, observe). This is the leaf that makes "start" provision its repo.
  • Consumes: #13162 (ensureAgentRepo), #13145 / #13148 / #13155 (the provisioning trio).
  • Touches FleetLifecycleService (#13049) and reads the metadata convention from FleetRegistryService (#13031) — FYI to the registry/lifecycle author at review.

Decision Record impact

none.

Live latest-open sweep: checked the live tracker (open issues + in-flight A2A [lane-claim] window) immediately before filing; no equivalent — the only active claim is an unrelated marathon-benchmark lane.

Authored by Claude Opus 4.8 (Claude Code), @neo-opus-ada. Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e Retrieval Hint: "Fleet Manager spawn provisioning startAgentProvisioned cwd ensureAgentRepo"