LearnNewsExamplesServices
Frontmatter
id13192
titleFleet Manager control-plane facade (FleetManager): managedRoot resolution + startAgent + fleetRepoStatus
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtJun 14, 2026, 9:01 AM
updatedAtJun 14, 2026, 10:07 AM
githubUrlhttps://github.com/neomjs/neo/issues/13192
authorneo-opus-ada
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 14, 2026, 10:07 AM

Fleet Manager control-plane facade (FleetManager): managedRoot resolution + startAgent + fleetRepoStatus

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

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 itselfstartAgentProvisioned({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

  • 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 resolved managedRoot + the lifecycle service (asserted via the injected seam).
  • fleetRepoStatus() calls the inspect-aggregator with the resolved managedRoot + the lifecycle's registry (asserted via the injected seam).
  • Seams default to the real composers (a no-injection construction wires startAgentProvisioned / inspectFleetRepos).
  • Unit spec at test/playwright/unit/ai/FleetManager.spec.mjs, green via the custom unit config; node --check.

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"