feature(repo_cleanup): make branch deletion reachable from a cloud session - #278
Merged
Conversation
…ssion
A cloud Claude session — phone or claude.ai/code — can audit branches but
cannot remove one: `git push origin --delete` returns 403 for the session
credential (the proxy logs no relay failure, so it is GitHub's policy, not
egress), and the GitHub tool surface those sessions get has no delete-ref
call at all. Branch cleanup was therefore laptop-only, and it showed: a sweep
on 2026-08-25 found 233 branches across Mind and Brain, 188 of them provably
spent, with nothing able to act on the finding.
A workflow's GITHUB_TOKEN is a different credential, and these repos already
trust it with `contents: write` — dashboard_refresh.yml commits to main with
it. So the sweep now runs inside the repo on Actions, and any surface that can
dispatch a workflow can drive it.
- `bin/branch_sweep.sh` — the executable half of the skill. Classifies every
remote branch via branch_contribution.sh (never a hand-rolled ahead-count,
per agent_failure_modes D1/D2) and deletes only what is provably contained.
Two extra pieces of care:
* It unshallows first. On a truncated history every ancestry question is
wrong in the same direction — nothing looks contained — so a shallow
clone would silently sweep nothing while reporting success. This was not
hypothetical: the audit that motivated this ran shallow first and read 94
plainly-merged branches as unmerged.
* A CONTRIBUTES branch may still be a squash-merge git 2.34 cannot see
through. It asks GitHub for a merged PR at that exact tip, then falls
back to proving the squash locally by patch-id. Unproven means kept.
- `.github/workflows/branch_sweep.yml` — dispatch (audit|delete, optional
limit) plus a weekly audit-only cron; a scheduled run cannot delete whatever
the cron says. The step summary carries the report, which is the readable
surface on a phone.
- `tests/test_branch_sweep.py` — known-answer repos for the four gates that
keep a branch out of the delete set: main, `archive/condemned/*` Gut transit
refs, open PR heads, and unproven content. The deletions are one push; the
refusals are the part worth pinning.
- The skill now routes cloud sessions to the dispatch instead of telling them
the sweep is blocked, and says once that a repo still accumulating merged
heads has "Automatically delete head branches" off — the setting that stops
this backlog forming, which no sweep substitutes for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KwqicJpMqmcT5RVbyNwdKq
CI caught two things the local run could not. 1. The shallow guard tested `-f "$(git -C $REPO rev-parse --git-dir)/shallow"`. With `-C`, `--git-dir` comes back *relative* (`.git`), so that path resolves against the calling process's cwd — the guard was really asking "is the directory I happen to be standing in a shallow clone?". On an Actions runner the answer is yes, because actions/checkout is shallow by default, so the sweep tried to unshallow a complete repository and died on `fatal: --unshallow on a complete repository does not make sense`. Now it asks `rev-parse --is-shallow-repository`, the purpose-built predicate, which is about the repo named by `-C` and nothing else. Reproduced first: a cwd carrying `.git/shallow` makes the old expression true and the new one false for the same complete repo. Worth noting the failure mode was the safe direction by luck, not design — it aborted rather than sweeping on bad verdicts. The next caller might not be so lucky. 2. Prerequisite ordering. The gh check ran *after* the fetch/unshallow, so a run with no gh rewrote the caller's clone and only then refused. Moved both the presence and auth checks above any git mutation: if we are going to say no, say it before touching anything. The test's deliberately-lean PATH gained the coreutils the script actually calls (dirname and friends); without them the no-gh test was exercising a missing-dirname crash rather than the refusal it claims to pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KwqicJpMqmcT5RVbyNwdKq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A cloud Claude session — phone, or claude.ai/code — can audit branches but cannot remove one.
git push origin --deletereturns 403 for the session credential (the agent proxy logs no relay failure, so this is GitHub's policy, not egress), and the GitHub tool surface those sessions get has no delete-ref call at all. Branch cleanup was therefore laptop-only.It showed. A
/repo_cleanupsweep on 2026-08-25 found 233 branches across Mind and Brain, 188 of them provably spent, and could act on none of it.A workflow's
GITHUB_TOKENis a different credential, and these repos already trust it withcontents: write—dashboard_refresh.ymlcommits tomainwith it. So the sweep now runs inside the repo on Actions, and any surface that can dispatch a workflow can drive it.What's here
bin/branch_sweep.sh— the executable half of the skill. Classifies every remote branch viabranch_contribution.sh(never a hand-rolled ahead-count, perdocs/agent_failure_modes.mdD1/D2) and deletes only what is provably contained. Two pieces of care, both earned during the audit that motivated this:CONTRIBUTESbranch may still be a squash-merge git 2.34 cannot see through. It asks GitHub for a merged PR at that exact tip, then falls back to proving the squash locally by patch-id. Unproven means kept. The API path is what recovers the 6 of PyAutoMind's 19 squash-merges the local proof declines..github/workflows/branch_sweep.yml— dispatch (audit|delete, optionallimit) plus a weekly audit-only cron. A scheduled run forcesauditregardless of input, so nothing is ever removed unattended. The step summary carries the report, which is the readable surface on a phone.tests/test_branch_sweep.py— known-answer repos for the four gates that keep a branch out of the delete set:main,archive/condemned/*Gut transit refs, open PR heads, and unproven content. The deletions are onegit push; the refusals are the part worth pinning. One test performs a real delete against a live origin and asserts every gate held.skills/repo_cleanup/— routes cloud sessions to the dispatch instead of reporting the sweep blocked, and says once that a repo still accumulating merged heads has "Automatically delete head branches" off — the setting that stops this backlog forming, which no sweep substitutes for.Checks
pytest tests/→ 499 passed.bin/check_skill_line_counts.sh→ OK (all 39 skills within the 200-line budget).Merge order
This PR merges before PyAutoLabs/PyAutoMind#321. That workflow checks PyAutoBrain out at its default branch and runs
.brain/bin/branch_sweep.sh; until this lands, the script is not onmainand a dispatch there would fail.🤖 Generated with Claude Code
https://claude.ai/code/session_01KwqicJpMqmcT5RVbyNwdKq