fix(agent-actions): re-verify a staged merge live state and precision breaker at accept time - #2354
Conversation
… at accept time decidePendingAgentAction's only freshness check before replaying a staged approval-queue action was head-SHA equality. auto_with_approval rows have no expiry, so between staging and a maintainer's accept, CI could flip red, the base could go dirty, a reviewer could request changes, or the merge-precision circuit-breaker could engage — none of which move the head SHA, so none of them were caught. - Re-fetch live CI state, mergeable_state, and reviewDecision for a staged merge at accept time; supersede (deny, audit, leave the row untouched) instead of executing on stale justification. Best-effort: a failed live read fails open on that specific check, since the mutation call independently needs a valid token/state and fails cleanly on its own. - Re-apply the same merge/close precision circuit-breakers the live webhook path already applies, so a breaker engaged after staging still holds the row (downgrades to a needs-human-review label) instead of executing unmodified. - Re-sync the merge method to the repo's current config instead of the staging-time snapshot. While wiring the close breaker, found `closeKind` never survived staging at all — `actionParams()` dropped it, so `downgradeCloseToHold`'s heuristic-close match could never fire for any staged close regardless of this fix. Threaded it through `AgentPendingActionParams` and `actionParams()` so it round-trips. Advances #1936. Closes #2126, #2127, #2131. Advances #2132 (the CI/mergeable/ review portion lands here; the linked-issue-hard-rule re-check does not — see that issue for the remaining scope).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2354 +/- ##
=======================================
Coverage 95.71% 95.72%
=======================================
Files 222 222
Lines 24661 24682 +21
Branches 8949 8961 +12
=======================================
+ Hits 23605 23627 +22
Misses 433 433
+ Partials 623 622 -1
🚀 New features to boost your workflow:
|
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-01 19:03:32 UTC
⏸️ Suggested Action - Manual Review
Review summary Blockers
Nits — 6 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 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 Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
The three live re-checks (CI aggregate, mergeable state, review decision) were awaited via a bare Promise.all, so a transient rejection from any one of them threw out of decidePendingAgentAction instead of failing open on that specific check -- exactly the design this code's own comment describes, but Promise.all does not provide that isolation even though each function already catches its own fetch errors internally today (a future edit removing one of those internal catches would silently reintroduce a crash with no test to catch it). Switch to Promise.allSettled and treat a rejected settle as "nothing concerning found" for that check, matching each function's own already-established fail-open return value.
… just failed An accept-time live CI recheck that fulfills with "pending" or "unverified" (rather than rejecting) previously fell through to the same non-blocking path as "passed", letting a staged merge execute on live CI that had moved off green without ever going red. Distinguish a genuinely non-passing FULFILLED read from a REJECTED one (fail-open, unchanged) instead of collapsing both into a single sentinel string.
…outes happy path routes-agent-approval.test.ts's accept happy path never mocked fetchLiveCiAggregate, so an unconfigured GITHUB_APP_PRIVATE_KEY left the token undefined and the live read fulfilled with ciState "unverified" - now a genuine stale signal instead of an accidentally-tolerated one.
What
decidePendingAgentAction's only freshness check before replaying a staged approval-queue action was head-SHA equality.auto_with_approvalrows have no expiry, so between staging and a maintainer's accept: CI could flip red, the base could go dirty, a reviewer could request changes, or the merge-precision circuit-breaker could engage — none of which move the head SHA, so none of them were caught.Fixes
mergeable_state, andreviewDecisionfor a staged merge at accept time. If any contradicts the staging-time justification, supersede — deny, audit, leave the row untouched — instead of executing on stale grounds. Best-effort: a failed live read fails open on that specific check (the mutation call independently mints its own token and needs valid state, so it fails cleanly on its own if something is actually wrong).autoMaintain.mergeMethodinstead of the staging-time snapshot. The head-SHA pin stays frozen (that's the reviewed commit) — only the method, a live config preference, is re-synced.Bonus fix found while wiring the close breaker
closeKindnever survived staging at all —actionParams()silently dropped it when persisting a planned action's params, sodowngradeCloseToHold'scloseKind === "heuristic"match could never fire for any staged close, regardless of this fix. ThreadedcloseKindthroughAgentPendingActionParamsandactionParams()so it round-trips from staging to accept.Tests
actionParamsround-tripscloseKind.Full unsharded
test:coveragegreen;typecheckgreen.Advances #1936. Closes #2126, #2127, #2131. Advances #2132.