A PR that never actually touches a guarded path can get permanently stuck with the manual-review
label and "merge withheld" — traced live to several JSONbored/awesome-claude PRs that sat held for
8-9 hours despite touching nothing in hardGuardrailGlobs.
Root cause
isGuardrailHit (packages/loopover-engine/src/signals/change-guardrail.ts) is deliberately
fail-safe:
export function isGuardrailHit(changedPaths: string[], hardGuardrailGlobs: string[]): boolean {
if (hardGuardrailGlobs.length === 0) return false;
return changedPaths.length === 0 || changedPathsHittingGuardrail(changedPaths, hardGuardrailGlobs).length > 0;
}
When the changed-files list is empty, it treats that as a hit rather than risk missing a guarded
path (#1062). That's the right call when the diff is genuinely unknown. The problem is upstream:
fetchAndStorePullRequestFilesForReview (src/github/backfill.ts) can legitimately return [] for
a PR that has real files, because GitHub can return a clean, successful, empty files: [] response
for a PR whose diff it hasn't finished computing yet (common right after a PR is opened or pushed) —
this is not a rate limit (the shared client in src/github/client.ts already retries those
internally), so nothing currently retries it.
Once that empty result trips the guardrail fail-safe, planAgentMaintenanceActions
(src/settings/agent-actions.ts) applies the manual-review label — and by deliberate design
(#stale-disposition-label-cleanup), that label is never auto-removed, since the planner can't
tell a bot-applied guardrail hold apart from a maintainer's own manual hold using the same label.
So a single transient empty-files read permanently locks an otherwise-clean PR into "held for manual
review, merge denied" until a human notices.
Confirmed live: JSONbored/awesome-claude#5176, #5298, #5162, #5092, #5164 — all CONTRIBUTOR-
association repeat contributors (not first-timers, not bots), all carrying manual-review, all
denied merge repeatedly for 8-9+ hours with "manual-review label \"manual-review\" is present on the live PR — merge not executed". Their actual changed files (checked via the GitHub API) never
touch any of the repo's configured hardGuardrailGlobs (.github/workflows/**, .loopover.yml,
codecov.yml, etc.) at all.
Fix: retry the inline file fetch once, after a short delay, when the first attempt comes back
empty, before accepting it and computing the guardrail check against it. This doesn't touch the
fail-safe (a genuinely persistent failure still degrades to [] → still a hit) or the "never
auto-remove manualReview" invariant (left alone, intentionally) — it just makes the empty result far
less likely to be a false positive in the first place.
A PR that never actually touches a guarded path can get permanently stuck with the
manual-reviewlabel and "merge withheld" — traced live to several JSONbored/awesome-claude PRs that sat held for
8-9 hours despite touching nothing in
hardGuardrailGlobs.Root cause
isGuardrailHit(packages/loopover-engine/src/signals/change-guardrail.ts) is deliberatelyfail-safe:
When the changed-files list is empty, it treats that as a hit rather than risk missing a guarded
path (
#1062). That's the right call when the diff is genuinely unknown. The problem is upstream:fetchAndStorePullRequestFilesForReview(src/github/backfill.ts) can legitimately return[]fora PR that has real files, because GitHub can return a clean, successful, empty
files: []responsefor a PR whose diff it hasn't finished computing yet (common right after a PR is opened or pushed) —
this is not a rate limit (the shared client in
src/github/client.tsalready retries thoseinternally), so nothing currently retries it.
Once that empty result trips the guardrail fail-safe,
planAgentMaintenanceActions(
src/settings/agent-actions.ts) applies themanual-reviewlabel — and by deliberate design(
#stale-disposition-label-cleanup), that label is never auto-removed, since the planner can'ttell a bot-applied guardrail hold apart from a maintainer's own manual hold using the same label.
So a single transient empty-files read permanently locks an otherwise-clean PR into "held for manual
review, merge denied" until a human notices.
Confirmed live: JSONbored/awesome-claude#5176, #5298, #5162, #5092, #5164 — all
CONTRIBUTOR-association repeat contributors (not first-timers, not bots), all carrying
manual-review, alldenied merge repeatedly for 8-9+ hours with
"manual-review label \"manual-review\" is present on the live PR — merge not executed". Their actual changed files (checked via the GitHub API) nevertouch any of the repo's configured
hardGuardrailGlobs(.github/workflows/**,.loopover.yml,codecov.yml, etc.) at all.Fix: retry the inline file fetch once, after a short delay, when the first attempt comes back
empty, before accepting it and computing the guardrail check against it. This doesn't touch the
fail-safe (a genuinely persistent failure still degrades to
[]→ still a hit) or the "neverauto-remove manualReview" invariant (left alone, intentionally) — it just makes the empty result far
less likely to be a false positive in the first place.