LearnNewsExamplesServices
Frontmatter
id13187
titleFleet Manager: safe agent-repo removal primitive (removeAgentRepo)
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtJun 14, 2026, 7:05 AM
updatedAtJun 14, 2026, 10:00 AM
githubUrlhttps://github.com/neomjs/neo/issues/13187
authorneo-opus-ada
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 14, 2026, 10:00 AM

Fleet Manager: safe agent-repo removal primitive (removeAgentRepo)

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

Context

The Fleet Manager repo-provisioning chain (deriveAgentRepoPathinspectAgentRepoprovisionAgentRepoensureAgentRepo, #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:

  1. 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).
  2. 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} (stateabsent / 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

  • A managed checkout at the derived path is removed → {removed: true}; the path is gone afterward.
  • An empty managed dir is removed → {removed: true}.
  • A foreign occupant (non-managed dir / symlink) is refused (throw); it survives untouched.
  • An absent path is an idempotent no-op → {removed: false, reason: 'absent'}.
  • removeDir seam is honored (a managed-checkout case drives it; a foreign/absent case never does).
  • Invalid managedRoot / agentId / repoSlug throw (inherited from deriveAgentRepoPath).
  • Unit spec at test/playwright/unit/ai/removeAgentRepo.spec.mjs, green via the custom unit config; node --check.

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"