ADR-0069 — WorkspaceGCLoop: Autonomous Worktree Garbage Collection¶
Status: Proposed Date: 2026-05-19 Enforced by: tests/test_workspace_gc_loop.py
Context¶
The implementation phase creates a git worktree per issue via WorkspacePort.create. When a PR is merged, the post-work cleanup normally destroys the worktree. Three leak classes exist where cleanup does not run:
- A PR is merged manually via the GitHub UI (not through the orchestrator's merge path).
- A human resolves a HITL issue and closes the PR, bypassing the orchestrator.
- The orchestrator crashes or is restarted while a cleanup step is in flight.
Over time these leaks accumulate worktree directories on disk, orphaned branches on the remote, and stale StateTracker entries. The disk pressure and remote branch clutter are visible noise; the stale state entries can cause the pipeline to treat an issue as in-flight when it is not.
Decision¶
Introduce WorkspaceGCLoop, a BaseBackgroundLoop that runs a three-phase GC pass on every tick:
- Phase 1 — tracked workspaces: for each entry in
StateTracker.get_active_workspaces(), check whether the PR is merged/closed; if safe, remove the state entry and callWorkspacePort.destroy(). - Phase 2 — orphaned disk directories: scan the worktree root for directories that have no
StateTrackerentry and no open PR. - Phase 3 — orphaned remote branches: list remote
issue/*branches with no open PR and noStateTrackerentry.
Cap at _MAX_GC_PER_CYCLE = 20 collections per tick to avoid long-running passes. State removal precedes destroy() so a crash between the two steps leaves the entry gone rather than leaking permanently (destroy() is idempotent).
Kill-switch: enabled_cb("workspace_gc") AND config.workspace_gc_loop_enabled.
Consequences¶
- Worktree leaks become self-healing; operators do not need to run manual
git worktree prunecommands. - The pipeline's active-workspace view in
StateTrackerreflects reality within one GC interval. - The 20-per-cycle cap means large backlogs drain gradually; acceptable because GC is low-priority background work.
Alternatives considered¶
- GC at merge-path only. Already the first line of defense, but does not cover manual merges, HITL closures, or crash-mid-cleanup.
- Cron script outside the orchestrator. Possible but adds an out-of-process dependency; the orchestrator already has the state context needed to decide what's safe to GC.