Skip to content

fix(agent-actions): actuation freshness guard never re-verifies CI/mergeable state before merge or close #2128

Description

@JSONbored

Parent: #1936

Problem

executeAgentMaintenanceActions (src/services/agent-action-executor.ts) calls fetchPullRequestFreshness before every live action, but classifyPullRequestFreshness (src/github/pr-freshness.ts:32-55) only compares live state/head.sha against the plan's expectedHeadSha. The CI aggregate and mergeable_state that actually drove the merge-vs-close decision in maybeRunAgentMaintenance (src/queue/processors.ts:1621-1629, 1668-1717) are fetched once, seconds to tens-of-seconds before actuation, and are never re-read at the moment of actuation.

GitHub's own PUT /pulls/{n}/merge does enforce branch-protection required checks server-side (a backstop, when configured) — but PATCH /pulls/{n} for close has no server-side gate at all, and fires purely off the stale-but-headSha-matching plan. Notably, the two-pass "flag then verify" mechanism that exists for linked-issue-hard-rule closes (agent-actions.ts:344-364) does not cover heuristic CI-failed closes — those are one-shot.

Failure scenario: a required check finishes GREEN a few seconds after maybeRunAgentMaintenance already fetched the CI aggregate as failed/pending (a flaky read, or the check posts mid-flight). The planner computes a heuristic close (CI-failed). The freshness guard only checks headSha/state — unchanged — so it passes, and closePullRequest executes even though CI is actually green by the time of the PATCH. There is no second look; the PR is closed and the contributor gets no further re-evaluation.

Requirements

  • A close action driven by a CI-failure heuristic must not fire if live CI has since turned green.
  • The fix should add re-verification with minimal added latency (the freshness check already does one network round-trip).

Deliverables

  • Extend the freshness re-check so a heuristic, CI-driven close (closeKind: "heuristic") re-derives live CI state (reusing the existing refreshLiveCiAggregate/live-CI-rollup helpers) immediately before the mutation, and denies the action (audit outcome: "CI state changed since planning") if the live state no longer shows failed.
  • Consider applying the same live re-check to merge as defense-in-depth alongside GitHub's branch-protection backstop, for repos that don't configure required checks.
  • Add a regression test in test/unit/agent-action-executor.test.ts covering: CI is failed at planning time, flips to passed before actuation, headSha unchanged — assert the close is denied.

Acceptance criteria

  • A heuristic CI-driven close is denied (not executed) if live CI has turned green in the window between planning and actuation.
  • No added false-denials for the common case where CI state is genuinely unchanged.

Expected outcome

A PR that becomes green in the narrow window between CI-aggregate fetch and actuation is never closed on stale information — actuation always reflects a check performed at the moment of the mutation, not seconds-to-tens-of-seconds earlier.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

Status
Done

Relationships

None yet

Development

No branches or pull requests

Issue actions