Skip to content

fix(disposition): hold loudly on an unstable mergeable state instead of approving into a silent stall - #8783

Merged
JSONbored merged 2 commits into
mainfrom
fix/disposition-mergeable-state-alignment
Jul 26, 2026
Merged

fix(disposition): hold loudly on an unstable mergeable state instead of approving into a silent stall#8783
JSONbored merged 2 commits into
mainfrom
fix/disposition-mergeable-state-alignment

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

The #8711 incident fix (adversarial-audit epic #8757). A non-required third-party check concluded action_required, GitHub reported mergeable_state: unstable, and ORB's four decision surfaces disagreed: the unified comment said "Manual Review", the planner posted an APPROVE review claiming "gate satisfied and CI is green", the review-state label resolved to ready-to-merge, the merge silently self-suppressed (mergeableClean requires "clean"), and the only record of why lived in an internal audit_events row. A human had to notice and merge manually.

Changes

  • src/settings/agent-actions.tsheldForManualReview gains a mergeableState === "unstable" term: no approve, no ready-to-merge label; the review-state label resolves to manual-review with a reason + comment naming the culprit non-required check(s). A new 1f fallback (mirroring 1d) keeps the hold visible on repos where review_state_label autonomy isn't acting but merge is. New mergeUnstableHoldReason/mergeUnstableHoldComment builders follow the advisoryHoldReason/advisoryHoldComment (feat(gate): configurable advisory check-runs so a non-resolving external status never blocks or stalls the gate forever #4372) pattern, generic wording when the CI aggregate couldn't itemize the source.
  • src/queue/processors.ts — threads the CI aggregate's nonRequiredFailingDetails into the planner input so the hold names the actual check.
  • src/services/agent-action-executor.ts — approve now live-rechecks mergeable state right before posting (the same Review latency can create a merge-conflict close window an author didn't cause #3863-shaped guard merge-adjacent actions already had): denied on live dirty/unstable, fails open on ambiguous reads so a transient fetch hiccup never strands approval-required repos.

Deliberately unchanged: dirty stays the close path, behind stays the rebase rail's, blocked stays approvable (the approval itself can be the unblocking act — requiring clean for approve would deadlock approval-required repos). Tightening-only: no path that previously withheld an approve/merge now grants one. Close semantics untouched (explicit regression test: a red-CI contributor PR still closes when also unstable).

Test plan

  • npm run typecheck — clean
  • 8 new regression tests reproducing fix(review): keep a fetched file distinguishable from an omitted one under thin budget #8711's exact state (gate success, required CI green, non-required check red, unstable): no approve, no merge, manual-review label + comment naming the check; generic-wording arm; idempotency; blocked/behind still approvable; close never softened
  • 3 new executor tests: approve denied on live unstable/dirty, fails open on ambiguous
  • 1,121 tests green across all 8 planner/executor-adjacent suites
  • Changed lines 100% covered (pre-existing uncovered lines don't intersect this diff)

Closes #8758

…of approving into a silent stall (#8758)

PR #8711 exposed the seam: a non-required third-party check concluded
action_required, GitHub reported mergeable_state "unstable", and ORB's four
surfaces disagreed — the unified comment said Manual Review, approve fired
("gate satisfied and CI is green"), the ready-to-merge label landed, the merge
silently self-suppressed (mergeableClean requires "clean"), and the only
record of why lived in an internal audit_events row. A human had to notice.

- heldForManualReview gains a mergeableState === "unstable" term: no approve,
  no ready-to-merge label, manual-review label + comment naming the culprit
  non-required check(s) (threaded via the CI aggregate's
  nonRequiredFailingDetails; generic wording when unitemized).
- New 1f fallback mirrors 1d so the hold stays visible on repos where
  review_state_label autonomy isn't acting but merge is.
- The executor live-rechecks mergeable_state before posting an approve
  (denied on live dirty/unstable, fails open on ambiguous reads), the same
  #3863-shaped guard merge-adjacent actions already had.
- dirty/behind/blocked semantics deliberately unchanged: dirty stays the
  close path, behind stays the rebase rail's, blocked stays approvable (the
  approval itself can be the unblocking act).
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Logic backtest

Replayed 0 historical case(s) for linked_issue_scope_mismatch through the base (a93ae0a) and head (9ce0851) versions of its detection logic (corpus checksum 4f53cda18c2b).

Backtest comparison: linked_issue_scope_mismatch

Verdict: unchanged — no comparable axis moved.

Advisory only — this check never blocks merge (#8105).

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 26, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 9ce0851 Commit Preview URL

Branch Preview URL
Jul 26 2026, 05:36 AM

@JSONbored JSONbored self-assigned this Jul 26, 2026
@JSONbored
JSONbored merged commit 476b98d into main Jul 26, 2026
7 checks passed
@JSONbored
JSONbored deleted the fix/disposition-mergeable-state-alignment branch July 26, 2026 05:39
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.25%. Comparing base (a93ae0a) to head (9ce0851).
⚠️ Report is 6 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8783      +/-   ##
==========================================
- Coverage   93.80%   92.25%   -1.55%     
==========================================
  Files         797      797              
  Lines       79517    79529      +12     
  Branches    24097    24103       +6     
==========================================
- Hits        74593    73372    -1221     
- Misses       3556     5090    +1534     
+ Partials     1368     1067     -301     
Flag Coverage Δ
backend 92.92% <100.00%> (-2.17%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/queue/processors.ts 95.67% <ø> (ø)
src/services/agent-action-executor.ts 97.27% <100.00%> (+0.01%) ⬆️
src/settings/agent-actions.ts 98.13% <100.00%> (+0.07%) ⬆️

... and 3 files with indirect coverage changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(disposition): unify mergeable-state predicates across approve/hold/label with the merge gate, and surface merge-hold reasons on the PR

1 participant