From ceab7f79b3b286ef2a7c70d602a103895e6fffc6 Mon Sep 17 00:00:00 2001 From: LBX154 <145820328+lbx154@users.noreply.github.com> Date: Wed, 30 Sep 2026 04:16:03 -0700 Subject: [PATCH 1/2] Count the Reviewer's verdict when the project never selected a venue A research project that reaches its final review without anything having set target_venue could not finish that review. The Reviewer's tool wrote the selected venue, an empty string, into its assessment; the venue check treats a blank venue as no assessment at all, reported "Final Reviewer omitted a valid assessment for the selected venue", counted it as a Reviewer backend failure, and after two such rounds stopped the task, while the Manager read the same review as a weak reject. The trial roofline campaign (s-02d3c282, mission 00ea5f8228e0) died exactly this way, and the lesson page Argus wrote from it blamed the Reviewer. When no venue is selected the review is now judged against the stated standard: the Reviewer's instruction says so instead of naming "(not selected)", the assessment carries that label as its venue, and the verdict counts like any other, passing or failing the operator's bar on its own merits. Co-Authored-By: Claude Fable 5.1 --- argus/core/venue_review.py | 23 +++++++++++++++++++++-- argus/reviewer/_core.py | 4 ++-- tests/test_venue_acceptance.py | 27 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/argus/core/venue_review.py b/argus/core/venue_review.py index c7a116b3e..7a94784df 100644 --- a/argus/core/venue_review.py +++ b/argus/core/venue_review.py @@ -32,6 +32,19 @@ def selected_venue(state_root: Path | str) -> str: return str(state.get("target_venue") or state.get("venue") or "").strip() +UNSELECTED_VENUE = "the stated standard (no venue selected)" + + +def venue_for_review(state_root: Path | str) -> str: + """The selected venue, or the label the final review is judged against when none was selected. + + A project can reach its final review with nothing having set ``target_venue``. + The Reviewer then judges against the stated standard and the operator's bar, + and its verdict must count: a blank venue field is not a missing verdict. + """ + return selected_venue(state_root) or UNSELECTED_VENUE + + def selected_acceptance_minimum(state_root: Path | str) -> str: """Read the operator's bar, independently of the Reviewer's own rating.""" from .pipeline_state import read_pipeline_state @@ -255,8 +268,14 @@ def venue_review_instruction(venue: str, *, minimum: str = "weak_accept") -> str minimum_label = minimum.replace("_", " ") return ( "## Final paper acceptance — mandatory operator standard\n" - f"Act as an independent reviewer for the currently selected venue: {venue or '(not selected)'}. " - "Read its researched criteria and the actual current manuscript, rendered pages, " + + ( + f"Act as an independent reviewer for the currently selected venue: {venue}. " + "Read its researched criteria and the actual current manuscript, rendered pages, " + if venue and venue != UNSELECTED_VENUE else + "No venue has been selected for this paper. Act as an independent reviewer for a " + "strong venue in its field, say which standard you applied, and read the actual " + "current manuscript, rendered pages, " + ) + "and claim-critical evidence. Judge novelty, significance, soundness, evidence, " "reproducibility, presentation, and fit at that venue. Finishing edits, compiling, " "an old certificate, or the Engineer's confidence is not an acceptance decision.\n" diff --git a/argus/reviewer/_core.py b/argus/reviewer/_core.py index adacd93c4..6fbf673d6 100644 --- a/argus/reviewer/_core.py +++ b/argus/reviewer/_core.py @@ -741,7 +741,7 @@ def evaluate( paper_review_snapshot, requires_venue_review, selected_acceptance_minimum, - selected_venue, + venue_for_review, venue_review_instruction, ) from ..skills.vertical_select import resolve_vertical_if_decided @@ -754,7 +754,7 @@ def evaluate( scope=scope, operation=operation, ) - venue = selected_venue(state_root) if venue_required else "" + venue = venue_for_review(state_root) if venue_required else "" acceptance_minimum = selected_acceptance_minimum(state_root) if venue_required else "weak_accept" venue_snapshot = paper_review_snapshot(artifact_root) if venue_required else None reviewed_manuscript_snapshot = None diff --git a/tests/test_venue_acceptance.py b/tests/test_venue_acceptance.py index 39ca75242..cfbe3a459 100644 --- a/tests/test_venue_acceptance.py +++ b/tests/test_venue_acceptance.py @@ -319,3 +319,30 @@ def test_stage_certificate_keeps_venue_rating_and_expires_on_figure_changes(pape record = latest_stage_review(paper, "review") assert record["certified"] is False assert "changed" in record["stale_reason"] + + +@pytest.mark.parametrize("recommendation, clear", [("weak_accept", True), ("weak_reject", False)]) +def test_reviewer_verdict_counts_when_no_venue_was_selected(paper, recommendation, clear): + from argus.core.venue_review import UNSELECTED_VENUE, venue_for_review + + state_root = paper / "session-state" + persist_vertical(state_root, "research") + state = read_pipeline_state(state_root) + state["current_stage"] = "review" + write_pipeline_state(state_root, state) + assert venue_for_review(state_root) == UNSELECTED_VENUE + runner = _Runner(assessment(recommendation, clear=clear)) + review = Reviewer(runner).evaluate( + objective="Judge the current paper", round_index=1, session_id=None, + main_summary="Ready for review", main_error=None, scope="final_submission", + config=ReviewerConfig(model="gpt-5.6-sol", active_vertical="research", working_dir=str(paper), artifact_root=str(paper), vertical_state_root=str(state_root)), + ) + assert "No venue has been selected for this paper" in runner.prompt + assert "(not selected)" not in runner.prompt + # A verdict against the stated standard is a verdict: never a Reviewer backend failure. + assert not review.backend_unavailable + assert "omitted a valid assessment" not in (review.reason or "") + assert review.venue_review["venue"] == UNSELECTED_VENUE + assert review.venue_review["recommendation"] == recommendation + assert f"Recommendation: {recommendation}" in (paper / "paper/REVIEW.md").read_text() + assert review.final_submission_certified is (recommendation == "weak_accept") From 6e4d53142eb48712f1b3bbb5f183ad79ab84f161 Mon Sep 17 00:00:00 2001 From: LBX154 <145820328+lbx154@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:08:13 -0700 Subject: [PATCH 2/2] Let a project with no selected venue pass the final gate The Reviewer path judged a no-venue paper against a fallback label, but the finalizers still required a selected venue, so an accepting verdict never let the project finish. An unselected venue now means there is nothing to bind the verdict to: the record keeps an empty venue (the report shows the standard label), the finalizers and stage certificate accept it, and the wording says "the stated standard" instead of "the selected venue". Co-Authored-By: Claude Opus 5.5 (1M context) --- argus/core/venue_review.py | 38 ++++++++++++++++++---------------- argus/reviewer/_core.py | 8 ++++--- tests/test_venue_acceptance.py | 38 ++++++++++++++++++++++++---------- 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/argus/core/venue_review.py b/argus/core/venue_review.py index 7a94784df..914e77ccc 100644 --- a/argus/core/venue_review.py +++ b/argus/core/venue_review.py @@ -32,17 +32,15 @@ def selected_venue(state_root: Path | str) -> str: return str(state.get("target_venue") or state.get("venue") or "").strip() +# Shown where a venue name would be when none was selected. A project can reach +# its final review with nothing having set ``target_venue``; the Reviewer then +# judges against the stated standard and the operator's bar, and its verdict +# counts. The recorded ``venue`` stays empty rather than holding this label. UNSELECTED_VENUE = "the stated standard (no venue selected)" -def venue_for_review(state_root: Path | str) -> str: - """The selected venue, or the label the final review is judged against when none was selected. - - A project can reach its final review with nothing having set ``target_venue``. - The Reviewer then judges against the stated standard and the operator's bar, - and its verdict must count: a blank venue field is not a missing verdict. - """ - return selected_venue(state_root) or UNSELECTED_VENUE +def _venue_phrase(venue: str) -> str: + return "the selected venue" if venue else "the stated standard" def selected_acceptance_minimum(state_root: Path | str) -> str: @@ -75,14 +73,14 @@ def _venue_key(value: str) -> str: def normalize_venue_review(value: Any) -> dict[str, Any] | None: if not isinstance(value, Mapping): return None - venue = value.get("venue") + venue = value.get("venue", "") rationale = value.get("rationale") recommendation = re.sub(r"[\s-]+", "_", str(value.get("recommendation") or "").strip().lower()) if recommendation == "best_paper_level": recommendation = "best_paper" issues = value.get("blocking_issues") if ( - not isinstance(venue, str) or not venue.strip() + not isinstance(venue, str) or not isinstance(rationale, str) or not rationale.strip() or recommendation not in RECOMMENDATIONS or not isinstance(value.get("acceptance_clear"), bool) @@ -105,18 +103,18 @@ def normalize_venue_review(value: Any) -> dict[str, Any] | None: def venue_review_issue(value: Any, *, venue: str, minimum: str = "weak_accept") -> str: assessment = normalize_venue_review(value) - if not venue: - return "no selected venue for the final paper review" if assessment is None: return "missing or invalid explicit venue recommendation" - if _venue_key(assessment["venue"]) != _venue_key(venue): + # With no selected venue there is nothing to bind the verdict to; with one, + # the verdict must be for that venue. + if venue and _venue_key(assessment["venue"]) != _venue_key(venue): return f"reviewed venue {assessment['venue']!r} differs from selected venue {venue!r}" if minimum not in ACCEPTED_RECOMMENDATIONS: return f"invalid operator venue acceptance minimum: {minimum!r}" if RECOMMENDATIONS.index(assessment["recommendation"]) < RECOMMENDATIONS.index(minimum): return f"venue recommendation is {assessment['recommendation']}; {minimum} or better is required" if assessment["acceptance_clear"] is not True: - return "Reviewer did not clearly support acceptance at the selected venue" + return f"Reviewer did not clearly support acceptance at {_venue_phrase(venue)}" if assessment["blocking_issues"]: return "reject-level issues remain: " + "; ".join(assessment["blocking_issues"]) if assessment.get("revision_required") is True: @@ -204,8 +202,11 @@ def enforce_venue_acceptance( decision.status = "blocked" decision.backend_unavailable = True decision.backend_stop_kind = "backend_unavailable" - decision.reason = "Final Reviewer omitted a valid assessment for the selected venue." - decision.next_action = "Retry the independent Reviewer on the same paper and submit a review action with its actual recommendation for the selected venue." + decision.reason = f"Final Reviewer omitted a valid assessment for {_venue_phrase(venue)}." + decision.next_action = ( + "Retry the independent Reviewer on the same paper and submit a review action " + f"with its actual recommendation for {_venue_phrase(venue)}." + ) return current = paper_review_snapshot(artifact_root) issue = venue_review_issue(assessment, venue=venue, minimum=minimum) @@ -236,7 +237,8 @@ def enforce_venue_acceptance( decision.reason = assessment["rationale"] if not decision.next_action.strip(): repairs = "; ".join(assessment["blocking_issues"]) or assessment["rationale"] - decision.next_action = "Revise the current paper against the selected venue's standard: " + repairs + standard = "the selected venue's standard" if venue else "the stated standard" + decision.next_action = f"Revise the current paper against {standard}: " + repairs if ( minimum in ACCEPTED_RECOMMENDATIONS and assessment["recommendation"] in ACCEPTED_RECOMMENDATIONS @@ -271,7 +273,7 @@ def venue_review_instruction(venue: str, *, minimum: str = "weak_accept") -> str + ( f"Act as an independent reviewer for the currently selected venue: {venue}. " "Read its researched criteria and the actual current manuscript, rendered pages, " - if venue and venue != UNSELECTED_VENUE else + if venue else "No venue has been selected for this paper. Act as an independent reviewer for a " "strong venue in its field, say which standard you applied, and read the actual " "current manuscript, rendered pages, " diff --git a/argus/reviewer/_core.py b/argus/reviewer/_core.py index 6fbf673d6..78fda2cf5 100644 --- a/argus/reviewer/_core.py +++ b/argus/reviewer/_core.py @@ -581,12 +581,14 @@ def _persist_research_review( or "" ).strip() if decision.venue_review is not None: + from ..core.venue_review import UNSELECTED_VENUE + challenge = "\n".join(f"- {issue}" for issue in decision.venue_review["blocking_issues"]) text = ( "# Authoritative review\n\n" + ( "## Selected-venue assessment\n" - f"Venue: {decision.venue_review['venue']}\n\n" + f"Venue: {decision.venue_review['venue'] or UNSELECTED_VENUE}\n\n" f"Recommendation: {decision.venue_review['recommendation']}\n\n" if decision.venue_review is not None else f"**Judgment:** {decision.status}\n\n" ) @@ -741,7 +743,7 @@ def evaluate( paper_review_snapshot, requires_venue_review, selected_acceptance_minimum, - venue_for_review, + selected_venue, venue_review_instruction, ) from ..skills.vertical_select import resolve_vertical_if_decided @@ -754,7 +756,7 @@ def evaluate( scope=scope, operation=operation, ) - venue = venue_for_review(state_root) if venue_required else "" + venue = selected_venue(state_root) if venue_required else "" acceptance_minimum = selected_acceptance_minimum(state_root) if venue_required else "weak_accept" venue_snapshot = paper_review_snapshot(artifact_root) if venue_required else None reviewed_manuscript_snapshot = None diff --git a/tests/test_venue_acceptance.py b/tests/test_venue_acceptance.py index cfbe3a459..20804e3b7 100644 --- a/tests/test_venue_acceptance.py +++ b/tests/test_venue_acceptance.py @@ -321,17 +321,18 @@ def test_stage_certificate_keeps_venue_rating_and_expires_on_figure_changes(pape assert "changed" in record["stale_reason"] -@pytest.mark.parametrize("recommendation, clear", [("weak_accept", True), ("weak_reject", False)]) -def test_reviewer_verdict_counts_when_no_venue_was_selected(paper, recommendation, clear): - from argus.core.venue_review import UNSELECTED_VENUE, venue_for_review +@pytest.mark.parametrize("recommendation", ["weak_accept", "weak_reject"]) +def test_reviewer_verdict_counts_when_no_venue_was_selected(paper, recommendation): + from types import SimpleNamespace - state_root = paper / "session-state" - persist_vertical(state_root, "research") + from argus.core.stage_certificate import latest_stage_review, record_stage_review + + state_root = paper state = read_pipeline_state(state_root) - state["current_stage"] = "review" + state.pop("target_venue", None) + state.pop("venue", None) write_pipeline_state(state_root, state) - assert venue_for_review(state_root) == UNSELECTED_VENUE - runner = _Runner(assessment(recommendation, clear=clear)) + runner = _Runner(assessment(recommendation)) review = Reviewer(runner).evaluate( objective="Judge the current paper", round_index=1, session_id=None, main_summary="Ready for review", main_error=None, scope="final_submission", @@ -342,7 +343,22 @@ def test_reviewer_verdict_counts_when_no_venue_was_selected(paper, recommendatio # A verdict against the stated standard is a verdict: never a Reviewer backend failure. assert not review.backend_unavailable assert "omitted a valid assessment" not in (review.reason or "") - assert review.venue_review["venue"] == UNSELECTED_VENUE + # No venue name is invented for the record. + assert review.venue_review["venue"] == "" assert review.venue_review["recommendation"] == recommendation - assert f"Recommendation: {recommendation}" in (paper / "paper/REVIEW.md").read_text() - assert review.final_submission_certified is (recommendation == "weak_accept") + report = (paper / "paper/REVIEW.md").read_text() + assert f"Recommendation: {recommendation}" in report + accepted = recommendation == "weak_accept" + assert review.final_submission_certified is accepted + # The finalizers apply the same check: an accepting verdict lets the project finish. + issue = current_venue_acceptance_issue(review, state_root=state_root, artifact_root=paper) + assert (issue == "") is accepted + record_stage_review( + state_root=state_root, project_root=paper, stage="review", item=SimpleNamespace(id="final-review"), + manager_action="complete", venue_review=review.venue_review, + venue_review_snapshot=review.venue_review_snapshot, + ) + assert latest_stage_review(state_root, "review")["certified"] is accepted + if not accepted: + assert "selected venue" not in issue + assert "selected venue" not in review.next_action