Skip to content

fix(warden): Prevent stale PR feedback writes#424

Merged
dcramer merged 7 commits into
mainfrom
fix/warden-stale-pr-feedback
Jul 4, 2026
Merged

fix(warden): Prevent stale PR feedback writes#424
dcramer merged 7 commits into
mainfrom
fix/warden-stale-pr-feedback

Conversation

@dcramer

@dcramer dcramer commented Jul 3, 2026

Copy link
Copy Markdown
Member

Warden verifies that a workflow run is still analyzing the current PR head before mutating PR review feedback: posting reviews, resolving threads, replying, dismissing blocking reviews, and no-trigger cleanup. An older run completes its Checks without touching current PR review state.

The check lives in a single `ReviewFeedbackGate` that every write path shares. It retries transient GitHub errors, memoizes results for a short TTL so bursts of writes share one `pulls.get` call, and distinguishes two failure modes:

  • Stale head (a newer push owns feedback): permanent for the run; all writes skip silently.
  • Unverifiable head (API errors after retries): writes skip, the state is cached only briefly so later phases retry, and the run fails loudly when the skip would suppress a REQUEST_CHANGES review the poster would actually have posted. Without this, a repo gating merges on `requestChanges` without `failCheck` would lose its merge gate to a single transient API error.

The gate also runs before fix evaluation so a stale run skips the LLM spend, not just the writes it would produce, and the "skipping due to trigger failures" log now fires only for trigger failures.

Non-blocking findings that cannot be attached as inline review comments stay in Checks instead of becoming body-only reviews that Warden cannot resolve later, and they are recorded with a new `no_inline_location` skipped reason in the findings output — previously they were either marked `posted` without reaching the PR or dropped from the observation stream entirely. Blocking REQUEST_CHANGES reviews keep the body fallback so the existing review lifecycle can still dismiss them.

The GitHub docs, README, and public llms.txt now describe Warden as Checks-first with eligible inline PR comments.

Comment thread packages/warden/src/action/workflow/pr-workflow.ts
Skip PR review feedback writes when a workflow run is no longer analyzing the current pull request head. Keep non-blocking unresolvable findings in Checks instead of body-only review comments, while preserving blocking request-changes reviews.

Co-Authored-By: GPT-5 Codex <noreply@anthropic.com>
Centralize the head-freshness check in a ReviewFeedbackGate that retries
transient GitHub errors, memoizes results briefly, and distinguishes a
stale head (skip silently; the newer run owns feedback) from an
unverifiable one (skip the write, and fail the run when that would
suppress a blocking REQUEST_CHANGES review). Previously a single
transient pulls.get failure silently disabled all review feedback for
the run, dropping the merge gate for requestChanges-only configs, and
every guard call was a separate uncached API request.

The gate replaces the optional fail-open guard callback in
evaluateFixesAndResolveStale, which now verifies the head before
spending LLM tokens on fix evaluation and no longer re-checks per
thread reply. Callers pass canResolveStale through unchanged, so the
'skipping due to trigger failures' log fires only for trigger failures
and head staleness gets its own log line.

Findings that never reach the PR now get an honest outcome: locationless
findings stripped from COMMENT review bodies and inline comments GitHub
rejects are recorded as skipped with the new no_inline_location reason
instead of being marked posted or dropped from the observation stream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/warden/src/action/workflow/pr-workflow.ts
@dcramer dcramer marked this pull request as ready for review July 4, 2026 00:41
The head-freshness check ran before postTriggerReview, but consolidation
and dedup inside it are LLM phases that can run for minutes before the
review write and duplicate-action comment updates. A head that advanced
during that window still received stale feedback.

The poster now takes the run's ReviewFeedbackGate and re-checks it after
the LLM phases, before the first GitHub write. The workflow escalation
for unverifiable heads moved after the post attempt so it also covers
heads that become unverifiable mid-post. The gate moved to action/review
since it guards review feedback, and the poster tests drive it through
the octokit boundary instead of an injected callback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5b5bf2f. Configure here.

Comment thread packages/warden/src/action/review/review-feedback-gate.ts
Comment thread packages/warden/src/action/review/poster.ts Outdated
processDuplicateActions sits between the poster's head check and
pulls.createReview and issues sequential GitHub writes, so with many
duplicate comments it can outlive the gate's cache window. Re-verify
the gate inside postReviewToGitHub immediately before the review write;
in the common case this is a cache hit, and it re-fetches exactly when
duplicate processing ran long.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/warden/src/action/review/poster.test.ts
The duplicate-processing regression test passed through a swallowed
TypeError: dedup shrank the finding set, the re-render path returned
the unmocked renderSkillReport's undefined, and postReviewToGitHub
threw before reaching the gate. Mock the re-render, assert the second
head fetch happened, and assert no findings were marked failed so the
test fails if the pre-write check is removed (verified by mutation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/warden/src/action/workflow/pr-workflow.ts
…nges

A review pass flagged the needsRequestChanges branch as uncovered by
the escalation predicate; it is only reachable past the poster's
reportOn early return, where the predicate is already true. State the
invariant so the next reader does not have to re-derive it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wouldPostBlockingReview assumes the poster's reportOn early return
makes the needsRequestChanges branch unreachable when no findings clear
reportOn. Exercise that scenario through the real poster so reordering
those checks fails a test instead of silently opening an escalation gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dcramer dcramer merged commit 3e6fa27 into main Jul 4, 2026
22 checks passed
@dcramer dcramer deleted the fix/warden-stale-pr-feedback branch July 4, 2026 04:11
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.

1 participant