Skip to content

fix(pr-review): never publish a clean verdict when the reviewer drops its findings - #541

Merged
charlesrhoward merged 7 commits into
mainfrom
fix/pr-review-dropped-findings
Sep 29, 2026
Merged

charlesrhoward merged 7 commits into
mainfrom
fix/pr-review-dropped-findings

Conversation

@charlesrhoward

@charlesrhoward charlesrhoward commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

The review on #535 said "one non-blocking warning and three minor suggestions, detailed in the comment", passed as "No material issues found", and listed nothing. The findings were never recorded anywhere, including the check run's output.

The stored ai_call shows why. The reviewer (zai/glm-5.3) called reportReview six times. The first five set hasIssues: true without a findings array, and the schema rejected each one. The sixth set hasIssues: false, still without findings; that passed validation and was published as a clean verdict. The same sequence appears in about ten reviews over the last day, including #530 and #536 and several webrenew repos. A second variant, where every call was rejected, left the rejected input as the report: webrenew/vmotif#1647 was published as "Attention needed" with nothing listed, and the missing-report repair never ran because a rejected call counted as filed.

Changes:

  • The normalized agent result keeps the SDK's invalid flag on tool calls, so the pipeline can tell an accepted report from a rejected one.
  • readReviewReportState (new, lib/workflows/pr-review-report-state.ts) is the one reader of the report. It uses the last accepted call. If an earlier call claimed issues and the accepted one clears the review with no findings, it returns dropped_findings.
  • A dropped_findings review gets one follow-up asking for the findings as entries in findings, using the same path as the missing-report repair. If the answer still lists nothing, the review is published as "Review incomplete" with a note saying why, gets a neutral check, and blocks auto-merge. A review whose every report was rejected now goes to the missing-report repair.
  • The reviewer's in-run mergePullRequest and queuePullRequestForMerge refuse to act on a dropped-findings report. They run before the post-run gate, so a flow with auto-merge could otherwise merge on the switched verdict.
  • The schema's rejection message now tells the model to resend with the findings, and that switching hasIssues to false leaves the review without a verdict.

One judgment call: a review that files findings and later files a clean report listing none is also treated as dropped. The reviewer keeps a way to report a clean review with suggestions: hasIssues: false with suggestion findings is accepted as before.

Tests cover the report-state rules, what a dropped-findings review publishes, the repair for both the all-rejected and dropped cases, and the merge-tool refusal. Each new behavior was mutation-checked: breaking it fails at least one test.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@mogplex mogplex Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mogplex PR Review

Status: Attention needed

Approve-ready: the dropped-findings fix works end-to-end — readReviewReportState refuses to trust a clearing report after an issues claim, the SDK's invalid flag survives into the normalized steps, the one-shot repair covers both the missing and dropped shapes, publication is a neutral "Review incomplete" check that blocks auto-merge, and tests cover the state rules, both repair kinds, the in-run refusal, and the published comment. One caveat: droppedFindingsRefusal in lib/agents/pr-reviewer.ts only fires after an accepted report, so the all-rejected incident shape can still merge in-run under an auto-merge flow before the post-run gate runs.

Warnings

  • In-run merge refusal doesn't cover the all-rejected report shape (lib/agents/pr-reviewer.ts:L104)
    In-run merge refusal doesn't cover the all-rejected report shape. droppedFindingsRefusal() (around L102-111) only refuses a merge when claimedIssues && acceptedReport !== null && clearsReviewWithoutFindings(acceptedReport).

    In the all-rejected shape this PR also fixes — a run where every reportReview claim fails schema validation (#1647-style) — acceptedReport stays null, so a reviewer that then calls mergePullRequest or queuePullRequestForMerge merges in-run, before finalizeFlowSuccess can run getPrReviewAutoMergeBlockReason (lib/workflows/automation-job-flow-success.ts). The check would then publish "Review incomplete" on an already-merged PR — the same hazard the refusal exists for, on a path where the post-run gate cannot intervene.

    Suggested tightening: refuse whenever issues were claimed and no clear-with-findings report was accepted, i.e. claimedIssues && (acceptedReport === null || clearsReviewWithoutFindings(acceptedReport)), which matches what readReviewReportState already does with the raw tool-call inputs. A test for the all-rejected-then-merge sequence would pin it down.

Suggestions

  • claimedIssues tracker misses malformed reportReview inputs (lib/agents/pr-reviewer.ts:L91)
    claimedIssues tracker misses malformed reportReview inputs. claimedIssues is set inside .superRefine (around L88-99), which Zod 3 skips when the base object itself fails to parse — a claim like { hasIssues: true } with summary missing is rejected without the refinement ever running, so claimedIssues stays false.

    The pipeline-side reader does not share this blind spot because it reads the raw tool-call input (call.input.hasIssues === true in lib/workflows/pr-review-report-state.ts), so only the in-run merge mirror is weakened: a malformed issue claim followed by an accepted clean report would let an in-run merge proceed while the published verdict is dropped_findings.

    A short comment documenting the asymmetry would help future readers; the runner is the only place that could observe rejected raw inputs directly.

  • Doc comment narrows what the implementation checks (lib/workflows/pr-review-report-state.ts:L36)
    Doc comment narrows what the implementation checks. The doc on readReviewReportState (L24-33) says the report clears "after an earlier call said there were issues", but the implementation checks calls.some((call) => call.input.hasIssues === true) — any issues claim in the run, including a rejected call placed after the accepted clearing report, yields dropped_findings.

    That conservative reading looks right and is pinned by the tests, but someone debugging a review that was republished as incomplete will search for an "earlier" claim and not find one. Consider rewording to "any call in the run, accepted or rejected, before or after the report".

  • No test for a repair whose own reportReview call fails validation (lib/workflows/pr-review-report-repair.ts:L130)
    No test for a repair whose own reportReview call fails validation. The incident reviewer fumbled reportReview five times in a row, so a repair call that also comes back schema-invalid is realistic.

    When that happens, readReviewReportState skips the invalid repair call and falls back to the original clearing report, leaving the review dropped_findings and published as incomplete, with no second repair — correct and loop-free, but untested.

    A case shaped like runReview([DROPPED_FINDINGS, INVALID_REPAIR]) would pin that branch.

View check run

@charlesrhoward

Copy link
Copy Markdown
Contributor Author

Addressed the review in 'fix(pr-review): refuse in-run merges when every issue report was rejected':

  • Warning (all-rejected shape merges in-run): the merge and queue tools now refuse whenever a report has claimed issues and no accepted report lists findings, which covers the case where nothing was ever accepted. Tests pin both tools for that sequence, and that a merge still proceeds when nothing claimed issues.
  • Malformed claims skip the refinement: documented on the superRefine. Only the in-run guard misses them; the published verdict reads the raw tool calls.
  • readReviewReportState doc now says any claim in the run, accepted or rejected, before or after the report.
  • Added the repair whose own report is rejected: the review stays dropped_findings, is published as incomplete, and is not asked again.

@mogplex mogplex Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mogplex PR Review

Status: Attention needed

Well-engineered fix with strong tests: verdict trust now derives from raw reportReview calls via readReviewReportState, with repair routing, neutral "Review incomplete" publication, and auto-merge blocking all wired correctly. Two warning-level gaps need discussion: cross-node claim matching in multi-review-node flows can void a legitimate post-fix clean re-review and block auto-merge, and the in-run merge guard misses claims rejected by base-schema validation (e.g., invalid finding severity) because claimedIssues is only set inside Zod's superRefine.

3 findings were added inline.

View check run

* accepted or rejected, before or after it, said there were issues has lost
* them: reviewers told that hasIssues=true needs findings sometimes flip
* hasIssues to false instead of listing them, and that report would
* otherwise publish as a clean verdict with its warnings gone. A report that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning: Dropped-findings matching spans multiple flow nodes, voiding legitimate re-review clean verdicts

readReviewReportState (lib/workflows/pr-review-report-state.ts:L27-L36) computes claimedIssues and the last accepted report across ALL merged steps. In flows, the merged result spans every agent node, and the flow finalization in automation-job-flow-success.ts re-extracts the verdict from it. A flow with two review-role nodes — e.g., review -> fix -> re-review — yields: node A files hasIssues:true with findings, the fix node repairs the PR, then node B re-reviews the fixed PR and legitimately files a clean report. The merged evaluation sees A's claim plus B's clearing report, classifies dropped_findings, and treats the verdict as missing, so the flow's auto-merge is blocked ("Mogplex review finished without a structured verdict") and the flow-level review publication is skipped. No repair runs in this case (fileMissingReviewReport runs per node, never on the merged result). Pre-PR behavior (last report wins) allowed this clean merge.

The heuristic "a clearing report after a claim lost the findings, since nothing says the findings were wrong" is right within one reviewer's session, but between nodes something did say the findings were wrong: the intervening fix.

Suggestion: scope claim-vs-clear matching to a single review node's own steps. executeFlowAgentNode already computes a per-node extractPrReviewHarnessResult(result).reviewOutcome; gating flow auto-merge and flow-level publication on the last review node's verdict (plus per-node verdictMissing) rather than re-reading the whole merged transcript avoids the false block. If the current fail-safe behavior across multiple review nodes is intended, a note in readReviewReportState's doc comment would make that explicit.

Comment thread lib/agents/pr-reviewer.ts
@@ -58,6 +59,8 @@ export function buildPRReviewTools(config: {
const request = config.fetch ?? fetch;
const contentOwner = config.headOwner ?? config.owner;
const contentRepo = config.headRepo ?? config.repo;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning: In-run merge guard blind spot: claims rejected by base schema never set claimedIssues

claimedIssues is set inside reportReviewInputSchema's superRefine (lib/agents/pr-reviewer.ts:L61-L79), which Zod only runs when the base object parses successfully. A reportReview call that fails base validation — missing summary, an invalid severity string like "blocker" in findings, line: 0, or more than 20 findings — never sets claimedIssues, so droppedFindingsRefusal() returns null and mergePullRequest/queuePullRequestForMerge proceed even though the reviewer claimed issues in this run (e.g., malformed-issues report, then clean accepted report, then merge). Post-run publication still detects dropped_findings via readReviewReportState, but the merge will already have executed before the post-run gate can block it — the in-run guard is the only control that acts before the merge.

The in-code comment acknowledges the blind spot ("Zod skips it for input missing a required field... the published verdict does not depend on this"), but that reassurance covers publication only; the merge refusal does depend on it. Observed retry behavior usually re-triggers superRefine and sets the flag, so this needs a single malformed claim followed by a flip to clean to slip through.

Suggestion: make the claim observable before strict validation rejects it — e.g., move required-field and finding-shape enforcement into superRefine itself (with the base object typed permissively so superRefine can set claimedIssues before adding its rejection issues), or record the flag from raw step data outside the schema. A regression test with a claim containing an invalid severity would pin the guarantee either way.

* The reviewer's report as the pipeline should trust it. Only calls the SDK
* accepted count as filed; a call whose input failed the schema was never
* recorded. A report that clears the review when any call in the run,
* accepted or rejected, before or after it, said there were issues has lost

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Claim-after-filed-report ordering: documented rule and behavior diverge for reports that list findings

The doc comment on readReviewReportState (lib/workflows/pr-review-report-state.ts:L24-L36) says a clearing report is dropped "when any call in the run, accepted or rejected, before or after it, said there were issues". In behavior, an accepted report that lists findings (e.g., hasIssues:false with suggestions) followed by a rejected hasIssues:true claim is treated as filed and published clean, while the identical rejected claim after a no-findings report yields dropped_findings. If a late claim deserves the repair ask in the lists-nothing case, consider whether it deserves it here too — or note the asymmetry in the comment so future readers don't puzzle over it.',

@charlesrhoward

Copy link
Copy Markdown
Contributor Author

Addressed the second review:

  • Warning, cross-node matching: a flow's verdict now comes from its last review node's own result (FlowRunState.reviewResult, set by recordFlowNodeResult), so a clean re-review after an earlier node's findings is published as clean and can merge. The flow's autofix outcome still comes from the whole run. A new flow test runs review → re-review → merge with issues and then a clean report, and asserts the merge happens.
  • Warning, claims the base schema rejects: the claim is now recorded in a z.preprocess step ahead of validation, so a malformed report (unknown severity, missing summary) still counts. A test pins the invalid-severity-then-clean-then-merge sequence, and another checks that the JSON schema the model sees is unchanged (required fields and the severity enum).
  • Suggestion, asymmetry: the readReviewReportState doc now says the rule is about lost findings, not the verdict. A report that lists findings has lost nothing, and steps are one review session's.

@mogplex

mogplex Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Mogplex PR Review

Status: No material issues found

Approve-ready: the fix is correct and layered across claim detection, per-session verdict state, a one-shot repair, a per-node flow verdict, and in-run merge refusals; the AI SDK behaviors it depends on (invalid tool calls keep parsed input with invalid:true; z.preprocess does not change the model-facing JSON schema) check out against upstream sources. Three non-blocking suggestions are filed: a parallel-review race on the flow verdict, the dropped-findings repair prompt's handling of deliberate retractions, and an adjacent pre-existing gap where an in-run merge can happen with no report filed at all.

Affected files

  • lib/workflows/automation-job-flow-run-state.ts
  • lib/workflows/pr-review-report-repair.ts
  • lib/agents/pr-reviewer.ts

Suggestions

  • Parallel review nodes race for the flow's state.reviewResult (lib/workflows/automation-job-flow-run-state.ts)
    recordFlowNodeResult assigns state.reviewResult for every review-role node, and executeResolvedFlow runs each ready batch with Promise.all. A flow with two review nodes on parallel branches therefore gets whichever of them completes last as its verdict — nondeterministic — and the comment about reading review report state ("a flow's later review node is judged on its own steps") reads as assuming sequential reviews.

    Was a parallel-review topology considered? If it can occur, consider defining the winner explicitly (e.g., the last review node in topological order) or rejecting flows with parallel review branches.

    Pre-existing behavior (merging every node's steps into one transcript) was equally arbitrary, so this is a scoping question rather than a regression.

  • Dropped-findings repair prompt leaves a deliberate retraction no honest path (lib/workflows/pr-review-report-repair.ts:L16)
    PR_REVIEW_DROPPED_FINDINGS_PROMPT instructs the model to "Call reportReview now with the findings you said you found", presupposing the findings still stand.

    A reviewer that deliberately retracted its issues after filing them has no way to reach a clean verdict in-session: obeying the prompt re-lists findings the reviewer no longer believes (publishing a request-changes verdict), while filing a clean report leaves the run dropped_findings and published as "Review incomplete".

    The conservative outcome is clearly intentional and documented in the PR and the comment about reading review report state, but the prompt could state the consequence explicitly — e.g., "if you are sure there are no issues, file that and the review will be marked incomplete" — so the model does not guess between re-listing and clearing.

  • In-run mergePullRequest still allowed when no report was ever filed (lib/agents/pr-reviewer.ts)
    droppedFindingsRefusal only blocks a merge when claimedIssues is true; a reviewer that never calls reportReview at all can still call mergePullRequest and merge immediately, and the post-run gate that computes the auto-merge block reason cannot undo an already-executed merge.

    This is pre-existing behavior outside this PR's scope, but the refusal condition already computes acceptedReport === null; refusing in-run merges when no accepted report exists would close the adjacent gap cheaply — the tool description's "Only call this after reporting hasIssues=false" is the only current guard for that path.

View check run

@charlesrhoward

Copy link
Copy Markdown
Contributor Author

Answers to the three suggestions in the last review (no warnings, so merging as is):

  • Parallel review nodes: a flow whose review nodes run on parallel branches gets the one that finishes last as its verdict. Before this PR it was the same, via the last report in the merged transcript, so this isn't a regression. Choosing a winner for parallel reviews is a flow-design question, and I've left it out of this fix.
  • Deliberate retractions: the dropped-findings prompt is getting the explicit consequence ("if you are sure there are no issues, say so; the review is then published as incomplete") in feat(pr-review): the reviewer fixes its own review when the format check flags it #542, which already rewrites the report follow-ups in this module.
  • In-run merge with no report filed: pre-existing, and closing it changes behavior for live auto-merge flows, so it's tracked in PR review: in-run merge tools allow a merge before any report is filed #543.

@charlesrhoward
charlesrhoward added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit b9316c1 Sep 29, 2026
17 of 18 checks passed
@charlesrhoward
charlesrhoward deleted the fix/pr-review-dropped-findings branch September 29, 2026 18:53
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