feat(loop): auto-cleanup worker worktree + branch after merge (#91) - #112
Conversation
merge-ready.sh now cleans up the merged PR's local worktree and branch right after a successful merge, so .claude/worktrees/ and local branches no longer accumulate stale `prunable` entries forever. - merge-ready.sh: pull headRefName from the PR json, and after a successful merge shell out to the new worktree-cleanup.sh helper, tagging its JSON-lines output with the PR number. - worktree-cleanup.sh (new): <base-branch> [<branch>...] helper, reused as a small independently-testable unit. Safety rails, ALL required: worktree path matches the worker naming convention (.claude/worktrees/agent-* or issue-*), the worktree tree is clean, and the branch is fully merged into base. Never --force, never `git branch -D` — a refusal from `git worktree remove` / `git branch -d` is logged as a skip and left alone. - worktree-cleanup.test.sh (new): offline smoke test against a throwaway temp git repo with real `git worktree add` worktrees (no network, no gh). Covers: merged+clean+matching -> removed; dirty -> preserved; unmerged -> preserved; non-matching path (main worktree, and an arbitrary dir name) -> never touched; no-worktree branch -> no crash. Picked up automatically by .claude/self/checks.sh's *.test.sh glob. Co-Authored-By: Claude Sonnet 5 <[email protected]>
merge-ready.sh calls worktree-cleanup.sh immediately after `gh pr merge` succeeds, which merges on the remote only — local $base isn't fast-forwarded until the post-loop local_sync block that runs AFTER cleanup. The old local-only `git branch --merged $base` check therefore always saw the just-merged branch as unmerged, so cleanup never fired in production. Fetch origin/$base and prefer ancestry against it (falling back to the local check when origin is unreachable). When `git branch -d` refuses because local $base hasn't caught up yet, fall back to a direct ref delete — but only once merge-base --is-ancestor has independently verified the merge against origin/$base, so nothing is ever force-deleted. Adds worktree-cleanup.test.sh scenario 6, which reproduces the production ordering with a real bare "origin" repo and a merge pushed from a separate clone while local main stays stale. It fails against the pre-fix script and passes after. Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
Fixed the merged-ness ordering bug flagged by review:
Added scenario 6 to Gates: build/lint/test all green ( Commit: 933f202 |
Summary
Closes #91.
Worker worktrees + their local branches accumulated forever after a PR merged —
git worktree listhad many staleprunableentries, several pointing at a pre-rename path that no longer exists on disk. This adds auto-cleanup right aftermerge-ready.shmerges a PR.merge-ready.sh: pullsheadRefNamefrom the PR JSON, and after a successful merge shells out to the newworktree-cleanup.shhelper, tagging its JSON-lines output with the PR number.worktree-cleanup.sh(new):<base-branch> [<branch>...]helper, factored out so it's independently unit-testable offline. Safety rails — ALL required, or the branch is skipped with a logged reason and left alone:.claude/worktrees/agent-*or.claude/worktrees/issue-*) — never the main worktree or anything elsegit status --porcelainempty)git branch --merged <base>)--force; nevergit branch -D— a refusal fromgit worktree remove/git branch -d(dirty/locked/unmerged) is logged as a skip, not overriddenworktree-cleanup.test.sh(new): offline smoke test against a throwaway temp git repo with realgit worktree addworktrees (no network, nogh). Covers: merged+clean+matching → removed; dirty → preserved; unmerged → preserved; non-matching path (main worktree, and an arbitrary dir name) → never touched; no-worktree branch → no crash. Picked up automatically by.claude/self/checks.sh's*.test.shglob.One-off migration (issue #91 also asked for this, run manually, not part of this diff): ran
git worktree prune -von the reCode repo to clear dangling registrations under the old pre-renameai-project-orchestrator/.claude/worktrees/*path (directories no longer exist on disk). No in-flight reCode workers were touched.Cockpit bonus (issue #91, optional): skipped —
renderWorktrees()incockpit.shcurrently sources its data from a plainreaddirSyncof.claude/worktrees, notgit worktree list --porcelain, so flaggingprunableentries would mean switching its whole data model (and touchingcockpit.test.sh's assertions), not a trivial addition.Test plan
GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh build— passGATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh lint— passGATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh test— pass (includes newworktree-cleanup.test.sh, 22/22 checks, plussmoke-fanout.sh)bash .claude/scripts/worktree-cleanup.test.shrunnable standalone🤖 Generated with Claude Code