Repository navigation
Script the Local Half of Post-Merge Cleanup and Sweep Finished Worktrees in pr_review.py - #3186
Conversation
…ees in pr_review.py cleanup <n> --checkout <primary> now removes the merged branch's worktrees, deletes the local branch only while it names the merged head, and fast-forwards the base clone. cleanup --sweep reports, and with --apply removes, worktrees and local branches that are clean, idle past --min-age-hours, and merged at their exact tip or contained in origin.
The sweep's dry run names the remote branch it would delete. The primary is switched only off the merged branch, and a base clone that cannot return is reported without failing landed removals. Branches are deleted with git branch -D after a tip re-read, worktrees mid-operation are kept, prune honors the age rule, and paths are normalized for Windows.
…n Windows The base clone's return never fails a run whose removals landed, and an idempotent re-run exits 0. Windows refuses a worktree the calling shell sits in, worktrees with submodules are kept on both paths, a moved tip is a refusal, and scripts/README.md describes --checkout and --sweep.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved safety issues remain in cleanup ordering, stale-worktree pruning, concurrent operations, and dry-run accuracy.
12 open issues
Preflight local state before remote deletion · New Preflight untracked changes before switching primary · New Validate stale entries before pruning · New Document containment in any origin ref · New Treat concurrently removed worktrees as complete · New Re-read branches after failed deletion · New Run remote deletion checks during dry runs · New Show primary base updates in dry-run output · New Preserve positional arguments after CLI options · New Correct primary-checkout claim about base ref updates · New Document primary fast-forward during apply · New Assert sweep continues after unread items · New
What changed in this PR
This PR adds scripted local post-merge cleanup and stale worktree sweeping to pr_review.py.
Changes:
- Adds local branch, worktree, and base-branch cleanup.
- Adds dry-run and apply sweep modes with safety checks.
- Adds documentation and integration tests.
| File | Description |
|---|---|
tests/test_pr_review.py |
Tests cleanup, sweep, and CLI behavior. |
scripts/README.md |
Documents cleanup and sweep workflows. |
scripts/pr_review.py |
Implements cleanup, sweep, validation, and CLI handling. |
Review effort: Lite
|
A recorded local strict-review pass covers head |
…ep Step With --checkout, every local refusal is decided before the remote branch is deleted. The sweep's report runs the remote half's read-only checks and names the base fast-forward. A worktree or branch a concurrent run already removed counts as done, a primary that cannot leave the branch keeps it, and a pull request number after the options parses again.
The primary leaves the merged branch before the remote delete, so a failed switch deletes nothing. A rebase or bisect naming the branch in any worktree refuses first. A prunable entry holding the branch is removed without force, a local tip behind the merged head counts as finished, and a partly removed worktree is unconfirmed.
|
A recorded local strict-review pass covers head |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #3186 +/- ##
===========================================
+ Coverage 64.14% 65.46% +1.31%
===========================================
Files 17 17
Lines 9198 9749 +551
===========================================
+ Hits 5900 6382 +482
- Misses 3298 3367 +69
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…Operations The remote delete uses a fresh ref read taken just before it, and an origin mirror that differs from the merged head refuses first. A rebase or bisect in the primary refuses like one in a linked worktree. A report-only sweep fetches without pruning, the closing reads never fail a landed cleanup, and the docs state post-delete re-checks and the ancestor rule.
…weep The report and --apply judge containment against the same pruned remote-tracking refs. The sweep keeps a branch the primary is rebasing, both closing reads are covered, and the README and docstring read in order.
|
A recorded local strict-review pass covers head |
…Read The preflight refuses when the primary holding the merged branch is paused mid-operation. The sweep keeps a branch with no readable reflog, carries on past a failed prune, and reuses cleanup_behind for containment. The stale-ref test proves each mode's own prune.
|
A recorded local strict-review pass covers head |
- #3186 adds the local half of post-merge cleanup to `pr_review.py cleanup`. With `--checkout <base-clone>`, it removes the merged branch's worktrees and local branch after the remote delete and fast-forwards the base clone's `develop`. `cleanup --sweep` reports, and with `--apply` removes, the worktrees and branches an interrupted or abandoned session left behind. - #3188 points every skill that finishes a worktree at those commands, in place of the raw `git worktree remove`, `git branch -D`, and remote delete, which the host-safety allowlist does not cover. The skills are `repo-worktree`, `drive-pr`, `merge-and-release`, `backlog-burndown`, `pr-review-conduct`, `unattended-handoff`, `resync-a-repo`, and `audit-a-repo`. `git-commit-conventions`'s post-squash `-D` exception now admits a tip behind the verified head. - #3190 fixes a promotion-review finding. `cleanup --sweep` now judges a branch held only by a pruned worktree entry in the same run, rather than skipping it until the next. - #3191 fixes a second promotion-review finding. `unattended-handoff` step 7 no longer claims that no worktree outlives the lane, and it names a cleanup that could not run or that failed. Installed Skills change at release. Closes #3184 Closes #1682 Closes #1693 🤖 Generated with [Claude Code](https://claude.com/claude-code)



Summary
Part of #3184. Skill changes follow in a second pull request.
Agents leave their local cleanup to the maintainer. The guard does not block the raw git steps. They are simply not on the permission allowlist, while
python3 scripts/pr_review.py:*is. So an attended agent prompts for each step, and an unattended one cannot run them. This change moves the whole cleanup into the allowlisted script.cleanup <n> --repo O/R --checkout <primary>runs the local half after the remote half:git branch -Dafter re-reading the tip;developonly when it held that branch;develop.Every refusal is decided before the first removal. The base clone's outcome is reported and never fails a run whose removals landed.
cleanup --sweep --repo O/R --checkout <primary> [--apply] [--min-age-hours N]lists what crashed or abandoned sessions left behind, and removes it only with--apply.O/Rat its exact tip, or be contained inorigin.cleanup <n>.scripts/README.mddescribes both modes.Verification
python3 tests/test_pr_review.py: 906 tests, all pass, and the fullunittest discoversuite passes. Each new refusal test was confirmed to fail with its guard disabled.ruff check,ruff format --check,mypy, and the commit hooks' prose and EOL gates are clean.A dry run against a real clone with a long backlog of leftovers listed:
origin;It kept a worktree touched 20 minutes earlier by a live session in another repository, which is the case the age rule exists for.
A recorded local strict-review pass on every push. The last one, on the final head, raised no findings. The first raised 31 findings (14 of them sentence length), the second 10, the third 7. Two rounds of fixes answered the first two passes. Three consolidations were declined with evidence:
cleanup_gitvs the existing_git:_gitis documented read-only, swallows a failure into rc 1, and uses a 30s timeout.GITHUB_REMOTEvsorigin_owner:origin_ownerreads only the owner of the script's own clone, throughinsteadOf. This parser checks the full name of--checkout, raw.GITHUB_REMOTEvscarry.normalized_origin:normalized_originmaps two URL forms to an https URL for comparison and parses no owner or name.Open findings
None. The local pass on 235b035 raised no findings.
Findings from the local pass on 9463d0e
Each was fixed in 235b035:
checkout developcan clear that paused state.loose()ages a branch by its reflog alone and reads a missing reflog as infinitely old, so withcore.logAllRefUpdates=falseor the reftable backend a just-created branch passes the age rule.git worktree pruneunder--applystops the whole sweep, while the docstrings say the other items are still tried.test_a_stale_remote_ref_for_a_deleted_branch_is_not_containmentruns--applyafter the report already pruned the stale ref, so it never proves that--applyprunes on its own.finished()repeats the ancestor test inline, which the existingcleanup_behindhelper already provides.Findings from the local passes on 04b731f and 3dbc9d7
Each was fixed in 3dbc9d7 or 9463d0e:
git branch -Drefuses after the remote delete.--checkout, the remote delete uses a ref read taken before the fetch, the preflight and the checkout, which widens the window in which a push to the merged branch is deleted without theTIP_MOVEDrefusal.git fetch --prune originin the primary, which the docs do not list.git statusandworktree listreads incleanup_localcan raise after every removal landed, turning success into exit 2.sed -isyntax, which BSD sed on macOS reads differently.test_a_primary_holding_the_branch_moves_to_develop_before_the_deleteasserts only the end state, not the order.--prunejudged containment against stale refs that--applywould prune.worktree listread's guard was untested.Findings from the local pass on 9744aba
Each was fixed in 6defb7b:
git worktree removeis not read back. When a concurrent cleanup already removed the worktree, the run exits 2 instead of reading the list and continuing.LOCAL_TIP_MOVEDat exit 1, not as already gone. Inloose()the same case is labelledchecked-out-elsewhere.--applyfast-forwards itsdevelop, and the dry run does not list that.would-remove remote branchline runs none of the remote half's checks, so it can name a branch that--applykeeps or finds already gone.origin/developororigin/main", while a detached worktree qualifies when contained in anyoriginref, and the README says "contained inorigin".test_a_read_failure_continues_and_exits_2asserts nothing processed after the failing item.numbernow optional, Python 3.11's argparse rejects a number placed after the options, such asstatus --repo O/R 7. Confirmed on 3.11; 3.12 and later accept it. No caller in the tree uses that order.🤖 Generated with Claude Code