Parent: #1936
Problem
The live webhook merge path applies two accuracy circuit-breakers immediately before executing an action: applyPrecisionBreakers(planned, await isHoldOnly(env, repoFullName), await isCloseHoldOnly(env, repoFullName)) (src/queue/processors.ts:1725-1729). If the holdonly flag is engaged — by the auto-tuner after merge precision drops, or manually by an operator — downgradeMergeToHold strips the merge action and substitutes a needs-human-review label. isHoldOnly (src/review/outcomes-wire.ts:62-79) reads the system_flags table fresh on every call specifically so an operator can halt auto-merge instantly.
The approval-queue accept path (src/services/agent-approval-queue.ts:58-71, decidePendingAgentAction) never calls applyPrecisionBreakers/isHoldOnly/isCloseHoldOnly — it rebuilds the action from the persisted pending row via pendingActionToPlanned and passes it straight to executeAgentMaintenanceActions, whose own gate stack (kill-switch → autonomy → dry-run → approval-staging → freshness → write-permission) has no precision-breaker step either.
Failure scenario: a repo's merge precision drops (or an operator manually flips holdonly:global/holdonly:<repo>) at time T. A merge was staged for auto_with_approval before T, with a head SHA that never changes. A maintainer, unaware the breaker just engaged fleet-wide, accepts the pre-existing pending action at T+1. Because accept never checks the breaker, the merge executes anyway — exactly the scenario the breaker exists to stop slips through the one path that bypasses it. Since a merge can only reach the approval queue by construction while the breaker was off at staging time, every staged pending merge is, by definition, a candidate for this gap once the breaker engages later.
Requirements
- Accepting a staged merge/close action must be subject to the same precision circuit-breaker as the live webhook path, evaluated at accept time (not staging time).
- An operator engaging
holdonly must halt all auto-merges, including ones already staged and awaiting a maintainer's accept — not just future ones.
Deliverables
- In
decidePendingAgentAction, before calling executeAgentMaintenanceActions, re-apply applyPrecisionBreakers using the current isHoldOnly/isCloseHoldOnly state, and pass the (possibly downgraded-to-hold) plan to the executor instead of the raw persisted action.
- If the breaker strips the action, record a distinct decision outcome (e.g.
breaker_engaged) rather than silently executing or silently no-op'ing, so the maintainer's UI/audit trail explains why their accept didn't merge — mirroring how a head-SHA mismatch is already surfaced as "superseded."
- Add a regression test in
test/unit/agent-approval-queue.test.ts: stage a merge while holdonly is off, flip holdonly:global on, accept, and assert mergePullRequest is not called and the outcome reflects the breaker engagement.
Acceptance criteria
- A staged merge accepted after the precision breaker engages does not call the GitHub merge API; it downgrades to hold/label with a distinguishable audit outcome.
- A staged merge accepted while the breaker is off still executes normally.
Expected outcome
An operator-engaged (or auto-tuner-engaged) merge-precision halt takes effect fleet-wide immediately, with no window where a pre-existing staged action can slip through it.
Parent: #1936
Problem
The live webhook merge path applies two accuracy circuit-breakers immediately before executing an action:
applyPrecisionBreakers(planned, await isHoldOnly(env, repoFullName), await isCloseHoldOnly(env, repoFullName))(src/queue/processors.ts:1725-1729). If theholdonlyflag is engaged — by the auto-tuner after merge precision drops, or manually by an operator —downgradeMergeToHoldstrips the merge action and substitutes a needs-human-review label.isHoldOnly(src/review/outcomes-wire.ts:62-79) reads thesystem_flagstable fresh on every call specifically so an operator can halt auto-merge instantly.The approval-queue accept path (
src/services/agent-approval-queue.ts:58-71,decidePendingAgentAction) never callsapplyPrecisionBreakers/isHoldOnly/isCloseHoldOnly— it rebuilds the action from the persisted pending row viapendingActionToPlannedand passes it straight toexecuteAgentMaintenanceActions, whose own gate stack (kill-switch → autonomy → dry-run → approval-staging → freshness → write-permission) has no precision-breaker step either.Failure scenario: a repo's merge precision drops (or an operator manually flips
holdonly:global/holdonly:<repo>) at time T. A merge was staged forauto_with_approvalbefore T, with a head SHA that never changes. A maintainer, unaware the breaker just engaged fleet-wide, accepts the pre-existing pending action at T+1. Because accept never checks the breaker, the merge executes anyway — exactly the scenario the breaker exists to stop slips through the one path that bypasses it. Since a merge can only reach the approval queue by construction while the breaker was off at staging time, every staged pending merge is, by definition, a candidate for this gap once the breaker engages later.Requirements
holdonlymust halt all auto-merges, including ones already staged and awaiting a maintainer's accept — not just future ones.Deliverables
decidePendingAgentAction, before callingexecuteAgentMaintenanceActions, re-applyapplyPrecisionBreakersusing the currentisHoldOnly/isCloseHoldOnlystate, and pass the (possibly downgraded-to-hold) plan to the executor instead of the raw persisted action.breaker_engaged) rather than silently executing or silently no-op'ing, so the maintainer's UI/audit trail explains why their accept didn't merge — mirroring how a head-SHA mismatch is already surfaced as "superseded."test/unit/agent-approval-queue.test.ts: stage a merge whileholdonlyis off, flipholdonly:globalon, accept, and assertmergePullRequestis not called and the outcome reflects the breaker engagement.Acceptance criteria
Expected outcome
An operator-engaged (or auto-tuner-engaged) merge-precision halt takes effect fleet-wide immediately, with no window where a pre-existing staged action can slip through it.