postrun: tighten unsubmitted-review gate to require create_pull_request_review for Review mode (#724)
The gate at `getUnsubmittedReview` accepted `toolState.finalSummaryWritten` as a valid Review exit, contradicting the post-failure error message which already says Review's only valid exit is `create_pull_request_review`. This let any caller that flipped `finalSummaryWritten` — including a `task`-dispatched `reviewfrog` subagent calling `pullfrog_report_progress` in violation of its prose-only read-only contract — silence the gate even when the orchestrator never submitted a review. Split per-mode: Review requires `toolState.review`, IncrementalReview keeps the existing `||` (its post-failure message explicitly accepts `report_progress` as a "no review warranted" exit). Test split mirrors the new semantics. closes #648
This commit is contained in:
committed by
pullfrog[bot]
parent
b9f0938405
commit
d857e06731
+14
-1
@@ -29,9 +29,22 @@ describe("getUnsubmittedReview", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when report_progress wrote a final summary", () => {
|
||||
it("fires for Review even when report_progress wrote a final summary", () => {
|
||||
// Review's only valid exit is `create_pull_request_review`. a summary
|
||||
// comment is not a substitute, and accepting it here previously let
|
||||
// subagent-flipped `finalSummaryWritten` silence the gate.
|
||||
expect(
|
||||
getUnsubmittedReview(makeToolState({ selectedMode: "Review", finalSummaryWritten: true }))
|
||||
).toBe("Review");
|
||||
});
|
||||
|
||||
it("returns null for IncrementalReview when report_progress wrote a final summary", () => {
|
||||
// IncrementalReview treats `report_progress` as a legitimate
|
||||
// "no review warranted" exit, matching the post-failure error message.
|
||||
expect(
|
||||
getUnsubmittedReview(
|
||||
makeToolState({ selectedMode: "IncrementalReview", finalSummaryWritten: true })
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
+16
-3
@@ -30,13 +30,26 @@ import {
|
||||
*
|
||||
* the gate is anchored to `hadProgressComment` so silent runs (non-issue
|
||||
* events, dispatcher skipped seeding) don't fire a nudge there's no UI for.
|
||||
*
|
||||
* `Review` and `IncrementalReview` have different valid exits:
|
||||
* - Review: only `create_pull_request_review` counts. `report_progress` is
|
||||
* not a substitute — a Review run that exits with just a summary comment
|
||||
* has produced nothing reviewable on the PR. matches the hard-fail
|
||||
* message at `expected = "create_pull_request_review"` below.
|
||||
* - IncrementalReview: `report_progress` is a legitimate "no review
|
||||
* warranted" exit, so either toolState flag short-circuits.
|
||||
* splitting per mode also closes the bypass where a subagent (e.g. a
|
||||
* `task`-dispatched `reviewfrog` lens) calls `report_progress` and silences
|
||||
* the gate even though the orchestrator never submitted a review.
|
||||
*/
|
||||
export function getUnsubmittedReview(toolState: ToolState): "Review" | "IncrementalReview" | null {
|
||||
const mode = toolState.selectedMode;
|
||||
if (mode !== "Review" && mode !== "IncrementalReview") return null;
|
||||
if (toolState.review || toolState.finalSummaryWritten) return null;
|
||||
if (!toolState.hadProgressComment) return null;
|
||||
return mode;
|
||||
if (mode === "Review") return toolState.review ? null : "Review";
|
||||
if (mode === "IncrementalReview") {
|
||||
return toolState.review || toolState.finalSummaryWritten ? null : "IncrementalReview";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user