Context
Sub-leaf of #13015 (FM MVP) — the capstone of the repo-provisioning trio. #13145 / PR #13146 (derive the path), #13148 / PR #13149 (inspect the state → action), and #13155 / PR #13156 (provision the action) are all merged on dev. This composes them into the single "ensure an agent's repo exists" operation the Fleet Manager lifecycle calls.
The Problem
The three primitives are individually useful, but a caller must wire them in the correct sequence (derive → inspect → provision) every time — error-prone + duplicated. The orchestrator is the single entry point: given an agent + repo + managed root + clone URL, ensure the managed checkout exists (clone if absent, reuse if present, conflict if a foreign occupant), returning the outcome.
The Architectural Reality
ai/services/fleet/ensureAgentRepo.mjs (Brain side, beside the three primitives + FleetLifecycleService). A plain async function that imports + composes deriveAgentRepoPath + inspectAgentRepo + provisionAgentRepo (all on dev).
- Inherits the primitives' contracts:
deriveAgentRepoPath validates + computes the stable path; inspectAgentRepo classifies the on-disk state; provisionAgentRepo executes (clone via the injectable seam / reuse / conflict-throw). The cloneRepo seam passes through for testability.
- Single-writer (Scenario-C-zero per #13015): the inspect→provision sequence isn't TOCTOU-guarded — and doesn't need to be (git clone fails safe if the dir changed, and the FM is single-writer per the epic guardrail).
The Fix
export async function ensureAgentRepo({managedRoot, agentId, repoSlug, cloneUrl, cloneRepo}) → {repoPath, state, action, cloned}:
repoPath = deriveAgentRepoPath({managedRoot, agentId, repoSlug})
inspection = inspectAgentRepo({repoPath})
result = await provisionAgentRepo({repoPath, provisioningAction: inspection.provisioningAction, cloneUrl, cloneRepo})
return {repoPath, state: inspection.state, ...result}
Acceptance Criteria
Out of Scope
cloneUrl / credential resolution (the caller supplies the already-resolved URL).
- Wiring
ensureAgentRepo into FleetLifecycleService spawn (a later lifecycle integration).
- Concurrency / locking (single-writer per the epic).
Decision Record impact
none — composes existing Brain-side helpers; no ADR interaction.
Related
Live latest-open sweep: checked open issues + an ensure / orchestrate / compose-repo scan at 2026-06-14T00:49Z; no equivalent found (the one "orchestrator" hit is #12065, the unrelated REM pipeline epic).
Release classification: post-release (Fleet Manager product line; nothing v13.x-blocking).
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e
Context
Sub-leaf of #13015 (FM MVP) — the capstone of the repo-provisioning trio. #13145 / PR #13146 (derive the path), #13148 / PR #13149 (inspect the state → action), and #13155 / PR #13156 (provision the action) are all merged on dev. This composes them into the single "ensure an agent's repo exists" operation the Fleet Manager lifecycle calls.
The Problem
The three primitives are individually useful, but a caller must wire them in the correct sequence (derive → inspect → provision) every time — error-prone + duplicated. The orchestrator is the single entry point: given an agent + repo + managed root + clone URL, ensure the managed checkout exists (clone if absent, reuse if present, conflict if a foreign occupant), returning the outcome.
The Architectural Reality
ai/services/fleet/ensureAgentRepo.mjs(Brain side, beside the three primitives +FleetLifecycleService). A plain async function that imports + composesderiveAgentRepoPath+inspectAgentRepo+provisionAgentRepo(all on dev).deriveAgentRepoPathvalidates + computes the stable path;inspectAgentRepoclassifies the on-disk state;provisionAgentRepoexecutes (clone via the injectable seam / reuse / conflict-throw). ThecloneReposeam passes through for testability.The Fix
export async function ensureAgentRepo({managedRoot, agentId, repoSlug, cloneUrl, cloneRepo})→{repoPath, state, action, cloned}:repoPath = deriveAgentRepoPath({managedRoot, agentId, repoSlug})inspection = inspectAgentRepo({repoPath})result = await provisionAgentRepo({repoPath, provisioningAction: inspection.provisioningAction, cloneUrl, cloneRepo})return {repoPath, state: inspection.state, ...result}Acceptance Criteria
ensureAgentRepocomposes derive → inspect → provision; async; imports the three merged primitives.cloneRepoinvoked once), returnscloned: true.cloneRepoNOT invoked), returnscloned: false.cloneRepoNOT invoked.cloneReposeam passes through toprovisionAgentRepofor testability; the default (un-injected) path uses the realgit clone.managedRoot/agentId/repoSlugthrow (inherited fromderiveAgentRepoPath).test/playwright/unit/ai/ensureAgentRepo.spec.mjs: temp-dir fixtures (absent / checkout / occupied) + an injected clone stub; green.Out of Scope
cloneUrl/ credential resolution (the caller supplies the already-resolved URL).ensureAgentRepointoFleetLifecycleServicespawn (a later lifecycle integration).Decision Record impact
none— composes existing Brain-side helpers; no ADR interaction.Related
Live latest-open sweep: checked open issues + an ensure / orchestrate / compose-repo scan at 2026-06-14T00:49Z; no equivalent found (the one "orchestrator" hit is #12065, the unrelated REM pipeline epic).
Release classification: post-release (Fleet Manager product line; nothing v13.x-blocking).
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e