Skip to content

feat(loop): auto-cleanup worker worktree + branch after merge (#91) - #112

Merged
robercano-ghbot merged 2 commits into
mainfrom
feat/issue-91-worktree-hygiene
Jul 10, 2026
Merged

feat(loop): auto-cleanup worker worktree + branch after merge (#91)#112
robercano-ghbot merged 2 commits into
mainfrom
feat/issue-91-worktree-hygiene

Conversation

@robercano-ghbot

Copy link
Copy Markdown
Collaborator

Summary

Closes #91.

Worker worktrees + their local branches accumulated forever after a PR merged — git worktree list had many stale prunable entries, several pointing at a pre-rename path that no longer exists on disk. This adds auto-cleanup right after merge-ready.sh merges a PR.

  • merge-ready.sh: pulls headRefName from the PR JSON, and after a successful merge shells 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, 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:
    • worktree path matches the worker naming convention (.claude/worktrees/agent-* or .claude/worktrees/issue-*) — never the main worktree or anything else
    • the worktree tree is clean (git status --porcelain empty)
    • the branch is fully merged into base (git branch --merged <base>)
    • never --force; never git branch -D — a refusal from git worktree remove / git branch -d (dirty/locked/unmerged) is logged as a skip, not overridden
  • 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.

One-off migration (issue #91 also asked for this, run manually, not part of this diff): ran git worktree prune -v on the reCode repo to clear dangling registrations under the old pre-rename ai-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() in cockpit.sh currently sources its data from a plain readdirSync of .claude/worktrees, not git worktree list --porcelain, so flagging prunable entries would mean switching its whole data model (and touching cockpit.test.sh's assertions), not a trivial addition.

Test plan

  • GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh build — pass
  • GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh lint — pass
  • GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh test — pass (includes new worktree-cleanup.test.sh, 22/22 checks, plus smoke-fanout.sh)
  • bash .claude/scripts/worktree-cleanup.test.sh runnable standalone

🤖 Generated with Claude Code

robercano and others added 2 commits July 10, 2026 12:01
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]>
@robercano-ghbot

Copy link
Copy Markdown
Collaborator Author

Fixed the merged-ness ordering bug flagged by review:

worktree-cleanup.sh now fetches origin/$base and checks ancestry against it (falling back to the old local git branch --merged check if origin is unreachable), instead of relying solely on local $base — which isn't fast-forwarded until merge-ready.sh's post-loop local_sync block, i.e. AFTER cleanup runs. git branch -d is still tried first; if it refuses because local $base hasn't caught up, a direct ref delete is used, but only once merge-base --is-ancestor has independently verified the merge against origin/$base — never -D/--force.

Added scenario 6 to worktree-cleanup.test.sh, which reproduces the exact production ordering (merge pushed to a real bare "origin" from a separate clone, local main deliberately left stale) — confirmed it fails against the pre-fix script and passes after. All 29 checks pass (was 23).

Gates: build/lint/test all green (GATES_FILE=.claude/self/gates.json bash .claude/scripts/gate.sh build|lint|test).

Commit: 933f202

@robercano-ghbot
robercano-ghbot merged commit 3bcaff4 into main Jul 10, 2026
9 checks passed
@robercano-ghbot
robercano-ghbot deleted the feat/issue-91-worktree-hygiene branch July 10, 2026 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Worktree hygiene: auto-remove worker worktrees (and branches) after their PR merges

2 participants