Context
The Fleet Manager Node-side primitives are now all merged — registry (#13031), lifecycle (#13049), the provisioning chain (#13145/#13148/#13155/#13162), the spawn-provision composer startAgentProvisioned (#13178), and the observe aggregator inspectFleetRepos (#13180). But every consumer must pass managedRoot by hand and wire the singletons itself — startAgentProvisioned({lifecycleService, managedRoot, agentId}), inspectFleetRepos({registry, managedRoot}). There is no single control-plane entry that resolves managedRoot once and offers the operator operations turnkey.
This is the surface-independent service layer of the control-plane keystone (pieces 1+2): a FleetManager facade. The operator-facing surface (an MCP control-plane vs the settings-pane wiring — piece 3) is separate, routed to @neo-gpt; it sits thinly on top of this facade, calling startAgent / fleetRepoStatus rather than re-resolving managedRoot or re-wiring the singletons each time.
The Problem
managedRoot is a parameter every composer caller must supply, and the registry/lifecycle singletons must be wired by hand at each call site. Without a single resolution + composition point, the eventual surface (and any other consumer) duplicates that plumbing, and managedRoot has no canonical resolution (env / default).
The Architectural Reality
startAgentProvisioned (#13178, on dev) → {lifecycleService, agentId, managedRoot, …}.
inspectFleetRepos (#13180, on dev) → {registry, managedRoot, …}.
FleetRegistryService.getDataDir() is the precedent: this.dataDir || path.resolve(__dirname, '../../../.neo-ai-data/fleet') (an import.meta.url-derived __dirname + a config/env override). managedRoot resolution mirrors it.
FleetLifecycleService exposes getRegistry(), so the facade can derive the registry from its lifecycle service (one source of truth).
The Fix
ai/services/fleet/FleetManager.mjs (new singleton, Neo.core.Base — sibling to FleetLifecycleService / FleetRegistryService):
getManagedRoot() → this.managedRoot || process.env.NEO_FLEET_MANAGED_ROOT || path.resolve(__dirname, '../../../.neo-ai-data/fleet/repos') — the getDataDir precedent (config field > env > __dirname-relative default), no hidden fallbacks.
async startAgent(agentId) → startAgentProvisioned({lifecycleService: this.getLifecycleService(), managedRoot: this.getManagedRoot(), agentId}) — turnkey provision-and-start.
fleetRepoStatus() → inspectFleetRepos({registry: this.getLifecycleService().getRegistry(), managedRoot: this.getManagedRoot()}) — turnkey fleet repo observability (registry derived from the lifecycle service = one source of truth).
- Injectable seams (default-real, mirroring
FleetLifecycleService.spawnFn/registry): lifecycleService (default the singleton) + provisionAndStartFn / repoStatusFn (default the composers) so the facade's resolution + wiring is unit-testable without real fs/git/spawn.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
FleetManager.getManagedRoot() (new) |
this ticket |
managedRoot config > NEO_FLEET_MANAGED_ROOT env > <repoRoot>/.neo-ai-data/fleet/repos |
the __dirname-relative default |
JSDoc + spec |
mirrors FleetRegistryService.getDataDir (verified :481) |
FleetManager.startAgent(id) (new) |
this ticket |
startAgentProvisioned with the resolved root + lifecycle singleton |
inherits the composer's fail-closed (#13178) |
JSDoc + spec |
startAgentProvisioned signature verified on dev |
FleetManager.fleetRepoStatus() (new) |
this ticket |
inspectFleetRepos with the resolved root + the lifecycle's registry |
inherits the aggregator (#13180) |
JSDoc + spec |
inspectFleetRepos signature verified on dev |
Decision Record impact
none — aligned-with the #13015 MVP decomposition + #13167's control-plane framing; composes merged primitives without modifying them.
Acceptance Criteria
Out of Scope
- The operator-facing surface (MCP control-plane tools vs settings-pane wiring — piece 3, @neo-gpt's call); this facade is what it sits on.
stop / restart (already on FleetLifecycleService).
- The agent-removal flow + its #13190 memory reconciliation (separate lifecycle leaf).
Related
- Parent epic #13015 (FM MVP). The control-plane service layer over #13178 (
startAgentProvisioned) + #13180 (inspectFleetRepos); #13167 control-plane framing; surface (piece 3) routed to @neo-gpt.
Live latest-open sweep: checked the live tracker + in-flight A2A claims immediately before filing; no equivalent.
Authored by Claude Opus 4.8 (Claude Code), @neo-opus-ada.
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e
Retrieval Hint: "Fleet Manager control-plane facade FleetManager getManagedRoot startAgent fleetRepoStatus"
Context
The Fleet Manager Node-side primitives are now all merged — registry (#13031), lifecycle (#13049), the provisioning chain (#13145/#13148/#13155/#13162), the spawn-provision composer
startAgentProvisioned(#13178), and the observe aggregatorinspectFleetRepos(#13180). But every consumer must passmanagedRootby hand and wire the singletons itself —startAgentProvisioned({lifecycleService, managedRoot, agentId}),inspectFleetRepos({registry, managedRoot}). There is no single control-plane entry that resolvesmanagedRootonce and offers the operator operations turnkey.This is the surface-independent service layer of the control-plane keystone (pieces 1+2): a
FleetManagerfacade. The operator-facing surface (an MCP control-plane vs the settings-pane wiring — piece 3) is separate, routed to @neo-gpt; it sits thinly on top of this facade, callingstartAgent/fleetRepoStatusrather than re-resolvingmanagedRootor re-wiring the singletons each time.The Problem
managedRootis a parameter every composer caller must supply, and the registry/lifecycle singletons must be wired by hand at each call site. Without a single resolution + composition point, the eventual surface (and any other consumer) duplicates that plumbing, andmanagedRoothas no canonical resolution (env / default).The Architectural Reality
startAgentProvisioned(#13178, on dev) →{lifecycleService, agentId, managedRoot, …}.inspectFleetRepos(#13180, on dev) →{registry, managedRoot, …}.FleetRegistryService.getDataDir()is the precedent:this.dataDir || path.resolve(__dirname, '../../../.neo-ai-data/fleet')(animport.meta.url-derived__dirname+ a config/env override).managedRootresolution mirrors it.FleetLifecycleServiceexposesgetRegistry(), so the facade can derive the registry from its lifecycle service (one source of truth).The Fix
ai/services/fleet/FleetManager.mjs(new singleton,Neo.core.Base— sibling toFleetLifecycleService/FleetRegistryService):getManagedRoot()→this.managedRoot || process.env.NEO_FLEET_MANAGED_ROOT || path.resolve(__dirname, '../../../.neo-ai-data/fleet/repos')— thegetDataDirprecedent (config field > env >__dirname-relative default), no hidden fallbacks.async startAgent(agentId)→startAgentProvisioned({lifecycleService: this.getLifecycleService(), managedRoot: this.getManagedRoot(), agentId})— turnkey provision-and-start.fleetRepoStatus()→inspectFleetRepos({registry: this.getLifecycleService().getRegistry(), managedRoot: this.getManagedRoot()})— turnkey fleet repo observability (registry derived from the lifecycle service = one source of truth).FleetLifecycleService.spawnFn/registry):lifecycleService(default the singleton) +provisionAndStartFn/repoStatusFn(default the composers) so the facade's resolution + wiring is unit-testable without real fs/git/spawn.Contract Ledger Matrix
FleetManager.getManagedRoot()(new)managedRootconfig >NEO_FLEET_MANAGED_ROOTenv ><repoRoot>/.neo-ai-data/fleet/repos__dirname-relative defaultFleetRegistryService.getDataDir(verified:481)FleetManager.startAgent(id)(new)startAgentProvisionedwith the resolved root + lifecycle singletonstartAgentProvisionedsignature verified on devFleetManager.fleetRepoStatus()(new)inspectFleetReposwith the resolved root + the lifecycle's registryinspectFleetRepossignature verified on devDecision Record impact
none— aligned-with the #13015 MVP decomposition + #13167's control-plane framing; composes merged primitives without modifying them.Acceptance Criteria
getManagedRoot()resolves config > env > default, with no hidden fallback (an unset env + unset config → the__dirname-relative default; a set env overrides; a set config field overrides both).startAgent(id)calls the provision-composer with the resolvedmanagedRoot+ the lifecycle service (asserted via the injected seam).fleetRepoStatus()calls the inspect-aggregator with the resolvedmanagedRoot+ the lifecycle's registry (asserted via the injected seam).startAgentProvisioned/inspectFleetRepos).test/playwright/unit/ai/FleetManager.spec.mjs, green via the custom unit config;node --check.Out of Scope
stop/restart(already onFleetLifecycleService).Related
startAgentProvisioned) + #13180 (inspectFleetRepos); #13167 control-plane framing; surface (piece 3) routed to @neo-gpt.Live latest-open sweep: checked the live tracker + in-flight A2A claims immediately before filing; no equivalent.
Authored by Claude Opus 4.8 (Claude Code), @neo-opus-ada. Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e Retrieval Hint: "Fleet Manager control-plane facade FleetManager getManagedRoot startAgent fleetRepoStatus"