You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
performAction's "merge" case (src/services/agent-action-executor.ts) computes const mergeSha = action.expectedHeadSha ?? ctx.headSha; — structurally identical to approve's (now-fixed, see #2377) approveSha fallback. When a staged merge row has no expectedHeadSha (staged by older code, or a planning pass that ran while the stored PR head SHA was transiently null — pullRequests.headSha is a nullable column), the fallback substitutes ctx.headSha, the CURRENT live head read at accept time.
The reasoning that previously excused merge from this class of bug ("GitHub's sha param 409s on mismatch") does not actually hold for the unpinned-row case: the fallback substitutes the CURRENT live head as mergeSha, so by construction it matches whatever GitHub reports as the current head at merge time — no mismatch, no 409. The 409 backstop only protects against a narrow race between reading ctx.headSha and the merge API call, not against "this row was staged with no pin at all, then the contributor force-pushed during the queue wait." An unpinned staged merge can therefore silently merge unreviewed code, the same failure mode #2377 fixed for approve.
Found via an adversarial multi-agent verification pass while implementing #2377 (the approve fix) — the initial "not exploitable / merge is safe due to a GitHub backstop" framing was checked and found incorrect for this specific scenario.
Requirements
The approval-queue accept flow (src/services/agent-approval-queue.ts's decidePendingAgentAction) must not replay a staged merge action that has no expectedHeadSha pin through the live-head fallback.
Regression test: a staged merge with no expectedHeadSha is denied on accept, mergePullRequest is never called, and the row ends up rejected (not accepted).
Confirm existing tests modeling a genuinely-live (non-replayed) unpinned merge via executeAgentMaintenanceActions directly are unaffected (this fix belongs in the accept-flow gate, not the shared executor — see fix(agent-actions): pin a staged approve to its reviewed head SHA #2377's own scoping rationale).
Acceptance criteria
An unpinned staged merge accepted after a force-push is refused, not silently merged against the new head.
Expected outcome
merge gets the same fail-closed treatment approve received in #2377, closing the parallel gap the adversarial review identified.
Parent: #1936
Problem
performAction's"merge"case (src/services/agent-action-executor.ts) computesconst mergeSha = action.expectedHeadSha ?? ctx.headSha;— structurally identical toapprove's (now-fixed, see #2377)approveShafallback. When a staged merge row has noexpectedHeadSha(staged by older code, or a planning pass that ran while the stored PR head SHA was transiently null —pullRequests.headShais a nullable column), the fallback substitutesctx.headSha, the CURRENT live head read at accept time.The reasoning that previously excused
mergefrom this class of bug ("GitHub'sshaparam 409s on mismatch") does not actually hold for the unpinned-row case: the fallback substitutes the CURRENT live head asmergeSha, so by construction it matches whatever GitHub reports as the current head at merge time — no mismatch, no 409. The 409 backstop only protects against a narrow race between readingctx.headShaand the merge API call, not against "this row was staged with no pin at all, then the contributor force-pushed during the queue wait." An unpinned staged merge can therefore silently merge unreviewed code, the same failure mode #2377 fixed for approve.Found via an adversarial multi-agent verification pass while implementing #2377 (the approve fix) — the initial "not exploitable / merge is safe due to a GitHub backstop" framing was checked and found incorrect for this specific scenario.
Requirements
src/services/agent-approval-queue.ts'sdecidePendingAgentAction) must not replay a stagedmergeaction that has noexpectedHeadShapin through the live-head fallback.Deliverables
approve(deny withstatus: "rejected", a distinguishableexecutionOutcome, and an audit event) tomergewhenpending.params.expectedHeadShais absent.expectedHeadShais denied on accept,mergePullRequestis never called, and the row ends uprejected(notaccepted).executeAgentMaintenanceActionsdirectly are unaffected (this fix belongs in the accept-flow gate, not the shared executor — see fix(agent-actions): pin a staged approve to its reviewed head SHA #2377's own scoping rationale).Acceptance criteria
Expected outcome
mergegets the same fail-closed treatmentapprovereceived in #2377, closing the parallel gap the adversarial review identified.