Summary
repo-fleet-hygiene:audit gates its merged-PR lookup on the branch being present
in the local remote-tracking inventory. In a fleet that deletes head branches on
merge, that gate suppresses evidence for precisely the branches most likely to be
cleanup candidates. Measured on an 11-repository fleet, the audit reported 103
HIGH candidates where a direct PR join found 195 — the gate hid 92 real,
PR-evidenced candidates.
Observed behavior
Audit run over --root D:/repos, 11 canonical checkouts, 506 local branches,
gh authenticated:
HIGH = 103, MEDIUM = 60, LOW = 11, UNKNOWN = 5
- The 5
UNKNOWN findings are all merge-evidence-privacy-gated, covering
300 branches whose merged state was never queried. One repository
accounted for 291 of them.
The finding text explains the mechanism:
Finding: merge-evidence-privacy-gated
Evidence: exact merged-PR lookup skipped for 291 branch(es) absent from the
local remote-tracking inventory: ...
Disposition: Merged state unverified; a merged branch whose remote ref was
auto-deleted and pruned reports no merged finding
Handoff: Push or re-fetch the branch to restore remote evidence, or verify
manually on GitHub, then rerun
The suggested handoff does not resolve it. git fetch --all --prune cannot
restore a remote ref that was deleted upstream on merge — after fetching all 11
repositories the same branches remain absent from the remote-tracking inventory,
so a rerun reports the same gap. The only escape offered is manual GitHub
verification of 300 branches.
Why the gate is not load-bearing
The audit already has the authoritative evidence source: gh pr list --repo <repo> --state merged --head <branch>, matched on headRefOid against the local
tip. That query does not require a remote-tracking ref to exist. Presence in the
remote-tracking inventory is not a precondition for the lookup — it is only a
proxy for "this branch was once pushed", which a merged PR proves far more
directly.
Cost is not the reason either
I replaced the per-branch query with one bulk call per repository:
gh pr list --repo "melodic-software/$name" --state all --limit 4000 \
--json number,headRefName,headRefOid,state,url
11 API calls covered all 506 branches in both directions (merged evidence and
open-PR protection). Joining locally on headRefName then comparing
headRefOid to the local tip reproduced every HIGH finding the audit made and
added 92 more. This is strictly cheaper than the current per-branch query path,
not more expensive.
Resulting classification over the same 506 branches:
| Disposition |
Count |
merged PR, headRefOid == local tip |
195 |
| local tip is an ancestor of the default branch |
27 |
| merged PR present, tip drift |
132 |
| no PR, all commits on some remote ref |
53 |
| no PR, not an ancestor, commits on no remote ref |
99 |
Secondary finding: the ancestry lane assumes merge commits
Related, and worth stating in the report header: local-ancestry-only and any
ancestry-derived reasoning are near-inert in a squash-merging fleet. Of 506
branches only 27 were ancestors of origin/<default>, because a squash merge
rewrites the commits. Correspondingly, git rev-list --count <tip> --not --remotes reported non-zero for 410 branches — work that is fully merged but
whose exact commit objects exist nowhere on any remote.
Both readings are literally true and both are misleading if used as a deletion
predicate. A consumer following the report's tiering without knowing the fleet's
merge strategy would either preserve almost everything or believe it has 410
branches of unpushed work. The report should state which predicate it used and
that ancestry evidence degrades under squash merge.
Suggested fix
- Drop the remote-tracking-presence gate before the merged-PR query. Absence of
a remote ref is expected for merged-and-deleted branches, not a reason to skip
the lookup.
- Switch to one bulk
gh pr list --state all call per repository and join
locally. Cheaper than the current path and it yields open-PR protection for
free.
- Keep a genuine
UNKNOWN tier for real API/auth/network failures only.
- Name the merge strategy assumption in the report, and note that ancestry
evidence is weak under squash merge.
Environment
repo-fleet-hygiene 0.7.0
- Claude Code on Windows 11 Pro 10.0.26200, Git 2.54.0.windows.1
gh authenticated, 11 repositories, 506 local branches
- Observed 2026-07-29
Summary
repo-fleet-hygiene:auditgates its merged-PR lookup on the branch being presentin the local remote-tracking inventory. In a fleet that deletes head branches on
merge, that gate suppresses evidence for precisely the branches most likely to be
cleanup candidates. Measured on an 11-repository fleet, the audit reported 103
HIGHcandidates where a direct PR join found 195 — the gate hid 92 real,PR-evidenced candidates.
Observed behavior
Audit run over
--root D:/repos, 11 canonical checkouts, 506 local branches,ghauthenticated:HIGH= 103,MEDIUM= 60,LOW= 11,UNKNOWN= 5UNKNOWNfindings are allmerge-evidence-privacy-gated, covering300 branches whose merged state was never queried. One repository
accounted for 291 of them.
The finding text explains the mechanism:
The suggested handoff does not resolve it.
git fetch --all --prunecannotrestore a remote ref that was deleted upstream on merge — after fetching all 11
repositories the same branches remain absent from the remote-tracking inventory,
so a rerun reports the same gap. The only escape offered is manual GitHub
verification of 300 branches.
Why the gate is not load-bearing
The audit already has the authoritative evidence source:
gh pr list --repo <repo> --state merged --head <branch>, matched onheadRefOidagainst the localtip. That query does not require a remote-tracking ref to exist. Presence in the
remote-tracking inventory is not a precondition for the lookup — it is only a
proxy for "this branch was once pushed", which a merged PR proves far more
directly.
Cost is not the reason either
I replaced the per-branch query with one bulk call per repository:
gh pr list --repo "melodic-software/$name" --state all --limit 4000 \ --json number,headRefName,headRefOid,state,url11 API calls covered all 506 branches in both directions (merged evidence and
open-PR protection). Joining locally on
headRefNamethen comparingheadRefOidto the local tip reproduced everyHIGHfinding the audit made andadded 92 more. This is strictly cheaper than the current per-branch query path,
not more expensive.
Resulting classification over the same 506 branches:
headRefOid== local tipSecondary finding: the ancestry lane assumes merge commits
Related, and worth stating in the report header:
local-ancestry-onlyand anyancestry-derived reasoning are near-inert in a squash-merging fleet. Of 506
branches only 27 were ancestors of
origin/<default>, because a squash mergerewrites the commits. Correspondingly,
git rev-list --count <tip> --not --remotesreported non-zero for 410 branches — work that is fully merged butwhose exact commit objects exist nowhere on any remote.
Both readings are literally true and both are misleading if used as a deletion
predicate. A consumer following the report's tiering without knowing the fleet's
merge strategy would either preserve almost everything or believe it has 410
branches of unpushed work. The report should state which predicate it used and
that ancestry evidence degrades under squash merge.
Suggested fix
a remote ref is expected for merged-and-deleted branches, not a reason to skip
the lookup.
gh pr list --state allcall per repository and joinlocally. Cheaper than the current path and it yields open-PR protection for
free.
UNKNOWNtier for real API/auth/network failures only.evidence is weak under squash merge.
Environment
repo-fleet-hygiene0.7.0ghauthenticated, 11 repositories, 506 local branches