Context
The Fleet Manager repo-provisioning chain (deriveAgentRepoPath → inspectAgentRepo → provisionAgentRepo → ensureAgentRepo, #13145 / #13148 / #13155 / #13162) lands an agent's managed checkout. It has no removal counterpart. A real fleet adds and removes agents over time; today, removing an agent (FleetRegistryService.removeAgent) leaves its provisioned checkout on disk forever — managed checkouts accumulate, and a stale leftover checkout at a derived path can confuse a later re-provision. This is the cleanup half of the provisioning lifecycle.
The Problem
Removal is destructive and safety-critical in two ways:
- It must never remove anything the Fleet Manager did not provision — a foreign occupant at the derived path (a symlink, a non-managed dir) must be refused, never clobbered (the mirror of provisioning's never-clobber).
- It interacts with the path-keyed auto-memory model: deleting a checkout orphans memory keyed on that path. So the policy of when to remove — including whether to preserve memory — is the caller's concern, not this primitive's. This primitive is a mechanism (safe removal on demand), never auto-wired into
removeAgent.
The Architectural Reality
ai/services/fleet/deriveAgentRepoPath.mjs — the stable, traversal-safe {managedRoot, agentId, repoSlug} → checkout path (containment guaranteed).
ai/services/fleet/inspectAgentRepo.mjs — read-only on-disk classifier: {exists, isCheckout, state, provisioningAction} (state ∈ absent / empty / checkout / occupied-non-checkout; symlink/foreign → occupied-non-checkout).
- Composing them is the safe-removal mechanism: derive the contained path, classify it, and remove only a managed checkout (or an empty managed dir), refusing anything else.
The Fix
ai/services/fleet/removeAgentRepo.mjs (new, standalone — sibling to ensureAgentRepo): removeAgentRepo({managedRoot, agentId, repoSlug, inspect = inspectAgentRepo, removeDir = <fs.rmSync wrapper>}):
- derive the contained path →
inspect;
absent → {removed: false, reason: 'absent'} (idempotent no-op);
checkout (our clone) or empty (our pre-clone dir) → removeDir(repoPath) → {removed: true, state};
occupied-non-checkout (a foreign occupant / symlink) → throw (never remove what we did not provision — fail-closed);
removeDir is an injectable seam (default-real recursive remove) so the contract is unit-testable without touching real trees beyond the test's own temp fixtures.
- Documented as destructive + caller-driven: it removes the agent's managed checkout including any uncommitted work; the when (and any auto-memory preservation policy) is the caller's. Never auto-invoked.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
removeAgentRepo({…}) (new) |
this ticket |
remove ONLY a managed checkout/empty at the derived path; refuse foreign; no-op on absent |
throw on foreign occupant (fail-closed) |
module JSDoc + spec |
inspectAgentRepo states verified on origin/dev (absent/empty/checkout/occupied-non-checkout) |
Decision Record impact
none — aligned-with the #13015 MVP decomposition (the removal half of the repo lifecycle); mirrors ensureAgentRepo's never-clobber with a never-remove-the-unmanaged guarantee.
Acceptance Criteria
Out of Scope
- Auto-wiring into
FleetRegistryService.removeAgent — that is a policy decision (when to clean up + whether to preserve path-keyed auto-memory) owned by the caller / control-plane surface, not this mechanism.
- Archiving / moving a checkout instead of deleting (this removes).
- Auto-memory association cleanup (a Memory-Core concern).
- The turnkey caller /
managedRoot resolution (separate FM control-plane work).
Related
- Parent epic #13015 (FM MVP). The removal counterpart to
ensureAgentRepo (#13162); consumes deriveAgentRepoPath (#13145) + inspectAgentRepo (#13148).
Live latest-open sweep: checked the live tracker + in-flight A2A claims immediately before filing; no equivalent (the only recent claim, Vega's #9847 remove_component NL tool, is an unrelated VDom-tool lane).
Authored by Claude Opus 4.8 (Claude Code), @neo-opus-ada.
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e
Retrieval Hint: "Fleet Manager removeAgentRepo safe repo removal cleanup counterpart ensureAgentRepo"
Context
The Fleet Manager repo-provisioning chain (
deriveAgentRepoPath→inspectAgentRepo→provisionAgentRepo→ensureAgentRepo, #13145 / #13148 / #13155 / #13162) lands an agent's managed checkout. It has no removal counterpart. A real fleet adds and removes agents over time; today, removing an agent (FleetRegistryService.removeAgent) leaves its provisioned checkout on disk forever — managed checkouts accumulate, and a stale leftover checkout at a derived path can confuse a later re-provision. This is the cleanup half of the provisioning lifecycle.The Problem
Removal is destructive and safety-critical in two ways:
removeAgent.The Architectural Reality
ai/services/fleet/deriveAgentRepoPath.mjs— the stable, traversal-safe{managedRoot, agentId, repoSlug}→ checkout path (containment guaranteed).ai/services/fleet/inspectAgentRepo.mjs— read-only on-disk classifier:{exists, isCheckout, state, provisioningAction}(state∈absent/empty/checkout/occupied-non-checkout; symlink/foreign →occupied-non-checkout).The Fix
ai/services/fleet/removeAgentRepo.mjs(new, standalone — sibling toensureAgentRepo):removeAgentRepo({managedRoot, agentId, repoSlug, inspect = inspectAgentRepo, removeDir = <fs.rmSync wrapper>}):inspect;absent→{removed: false, reason: 'absent'}(idempotent no-op);checkout(our clone) orempty(our pre-clone dir) →removeDir(repoPath)→{removed: true, state};occupied-non-checkout(a foreign occupant / symlink) → throw (never remove what we did not provision — fail-closed);removeDiris an injectable seam (default-real recursive remove) so the contract is unit-testable without touching real trees beyond the test's own temp fixtures.Contract Ledger Matrix
removeAgentRepo({…})(new)inspectAgentRepostates verified onorigin/dev(absent/empty/checkout/occupied-non-checkout)Decision Record impact
none— aligned-with the #13015 MVP decomposition (the removal half of the repo lifecycle); mirrorsensureAgentRepo's never-clobber with a never-remove-the-unmanaged guarantee.Acceptance Criteria
{removed: true}; the path is gone afterward.{removed: true}.{removed: false, reason: 'absent'}.removeDirseam is honored (a managed-checkout case drives it; a foreign/absent case never does).managedRoot/agentId/repoSlugthrow (inherited fromderiveAgentRepoPath).test/playwright/unit/ai/removeAgentRepo.spec.mjs, green via the custom unit config;node --check.Out of Scope
FleetRegistryService.removeAgent— that is a policy decision (when to clean up + whether to preserve path-keyed auto-memory) owned by the caller / control-plane surface, not this mechanism.managedRootresolution (separate FM control-plane work).Related
ensureAgentRepo(#13162); consumesderiveAgentRepoPath(#13145) +inspectAgentRepo(#13148).Live latest-open sweep: checked the live tracker + in-flight A2A claims immediately before filing; no equivalent (the only recent claim, Vega's #9847 remove_component NL tool, is an unrelated VDom-tool lane).
Authored by Claude Opus 4.8 (Claude Code), @neo-opus-ada. Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e Retrieval Hint: "Fleet Manager removeAgentRepo safe repo removal cleanup counterpart ensureAgentRepo"