What
branch-sweep.yml's header reassures the operator that deletion is safe because merged history is preserved on the default branch:
Merged-branch tips are ancestors of the default branch, so a merged branch that auto-delete somehow missed is also swept — its history is on main.
Nothing in the workflow checks that. The candidate rules are: not default, not protected, no open PR, and either tip older than stale_days or a prefix match. Reachability from the default branch is never consulted.
The assumption is false in this repo today — see #166, where three claude/* branches share no merge base at all with main and hold the only reachable copy of everything before 2026-08-01.
Why the prose is the risky part
The header is what an operator reads before flipping dry_run to false. It states a safety property the code does not enforce, which is precisely the failure mode docs/agentic-code-hygiene.md rule 3 names — a gate's own claim about itself is not evidence.
Suggested fix
Add a reachability check to the candidate loop and surface it in the printed table, so the dry run distinguishes "already on main" from "would discard commits":
if git merge-base --is-ancestor "$tip_sha" "origin/$default"; then
reason="$reason, merged"
else
unique=$(git rev-list --count "origin/$default..$tip_sha")
reason="$reason, ${unique} commit(s) NOT on $default"
fi
Then either refuse non-ancestor branches unless a second input explicitly opts in, or at minimum make the dry-run output say plainly which branches carry unreachable commits. The current output — WOULD DELETE <name> (prefix claude/) — reads identically whether the branch is redundant or is the last ref holding two months of history.
The workflow runs gh api rather than a full checkout, so this needs either a fetch of the candidate tips or the compare API; the cheap version is GET /repos/{repo}/compare/{default}...{branch} and reading behind_by / ahead_by.
Also worth reconsidering
prefix waives the age rule entirely. Combined with the missing reachability check, prefix: claude/ on this repo is a single dispatch away from deleting history. Age is a weak proxy for "over", but it is currently the only thing standing between a routine sweep and #166.
What
branch-sweep.yml's header reassures the operator that deletion is safe because merged history is preserved on the default branch:Nothing in the workflow checks that. The candidate rules are: not default, not protected, no open PR, and either tip older than
stale_daysor aprefixmatch. Reachability from the default branch is never consulted.The assumption is false in this repo today — see #166, where three
claude/*branches share no merge base at all withmainand hold the only reachable copy of everything before 2026-08-01.Why the prose is the risky part
The header is what an operator reads before flipping
dry_runto false. It states a safety property the code does not enforce, which is precisely the failure modedocs/agentic-code-hygiene.mdrule 3 names — a gate's own claim about itself is not evidence.Suggested fix
Add a reachability check to the candidate loop and surface it in the printed table, so the dry run distinguishes "already on main" from "would discard commits":
Then either refuse non-ancestor branches unless a second input explicitly opts in, or at minimum make the dry-run output say plainly which branches carry unreachable commits. The current output —
WOULD DELETE <name> (prefix claude/)— reads identically whether the branch is redundant or is the last ref holding two months of history.The workflow runs
gh apirather than a full checkout, so this needs either a fetch of the candidate tips or the compare API; the cheap version isGET /repos/{repo}/compare/{default}...{branch}and readingbehind_by/ahead_by.Also worth reconsidering
prefixwaives the age rule entirely. Combined with the missing reachability check,prefix: claude/on this repo is a single dispatch away from deleting history. Age is a weak proxy for "over", but it is currently the only thing standing between a routine sweep and #166.