ai-git-cleanup · git:20260927.5da899e · 2026-09-27 · sha256 697987fd2497aa00
ai-git-cleanup git:20260927.5da899eA
Immutable. This exact content never changes and is served at /api/v1/blob/697987fd2497aa00.
--- name: ai-git-cleanup description: >- Evidence-gated cleanup of finished local git branches and worktrees. Runs the read-only analyzer, shows every delete recommendation with its evidence, and removes only what a person approves at two confirmation gates. Manual only: invoke by name when a feature is finished. Never touches remote branches, CI or protected branches. disable-model-invocation: true license: LicenseRef-Attributed --- # ai-git-cleanup — remove finished local branches with proof Runs only when the person invoked it by name. If nobody asked for a cleanup in this conversation, stop: do not analyze, do not delete. ## What it produces A full analysis table at gate 1, and at most the approved removals at gate 2: finished branches and worktrees gone, every refusal visible, nothing deleted that still holds the only copy of work. ## Safety invariants - **PROTECTED, the default branch and the current branch are never deletable.** The analyzer enforces this with a regex and by name. Any recommendation that names one of them is a bug: stop and report it. - **Every delete recommendation carries named evidence** — the tip commit, the PR, the superseding branch. A category without evidence is not a recommendation; it goes to needs review. - **Every refname and path is single-quoted in every command you emit.** Git refname rules allow `$(...)`, backticks, quotes, `;`, `|` and `&`; double quotes still execute the first two. An apostrophe escapes by closing and reopening: `'has'\''quote'`. - **`git merge-base --is-ancestor` runs immediately before every `git branch -d`** (the entry's `verifyWith`), naming `refs/heads/<branch>` and the default branch — never a reported sha. Guard failure skips the delete and reports it. - **A dirty worktree is refused**, never queued. It is removed only after the person explicitly acknowledges, in their own words, that the uncommitted changes are lost. - **`unanalyzed` is always surfaced**, even when empty. A partial analysis must never read as a complete one. - **Fail closed.** A refuted candidate, an unreachable refuter, a failed proof — all downgrade to needs review, never to deletable. - **Deletions run only in the main session.** Analysis subagents are strictly read-only and never ask the person anything. ## Steps 1. **Analyze, read-only.** Run [scripts/analyze.mjs](scripts/analyze.mjs) with an optional repo path (`node scripts/analyze.mjs [repo-path]`, defaulting to the current directory). It prints one JSON document: `deleteCandidates` (reason, evidence, command, `verifyWith` for `SAFE_TO_DELETE`, and `worktreePath` when a linked worktree holds the branch), `needsReview`, `keep` (including `PROTECTED`), `worktrees` (path, branch, dirty, dirtyFiles, stale, and `command` when stale), `unanalyzed`, `defaultBranch`, `currentBranch`; branch names that form a related group in any bucket also carry an informational `cluster`. An empty `defaultBranch` means no anchor was found: every branch fails closed to `unanalyzed` with reason `NO_DEFAULT_BRANCH`, never to a delete. It never mutates the repository. 2. **Investigate what git cannot prove, read-only.** Branches in `needsReview` or `unanalyzed`, and any squash-merged or superseded claim, go to read-only investigator subagents — at most five, spawned by the main session, **never nested**, and batched (one related group, one agent). They read [references/merge-evidence.md](references/merge-evidence.md) first and come back with a named PR or commit, or with nothing. Nothing? Needs review. 3. **Refute every non-`SAFE_TO_DELETE` delete claim.** Each claim (squash-merged, superseded) goes to one skeptic whose job is to find a commit the claim cannot account for — at most five, spawned by the main session, never nested, read-only. Refuted, unverified, missing a verdict, or lost to a failed agent: needs review. `SAFE_TO_DELETE` skips this only because step 5 re-proves it with `merge-base` immediately before the delete. 4. **Gate 1 — the full analysis table, then `ask`.** Show every bucket as a table with evidence columns: delete candidates (reason, evidence, `verifyWith`), needs review, keep, worktrees (dirty, stale, dirty files), and `unanalyzed` — always, even when it is empty. Then `ask`: delete all recommended, pick, or stop. Nothing is deleted before that answer. 5. **Gate 2 — exact commands, explicit yes, then `ask`.** Print the exact command plan in execution order: worktree removals first (`git worktree remove '<path>'`), then each guard paired with its delete (`git merge-base --is-ancestor 'refs/heads/<branch>' '<default>' && git branch -d '<branch>'`). `SAFE_TO_DELETE` is pinned to `-d`; `-D` appears only for a claim that named its PR or commit and survived step 3, with that evidence on screen. Then `ask` for an explicit `yes`. Anything but `yes` stops the run. 6. **Execute in the main session.** Run the plan yourself, in order: worktrees before branches, guard before every `-d`, skip and report any guard that fails. A subagent never executes a deletion and never asks the person anything. ## Out of scope - Remote branches: no `git push origin --delete`, ever. Local git only. - No reflog GC, no `git gc`, no CI, no auto-deletion without both gates. - Not a branching model: this cleans up branches another process created. ## Lifecycle Lane: any Writes: nothing Read by: the person approving the cleanup Dies: after each run; the report is a snapshot of that moment Next: none Source: adapted from the Trail of Bits git-cleanup plugin (https://github.com/trailofbits/skills/tree/main/plugins/git-cleanup), CC BY-SA 4.0.