fix(gate): say WHY a green, approved PR did not merge — surface GitHub's own refusal - #9864
Conversation
…b's own refusal #9856: gate success, CI green, mergeable_state "clean", autonomy auto, approvals satisfied, the bot posted "LoopOver approves" and the panel said "Suggested Action - Approve/Merge · safe to merge" -- and it never merged. Recurring for weeks, across many PRs, with no stated reason. The cause was never a LoopOver bug. GitHub REFUSED the merge: merge forbidden (403): Merging stacked PRs via this API is not supported. Use the web interface instead. Stacked PRs cannot be merged through the REST API at all. The executor recorded the refusal against that head and correctly stopped retrying. But that reason is invisible to every surface: mergeable_state reads `clean`, no gate models it, the audit fell through to the residual "merge withheld because no merge action was planned", and the public comment kept promising a merge that cannot happen. Four distinct refusals across 7 PRs all rendered identically (403 stacked; three 405 repo-policy variants). Surface it in both places: - agentHoldAuditDetail reports GitHub's message when the block is for THIS head, checked last among the specifics (everything above it is a state LoopOver can still change on its own; this one only a human can). - the unified comment downgrades an otherwise-ready status to HELD and names the refusal in the verdict box, with the remedy ("merge it yourself"). Same downgrade-only discipline as the guardrail hold: a real gate/CI block still wins. A block recorded against a DIFFERENT head is stale and deliberately ignored -- that commit was never attempted, so claiming GitHub refused it would be a lie and would hide whatever is actually holding the new push. Clock discipline: the decision path uses the pass's recorded instant (decisionClock.nowMs), never a fresh Date.now() -- the #9492 invariant, whose guard caught a first attempt that violated it. The publish path matches on head SHA alone and explains why: it is a reporting surface with no decision clock, and merge_blocked_until bounds when LoopOver may RETRY, not whether GitHub refused this commit.
|
Caution 🛑 LoopOver review result - fixes requiredReview updated: 2026-07-29 15:23:44 UTC
Review summary Blockers
Nits — 6 non-blocking
Why this is blocked
📋 Copy for AI agents — paste into your coding agentCI checks failing
Decision drivers
Context & advisory signals — never blocks the verdict
Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent did not find any vulnerabilities or security issues in this PR. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9864 +/- ##
==========================================
- Coverage 91.65% 90.77% -0.88%
==========================================
Files 916 916
Lines 112772 112779 +7
Branches 27093 27099 +6
==========================================
- Hits 103357 102371 -986
- Misses 8126 9315 +1189
+ Partials 1289 1093 -196
Flags with carried forward coverage won't be shown. Click here to find out more.
|
The recurring mystery, solved
#9856: gate success, CI green,mergeable_state: clean, autonomyauto, approvals satisfied. The bot posted "LoopOver approves — the gate is satisfied and CI is green" and the panel said "Suggested Action - Approve/Merge · safe to merge". It never merged. This has recurred for weeks across many PRs with no stated reason.It was never a LoopOver bug. GitHub refused the merge:
Stacked PRs cannot be merged through the REST API at all. The executor recorded the refusal against that head and correctly stopped retrying — that part works exactly as designed.
The actual defect: the reason reaches nobody
merge_blocked_reasonis stored in the DB and consulted by the planner, but no surface reads it:mergeable_statereadsclean— the refusal is invisible to it"merge withheld because no merge action was planned"— true, and uselessAcross the fleet, 7 PRs / 4 distinct refusals all rendered identically: the 403 above (4 PRs), plus three 405 repo-policy variants ("Merge already in progress", and two others).
The fix — both surfaces
Audit —
agentHoldAuditDetailreports GitHub's message when the block is for this head. Checked last among the specifics: everything above it (pending CI, dirty/unstable, approvals, autonomy) is a state LoopOver can still change on its own; this one only a human can, so it must not mask them.Comment — an otherwise-ready status downgrades to held, and the verdict box names the refusal plus the remedy ("merge this pull request yourself"). Same downgrade-only discipline as the guardrail hold: a real gate/CI block still wins, and an advisory review stays advisory.
A block recorded against a different head is stale and deliberately ignored — that commit was never attempted, so claiming GitHub refused it would be a lie and would hide whatever is actually holding the new push.
Clock discipline (a guard caught me)
My first attempt used
Date.now()for the block's expiry.decision-clock-threading.test.ts(#9492) rejected it: a second clock read can disagree with the planner's within one pass, andreplayDecisionwould then certify a decision made on an unrecorded read asmatch— a false certification. The decision path now usesdecisionClock.nowMs.The publish path matches on head SHA alone, and the comment says why: it's a reporting surface with no decision clock, reached from callers that have none either, and
merge_blocked_untilbounds when LoopOver may retry, not whether GitHub refused this commit.Tests
9 new cases: the regression on both surfaces, the stale-head invariant, "more specific reasons still win", downgrade-only, no-block-unchanged, and a render assertion that the comment names the refusal and no longer contains "safe to merge". Full suite 25,350 passed;
dead-source-files,dispatch-gate-reasons,docs:driftgreen.Note for the maintainer
#9856is unmergeable by the ORB permanently — it needs the web UI ("Squash and merge" on the stack). That's a GitHub restriction, not something to fix here. This PR just makes it say so.