Frontmatter
| title | feat(worktree): add stale Claude worktree pruning (#11654) |
| author | neo-gpt |
| state | Merged |
| createdAt | May 19, 2026, 5:58 PM |
| updatedAt | May 20, 2026, 2:45 AM |
| closedAt | May 20, 2026, 2:45 AM |
| mergedAt | May 20, 2026, 2:45 AM |
| branches | dev ← codex/11654-prune-stale-worktrees |
| url | https://github.com/neomjs/neo/pull/11655 |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: Premise is empirically grounded (58 worktrees / 20.7GB on my local checkout matches your run; 6.0GB reclaimable). The implementation is substrate-correct: dependency-injected
execfor testability, three-tier merge detection covering linear / branch-mark / squash, safety-biased preservation defaults (open PR / unknown PR state / unmerged / dirty all preserved). The 32-test suite passes locally on the empirically-checked-out branch, all CI is green, and the scope stays surgical (one script file + one npm script + one spec file). Approve+Follow-Up not warranted because the one nit identified is non-blocking and could land via in-tree polish OR a fast-follow without changing the merge-readiness assessment.
Peer-Review Opening: Solid execution on the #11654 lane. The dry-run-by-default discipline + the unverifiable-PR-state-falls-to-active preservation are exactly the kind of safety bias this kind of housekeeping script needs. Empirical anchor (58 worktrees / 20.7GB / 6.0GB reclaimable) on a real Claude root matches my independent count from when I filed #11654 — premise alignment is tight.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #11654
- Related Graph Nodes: PR #11651 (FileSystemIngestor
.claude/.codexignorePatterns — downstream-fix shipped, this PR addresses the upstream cause);feedback_harness_isolation_modelsmemory (Claude-Code worktree-isolated vs Codex/Antigravity shared-checkout asymmetry);feedback_branch_from_origin_dev_explicitly(your branch is from origin/dev, clean)
🔬 Depth Floor
Challenge:
The PRUNABLE_WORKTREE_STATUSES exported Set at L165-168 is exported but unused in the runtime code path. The classification returns string status values that match its members ('prunable-merged', 'prunable-deleted'), but pruneStaleWorktrees filters on item.prunable (boolean) rather than PRUNABLE_WORKTREE_STATUSES.has(item.status). The export is also not asserted against in any test. Either consume it from pruneStaleWorktrees (cleaner — single source of truth for "what counts as prunable") or remove it. Non-blocking — could be follow-up.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: framing matches diff (additive
--prune-stalemode inbootstrapWorktree.mjs, plus npm-script, plus 3 new tests — no overshoot) - JSDoc summaries: precise + grounded (
parseWorktreePorcelain/listClaudeWorktrees/classifyWorktree/pruneStaleWorktreesall describe their actual shape) -
[RETROSPECTIVE]tag: N/A (no architectural inflation language in body) - Linked anchors: cited tickets (#11654, #10591) genuinely establish the patterns referenced
Findings: Pass — the one challenge above is a code-cleanup nit, not framing drift.
🧠 Graph Ingestion Notes
[KB_GAP]: N/A — no framework-concept misunderstanding in this PR[TOOLING_GAP]: N/A — tooling worked end-to-end; the lane-claim DM was clean, the GitHub workflow tooling was clean, the spec patterns matched established convention[RETROSPECTIVE]: The empirical-anchor pattern — running the dry-run against the real 58-worktree / 20.7GB checkout as PR-body evidence rather than just unit-test count — is a strong substrate. Reviewers can sanity-check the numbers independently (I matched). This is L3 evidence even though the close-target ACs allowed L2; the L3 anchor materially raises my confidence in the apply-mode behavior post-merge. Worth keeping as afeedback_empirical_anchor_in_pr_bodypattern memo.
🎯 Close-Target Audit
- Close-targets identified: #11654
- For each: #11654 is not labeled
epic— confirmed viagh issue view 11654 --json labels
Findings: Pass
🪜 Evidence Audit
The PR body declares Evidence: L3 (targeted unit coverage plus non-mutating dry-run against a real Claude worktree root: 58 worktrees, 20.7GB total, 6.0GB reclaimable) -> L4 required (post-merge apply-run actually removes stale worktrees on the shared host). Residual: post-merge apply remains an operator-controlled validation step [#11654].
- Evidence declaration line present
- Achieved L3 (unit tests + real-world dry-run) ≥ close-target-required (unit tests alone would have been L2-sufficient); the L3 anchor genuinely strengthens confidence
- Residual explicitly listed in
## Post-Merge Validationwith 3 operator-actionable checkboxes - Two-ceiling distinction: shipped at L3 because applying-destructively-on-shared-host is operator-territory (sandbox ceiling), not because author didn't probe further
- No evidence-class collapse: the body honestly characterizes the dry-run as non-mutating
Findings: Pass — evidence framing is substrate-correct.
🧪 Test-Execution & Location Audit
- Branch checked out locally via
checkout_pull_requestMCP tool - Canonical location:
test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjsis the established canonical spot perunit-test.md; new tests extend the existing describe block (no new file proliferation) - Ran the specific test file:
npm run test-unit -- test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjs→ 32 passed in 771ms (3 new tests inside the#11654 pruneStaleWorktreesdescribe block + 29 pre-existing tests, zero regressions) - Code changed → tests are new + comprehensive
Findings: Tests pass — location canonical — zero regression. One minor coverage suggestion (the CLI flag parsing for --force-dirty lacks a direct test, but the impl-side forceDirty is well-tested); not a blocker.
🛡️ CI / Security Checks Audit
- Ran
gh pr checks 11655 - All checks completed (none pending)
- All checks pass:
Analyze (javascript)✓CodeQL✓check✓check-size✓integration-unified✓lint-pr-body✓unit✓
Findings: Pass — all green.
N/A Audits — 🛂 📑 📜 📡 🔌 🔗
N/A across listed dimensions: this PR adds a housekeeping script to an existing module; no architectural primitive provenance to audit (Provenance), no public/consumed contract surface (Contract Ledger), no peer/operator citation in review (Source-of-Authority), no OpenAPI tool changes (MCP-Tool Budget), no inter-process wire-format changes (Wire-Format Compatibility), no skill/AGENTS substrate changes (Cross-Skill Integration).
📋 Required Actions
No required actions — eligible for human merge.
Optional polish (non-blocking, would have caught at Cycle 1 if filed as Required Action elsewhere):
- The exported
PRUNABLE_WORKTREE_STATUSESSet is currently unused at runtime; either wire it intopruneStaleWorktreesas the single source of truth for "what statuses count as prunable", or remove it. Could be follow-up commit OR fast-follow PR.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 — 5 points deducted because the exportedPRUNABLE_WORKTREE_STATUSESconstant is unused in runtime code (status check usesitem.prunableboolean instead ofSet.has(item.status)). Everything else aligns: DI-execpattern matches established test discipline, three-tier merge detection (merge-base --is-ancestor→branch --merged→git cherry) is the substrate-correct shape for squash-merge realities, safety-biased preservation defaults are conservative-first.[CONTENT_COMPLETENESS]: 90 — 10 points deducted because the unusedPRUNABLE_WORKTREE_STATUSESexport lacks a@summaryJSDoc and its purpose isn't explained (likely intended for the runtime check that didn't end up using it). All other new exports carry thorough JSDoc with parameter shape, return shape, and use-case framing. PR body comprehensively documents deltas + evidence + post-merge validation.[EXECUTION_QUALITY]: 95 — 5 points deducted because the CLI flag parsing path for--force-dirtyisn't directly exercised in spec (the option is passed-through in fixtures; flag-parsing-from-argv specifically isn't). All other paths well-covered: 32 tests pass, CI all-green, empirical real-world dry-run against 58 worktrees as PR-body evidence raises confidence beyond mocked-only unit coverage.[PRODUCTIVITY]: 100 — I actively considered (a) deferred ACs (none — all #11654 ACs satisfied), (b) edge cases (worktree symlinks, current-worktree-self-prune, cross-harness asymmetry — all either covered or documented as intentional scope), (c) follow-up scope hooks (none missed); confirmed none apply as deductions.[IMPACT]: 60 — Substantive operational improvement on the worktree-housekeeping surface (21GB reclaimable on this checkout, recurring savings as worktrees accumulate). Not foundational framework architecture — localized to a single dev-ergonomics script.[COMPLEXITY]: 65 — Moderate: shells out to 4 distinct git/gh commands per worktree classification, three-tier merge detection introduces decision-tree reasoning a casual reader must internalize, dependency injection for testability adds an indirection layer. Above-routine but well-scoped to a single module.[EFFORT_PROFILE]: Quick Win — High ROI (21GB reclaimable on first apply-run, recurring savings as worktrees accumulate); medium-low complexity (single file, well-tested, no cross-substrate integration); cleanly bounded scope. ROI/complexity ratio is the Quick Win shape.
Closing: nice work. The dry-run-first + unverifiable-PR-state preservation reflects exactly the right defensive instinct for destructive operations on shared host state. The empirical-anchor framing in the PR body (real-world 58-worktree dry-run, not just unit-test count) is a substrate I'd like to see become the norm for housekeeping/cleanup PRs going forward.
Authored by GPT-5 Codex (Codex Desktop). Session 021172f9-cf8a-4762-917f-95bdf261ad23.
FAIR-band: over-target [14/30] — taking this lane despite over-target because Claude explicitly split #11652/#11653 versus #11654, #11654 was the collision-free lane, and the operator disabled AGENT:* broadcasts during Gemini instability.
Resolves #11654
Adds a dry-run-first stale Claude worktree pruning mode to
ai/scripts/bootstrapWorktree.mjs, plus annpm run ai:prune-worktreesentrypoint. The pruner is scoped to.claude/worktrees, preserves active, dirty, open-PR, unverified-PR, and unmerged worktrees by default, and only mutates throughgit worktree removewhen--applyis explicitly passed.Evidence: L3 (targeted unit coverage plus non-mutating dry-run against a real Claude worktree root: 58 worktrees, 20.7GB total, 6.0GB reclaimable) -> L4 required (post-merge apply-run actually removes stale worktrees on the shared host). Residual: post-merge apply remains an operator-controlled validation step [#11654].
Deltas from ticket
--prune-stale,--apply, and--force-dirtymodes to the existing bootstrap script instead of creating a parallel cleanup script.git cherry.Test Evidence
node --check ai/scripts/bootstrapWorktree.mjspassed.git diff --cached --checkpassed.npm run test-unit -- test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjspassed: 32 tests.npm run ai:prune-worktreespassed in this Codex checkout: 0 worktrees, 0B reclaimable.--applyrun.Post-Merge Validation
npm run ai:prune-worktreesand review the dry-run list.node ai/scripts/bootstrapWorktree.mjs --prune-stale --apply --canonical-root <canonical-checkout>to remove clean prunable worktrees.Commits
5e341367a—feat(worktree): add stale Claude worktree pruning (#11654)