---
id: 12677
title: 'Simplify worktree prune: keep current, delete the rest, hydrate'
state: CLOSED
labels:
  - enhancement
  - ai
  - refactoring
assignees:
  - neo-gpt
createdAt: '2026-06-07T05:58:45Z'
updatedAt: '2026-06-07T16:25:34Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12677'
author: neo-claude-opus
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-06-07T16:25:34Z'
---
# Simplify worktree prune: keep current, delete the rest, hydrate

## Context

ada's git-worktree sprawl (>20–50, manually deleted by @tobiu on 2026-06-07) is a standing system-load source: each worktree under `.claude/worktrees/` is a full checkout that WebStorm re-indexes, `fsnotifier` watches, and the KB-sync can re-embed. Tonight that compounded into WebStorm runaways (1100–1314% CPU, 160–246 threads) + memory pressure (~3.4G free) + MC embedder saturation. The cleanup that should prevent the sprawl doesn't fire and doesn't delete.

The existing prune — `ai:prune-worktrees` → `node ai/scripts/migrations/bootstrapWorktree.mjs --prune-stale` (`pruneStaleWorktrees()`) — fails for three reasons:
1. The npm script (`package.json:47`) runs `--prune-stale` **without `--apply`** → dry-run; it reports prunable worktrees but deletes nothing.
2. No auto-trigger exists in the tracked repo (no launchd/cron/orchestrator/git-hook; no `>50` threshold caller — that's harness-private/unwired).
3. The criteria preserve too much: prunable = branch merged/patch-equivalent to base, or branch gone (`bootstrapWorktree.mjs` ~654–744). Dirty/unmerged/active worktrees are kept indefinitely.

@tobiu's framing: the merge-classification / PR-status / dirty taxonomy is over-careful — *"typical opus-4.6 model code"* — and is itself the root cause. By trying never to delete something potentially-valuable, it preserves exactly the abandoned worktrees that accumulate into sprawl.

## The Problem

A git worktree is a **disposable checkout**: committed work lives in branches/remotes, not in the worktree. So "preserve unmerged/dirty worktrees" mostly preserves abandoned cruft — the sprawl. The safety machinery creates the problem it's trying to avoid. The correct model is simple and aggressive: **keep only the current/active worktree; delete the rest; hydrate the current.**

## The Architectural Reality

- `ai/scripts/migrations/bootstrapWorktree.mjs`:
  - `pruneStaleWorktrees()` (~766) — prune entry; default never-mutates (dry-run; `apply=false`, ~751–757).
  - Merge-classification to **remove**: `isMergedToBase()`, PR-status gating, the dirty/unmerged/`prunable-merged` taxonomy + `removeArgs` (~654–744).
  - Hydrate side to **reuse**: `--link-data` / `materializeServerConfigTemplate` / symlinked subdirs (~137/335/483).
  - Current-worktree resolution via `git worktree list --porcelain` (~575).
- `package.json:47` — `ai:prune-worktrees` (currently dry-run).
- Worktrees live under `.claude/worktrees/<name>/` (per #11654).

## The Fix

Replace the careful classification with a flat keep-current / delete-rest + hydrate:
1. `git worktree list --porcelain` → identify the **current/active** worktree.
2. For every other worktree path → `git worktree remove --force <path>` (force = no merge/dirty gate).
3. Run the existing **hydrate** on the current worktree (`--link-data` + symlink/config-template setup).
4. Make the prune **delete by default** (`--apply` semantics); demote dry-run to an explicit `--dry-run` opt-in; update `ai:prune-worktrees`.
5. Add a **scheduled** trigger (launchd/cron, or a documented periodic invocation) — not a count-threshold hook.
6. **Delete** `isMergedToBase`, the PR-status checks, and the dirty/unmerged/age taxonomy — net code reduction.

## Contract Ledger

| Target Surface | Source of Authority | Proposed Behavior | Fallback | Evidence |
|---|---|---|---|---|
| `ai:prune-worktrees` (npm) | `package.json:47` | dry-run → **deletes all-but-current + hydrates** | `--dry-run` opt-in | this ticket |
| `--prune-stale` flag | `bootstrapWorktree.mjs` | criteria merged-only → **all-but-current** | current-worktree guard | ~654-766 |
| scheduled prune trigger | new (launchd/cron) | none → interval-based run | manual `npm run` | this ticket |

## Decision Record impact

`none` (no ADR governs worktree pruning). Supersedes the merge-based approach from #11654 (closed).

## Acceptance Criteria

- [ ] `--prune-stale` removes ALL worktrees except the current/active one (`git worktree remove --force`), then hydrates the current via the existing `--link-data`/symlink setup.
- [ ] The current/active worktree is **never** removed — explicit guard + a unit test asserting it.
- [ ] Merge-classification / PR-status / dirty taxonomy / any age-TTL logic is **removed** (net deletion; the diff is smaller than the code it replaces).
- [ ] Prune **deletes by default** (`--apply` semantics); dry-run is an explicit `--dry-run`; `ai:prune-worktrees` updated + documented.
- [ ] A scheduled trigger runs the prune on an interval (not a count-threshold hook).
- [ ] Unit test: keep-current/delete-rest classification over a mocked `git worktree list --porcelain`.

## Out of Scope

- The harness-private ">50 hook" (ada's side) — this ticket fixes the tracked script + a scheduled trigger; the harness hook can simply call the corrected script.
- The broader OUR-team-vs-fork `.agents/workflows` separation (#11837 successor).
- The miniSummary backfill (#12673) / context-recovery skill (#12674) — sibling recency work, unrelated.

## Avoided Traps / Gold Standards Rejected

- **Do NOT re-introduce merge/age/PR "safety" classification.** That over-caution is what produced the sprawl; a worktree is disposable (committed work is in branches/remotes). Simpler-aggressive is the correct default here, not a risk.
- **Do NOT gate on a count threshold** (fragile; fires only on a bound event). A scheduled interval is robust.

## Related

- Supersedes the merge-based prune from #11654 (closed) + #10432 / PR #10433.
- Systemic root of the recurring load this session (worktree sprawl → WebStorm reindex / `fsnotifier` / KB re-embed).
- Coordinate with @neo-opus-ada (author of `bootstrapWorktree.mjs`).

Origin Session ID: c28bcd14-850c-45e3-aae7-3655cdb7127c

Handoff Retrieval Hints: `query_raw_memories` "worktree prune delete-all-except-current hydrate over-careful"; files `ai/scripts/migrations/bootstrapWorktree.mjs` (`pruneStaleWorktrees`), `package.json` (`ai:prune-worktrees`).

Authored by Claude (Opus 4.8, neo-claude-opus). Diagnosed this session; design corrected by @tobiu (drop the over-careful merge-classification).

## Timeline

- 2026-06-07T05:58:46Z @neo-claude-opus added the `enhancement` label
- 2026-06-07T05:58:46Z @neo-claude-opus added the `ai` label
- 2026-06-07T05:58:46Z @neo-claude-opus added the `refactoring` label
- 2026-06-07T07:06:51Z @neo-gpt assigned to @neo-gpt
- 2026-06-07T07:17:30Z @neo-gpt cross-referenced by PR #12678
- 2026-06-07T07:27:59Z @neo-gpt referenced in commit `4e797f7` - "feat(ai): simplify worktree pruning (#12677)"
- 2026-06-07T07:39:14Z @neo-gpt referenced in commit `63f4a15` - "feat(ai): simplify worktree pruning (#12677)"
- 2026-06-07T16:25:34Z @tobiu closed this issue
- 2026-06-07T16:25:34Z @tobiu referenced in commit `bc3989d` - "feat(ai): simplify worktree pruning (#12677) (#12678)"

