Skip to content

fix(specialist): stop rejecting our own proposals on a format rule we ship broken - #1161

Merged
ZhengGong-amd merged 31 commits into
mainfrom
fix/1143-proposal-schema
Aug 17, 2026
Merged

fix(specialist): stop rejecting our own proposals on a format rule we ship broken#1161
ZhengGong-amd merged 31 commits into
mainfrom
fix/1143-proposal-schema

Conversation

@zoroyihan7

@zoroyihan7 zoroyihan7 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Refs #1143.

Deliberately not Closes: #1143's agreed fix had two halves, and only the first
is here. (a) strip the forbidden fields at source instead of only auditing them —
done. (b) pass approved_variant_names so a partial reject stops discarding
every variant — untouched, and _handle_review_verdict's collapse still ranks
reject above advise, so a multi-variant proposal with one genuinely rejected
variant still loses the set. The reported failure no longer produces a reject
at all, but that general problem outlives this PR and the issue should stay open
for it.

The Critic rejected its own scouts' proposals for carrying a self-reported gain
field, so the run's only novel optimization ideas never reached a benchmark and
there was no budget left to resubmit. The cause is entirely in this repo — no
run artifacts were needed to reproduce the reasoning.

What was actually wrong

The system detected the violation, declined to fix it, then punished itself for it.
scan_quantitative_claims finds the forbidden fields, records them in an audit
note, and forwards the payload unchanged; the Critic is then instructed to reject
the whole proposal_set over them.

We also instructed the specialist to commit the violation. The cold-start
directive told it to flag each fallback proposal confidence: low — a field in
FORBIDDEN_PROPOSAL_FIELDS. A compliant specialist therefore tripped the guard
on exactly the round where it was the only source of ideas, which is the reported
scenario.

The guard's scope contradicted itself. The rule the Critic is given covers
proposal_set[*], but the scan also applied it to the payload top level, where
confidence is the round-level self-assessment our own output schema asks for
and two audit writers record. That is not a per-proposal gain claim and cannot
bias which variant gets benched.

The ban was prose only, so the Critic generalised it. specialist_quantitative_claim_violation
appears nowhere in the codebase except the Critic prompt — nothing produces,
consumes or tests it. The verdict that lost the run named predicted_gain_pct,
which is not on the enforced list, and is a required field on the
propose_action channel taught by our own prompt template. No output could
satisfy both readings of that field name.

Changes

Commit 1 — fix(specialist): stop shipping a proposal schema that fails its own guard

  • The runner strips the forbidden fields before writing specialist_done.json.
    The claim is worthless either way (measured gain is the Coordinator's), so
    dropping it makes the verdict unreachable rather than merely audited; the audit
    note still records what was there.
  • Cold start no longer asks for confidence.
  • Guard scope split so the top-level round assessment survives.

Commit 2 — fix(critic): make a proposal format slip advisory instead of fatal

  • review_constraints now carries a rule descriptor generated from
    FORBIDDEN_PROPOSAL_FIELDS, so the Critic's list cannot drift from the
    enforced one. Uses the mechanism the cross-domain rules already use rather than
    templating a verbatim-loaded prompt.
  • The verdict for this class becomes advise. Detection breadth is deliberately
    kept — a gain claim smuggled under an unlisted name should still be noticed —
    but noticing it must not reject the set.

Commit 3 — fix(loop): hold a reject to the verdict its own rule declared

Commit 2 left the behavioural requirement as prose, same class as the defect
it fixes: nothing stopped the Critic rejecting anyway, and the reason code it
cites had no production consumer, so a repeat would lose the round undetected.

  • advisory_only_reason_codes() derives the code set from the descriptors the
    Critic is handed — the quantitative-claim rule plus every cross-domain rule
    declaring advise — rather than restating them, so a rule that changes its
    verdict cannot leave a stale entry behind.
  • A reject citing one of those codes is held to advise in the verdict path,
    logged, and recorded as an observation, so prompt drift stays visible instead
    of being silently corrected.
  • The hold runs per verdict_map entry before the collapse, since the collapse
    ranks reject above advise: one variant rejected on an advisory-only rule
    would otherwise still discard its siblings' advice. This narrows the blast
    radius for this class only; half (b) above is still the general fix.
  • advise is already permissive for the integrate_patch gate, so the downgrade
    lands the proposal exactly where an obedient Critic would have.

Kept as separate commits so the prompt-behaviour change can be reverted on its own.

Test plan

  • test_specialist_patch_safety_unit.py — strip semantics, non-dict payloads,
    round-level confidence preserved, descriptor matches the enforced list and
    carries advise, and the advisory-code set covers every rule that declared
    advise while leaving a rule that kept reject out of it
  • test_critic_verdict_map.py — a reject citing an advisory-only rule is held
    to advise and materialises; the downgrade leaves a log line and an
    observation; a cross-domain hint reject is held too; a reject with no code or
    a code outside the set still rejects; one held variant does not out-rank its
    siblings, and a map rejected only on advisory grounds survives
  • test_specialist_runner_helpers_unit.py_finalize writes a stripped
    artifact and still emits the audit note
  • test_specialist_prompt_builder_coverage_unit.py — cold start no longer
    asks for a forbidden field
  • test_critic_agent_backend.py — the bundle delivered to the model carries
    the rule
  • 1544 passed across the specialist / critic / explore / writeback / prompt
    suites; the only 2 failures are pre-existing (claude-agent-sdk not
    installed in this environment, identical on the base commit)

Made with Cursor

zoroyihan7 and others added 2 commits August 12, 2026 05:45
…uard

The Critic is instructed to reject a whole ``proposal_set`` that carries a
self-reported gain field, and the runner detected those fields, wrote an audit
note, and forwarded them anyway. So a format slip cost the round every idea the
specialist produced, with no budget left to resubmit -- the run's only novel
optimizations never reached a benchmark.

Two sides of the same defect:

* The runner now strips the forbidden fields before writing
  ``specialist_done.json``. The claim is worthless either way (measured gain is
  the Coordinator's), so dropping it makes the verdict unreachable rather than
  merely audited. The audit note still records what was there.
* Cold start actively instructed the violation: it told the specialist to flag
  each fallback proposal ``confidence: low``, which is in
  FORBIDDEN_PROPOSAL_FIELDS. A compliant specialist therefore tripped the guard
  on exactly the round where it was the only source of ideas.

Also splits the guard's scope to match the rule the Critic is given. The ban is
on ``proposal_set[*]``, but the scan applied it to the payload top level too,
where ``confidence`` is the round-level self-assessment our own output schema
asks for and two audit writers record -- not a per-proposal gain claim, and
unable to bias which variant gets benched.

Refs #1143

Co-authored-by: Cursor <cursoragent@cursor.com>
The quantitative-claim guard lived only as a hand-copied field list in the
Critic prompt, with nothing in the codebase producing, consuming or testing its
reason code. So the Critic generalised it: the verdict that lost a real run
named ``predicted_gain_pct``, which is not on the enforced list at all -- and is
a required field on the ``propose_action`` channel, taught by our own prompt
template. Nothing could satisfy both readings of the same field name.

Delivers the rule as data instead: ``review_constraints`` now carries a
descriptor generated from FORBIDDEN_PROPOSAL_FIELDS, so the Critic's list cannot
drift from the one the runner strips. Follows the mechanism the cross-domain
rules already use rather than templating the verbatim-loaded prompt.

The verdict for this class becomes ``advise``. Detection breadth is deliberately
kept -- a gain claim smuggled under an unlisted name should still be noticed --
but noticing it must not reject the set: the fields are stripped before review,
so anything arriving is a format problem, and rejecting on format costs the
round every proposal in it with no chance to resubmit.

Refs #1143

Co-authored-by: Cursor <cursoragent@cursor.com>
@zoroyihan7
zoroyihan7 requested a review from a team as a code owner August 12, 2026 05:59
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

CI E2E report — ✅ Succeeded

item value
result ✅ Succeeded
model Qwen/Qwen3-0.6B (dense)
resources 1× GPU, TP=1
PR branch fix/1143-proposal-schema
commit c14ee71cbd849e6f86fb7880e083089242534e66
session_id 66d75126-eb9b-414e-8c37-c41bdfb1575e
queue → dispatch 0s
run time 160m 17s
total 160m 17s

details

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	src/hyperloom/orchestrator/prompts/critic.md

@chaojhou chaojhou left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The diagnosis in this PR is better than the issue it closes. Two root causes here were not in #1143 at all, and both sit closer to the real defect than the field-name confusion the issue led with:

  • We instructed the specialist to commit the violation. The cold-start directive asked it to flag each fallback proposal confidence: low, and confidence is in FORBIDDEN_PROPOSAL_FIELDS. A compliant specialist therefore tripped the guard on exactly the round where it was the only source of ideas.
  • The guard's scope contradicted itself. The rule handed to the Critic covers proposal_set[*], but the scan also applied it to the payload top level, where confidence is the round-level self-assessment our own output schema asks for and two audit writers record.

I checked the implementation against main rather than taking it on trust, and these hold:

  • The strip runs before self._write_specialist_done(...), and the ordering is scan-then-strip, so the audit note keeps the evidence while the artifact ships clean.
  • The drift is structurally gone: quantitative_claim_rule_descriptor() derives forbidden_proposal_fields from sorted(FORBIDDEN_PROPOSAL_FIELDS) and critic.md no longer hardcodes the six names. Worth noting the prompt copy had already drifted — it was missing expected_gain_pct.
  • advise genuinely lets the proposal through: intent_router.py materializes on verdict in ("approve", "advise"), so switching the verdict is the real fix and not just a softer label.
  • The framework_agent carve-out for predicted_gain_pct / prior_score / prior_rank is preserved and clarified. That matters, because the verdict that lost the run named predicted_gain_pct and anyone fixing this from the issue text alone would plausibly have deleted a field that propose_action, the Critic's own verdict schema and the gain-calibration path all depend on.

Two things I would still change.

1. The remedy is prose-only, same class as the diagnosis (can be a follow-up)

The PR's own finding is "the ban was prose only, so the Critic generalised it". The rule is now delivered as data, which fixes the drift — but the behavioural requirement ("emit advise, never reject") is still an instruction to a model with no code backstop.

QUANTITATIVE_CLAIM_REASON_CODE has zero production consumers: the constant, the descriptor field, and one test asserting the descriptor carries it. So if the Critic still emits reject with this reason, the round is lost exactly as before and nothing detects it.

Now that failure_reason_code is a named constant travelling in the bundle, a deterministic backstop is cheap: in the verdict path, when verdict == "reject" and the reason matches QUANTITATIVE_CLAIM_REASON_CODE, downgrade to advise and log it. That turns this from "we asked nicely" into "it cannot happen". Given the cost of the failure is an entire round's novel ideas, I think it is worth having — but I am fine with it landing separately.

2. Closes #1143 overstates the scope (worth changing now)

#1143's agreed fix had two halves: (a) strip the forbidden fields at source instead of only auditing them, and (b) pass approved_variant_names so a partial reject stops discarding every variant.

(a) is done thoroughly here. (b) is untouched, and the collapse in intent_router.py still ranks reject above advise:

verdict = ("approve" if "approve" in sub_verdicts
           else "reject" if "reject" in sub_verdicts
           else "advise" if "advise" in sub_verdicts
           else "needs_review")

So a multi-variant proposal with one genuinely rejected variant still discards the whole set, including the ones that should have been advised through, and approved_variant_names is still never passed on the live path.

Since this class no longer produces a reject, the reported failure is fixed — but the general "one bad variant kills the set" problem remains. I would either do (b) here or retitle to Refs #1143 and keep the issue open for it. My preference is the retitle: if the issue closes on merge, (b) is the half that quietly gets forgotten.

Nit

strip_forbidden_proposal_fields returns the removed field names but the production call site ignores them (the audit uses the scan's list). Not a defect, just noting the return value exists for the tests.

Splitting the prompt-behaviour change into its own commit so it can be reverted alone is the right instinct for a change like this.

zoroyihan7 and others added 2 commits August 13, 2026 05:35
The rules that ban a self-reported gain field and the cross-domain strategy
hints all declare ``advise`` as their failure verdict, because a reject on a
format or strategy hint discards the round's whole proposal set. That
declaration reached the Critic as prose only, so a model that rejected anyway
lost the round exactly as before and nothing detected it: the reason code it
cites had no production consumer at all.

Enforce the declaration in the verdict path. ``advisory_only_reason_codes``
derives the code set from the descriptors the Critic is handed -- not a second
list -- so a rule that changes its verdict cannot leave a stale entry behind,
and a reject citing one of those codes is held to ``advise``, logged, and
recorded as an observation so the prompt drift stays visible.

The hold runs per ``verdict_map`` entry before the collapse, since the collapse
ranks reject above advise: one variant rejected on an advisory-only rule would
otherwise still discard its siblings' advice. ``advise`` is already permissive
for the integrate_patch gate, so the downgrade lands the proposal exactly where
an obedient Critic would have.

Co-authored-by: Cursor <cursoragent@cursor.com>
@zoroyihan7

Copy link
Copy Markdown
Contributor Author

Thanks — both changes are in, as be260e19, plus a sync with main now that #1162 has landed.

1. The remedy is no longer prose-only

Took this in this PR rather than as a follow-up: the cost of the failure is a
round's novel ideas, and the constant having zero production consumers was the
same defect class the PR is about.

Generalised it one step beyond QUANTITATIVE_CLAIM_REASON_CODE, because the
three CROSS_DOMAIN_RULES declare advise for the same reason and were equally
unenforced. advisory_only_reason_codes() derives the code set from the
descriptors the Critic is actually handed, so a rule that changes its
failure_verdict cannot leave a stale entry behind, and a typo in one rule's
verdict literal cannot silently drop it (hence ADVISE_VERDICT spelled once and
read by the table).

Two details worth flagging:

  • The hold runs per verdict_map entry, before the collapse. With it applied
    only to the collapsed verdict, a single variant rejected on an advisory-only
    rule would still out-rank its siblings' advise and discard the set — the
    ranking you quote in point 2. So the entry-level hold is what makes the
    downgrade actually reach the multi-variant case.
  • advise is already in INTEGRATE_PATCH_PERMISSIVE_VERDICTS, so a
    downgraded verdict lands a specialist patch exactly where an obedient Critic
    would have, rather than into a state the gate has no rule for.

The downgrade is logged and recorded as a verdict_downgraded_to_rule_verdict
observation. Silently correcting the model would have hidden exactly the drift
that cost the run, so it is detectable now — which was the substance of your
objection.

I left critic.md alone deliberately: telling the model a backstop exists
invites it to lean on the backstop.

2. ClosesRefs

Agreed, and retitled to Refs #1143 with the reason stated in the description:
half (b) (approved_variant_names on the live path, and the collapse ranking
reject above advise) is untouched, so a multi-variant proposal with one
genuinely rejected variant still loses the set. The entry-level hold above
narrows that for this one class only; the general problem outlives this PR and
should keep the issue open. Your reasoning for the retitle over doing (b) here is
also mine — (b) needs the _materialize_approved_proposal filter wired to the
live path and deserves its own review, not a rider on a prompt fix.

3. Nit

Correct, and left as-is. The production call site audits from the scan's list
because the scan runs first and sees the pre-strip payload; the return value is
what lets the tests assert which fields the strip removed without re-deriving
it. Changing the call site to consume it would make the audit depend on strip
order for no gain.

New tests: nine, split between the code-set derivation
(test_specialist_patch_safety_unit.py) and the verdict path
(test_critic_verdict_map.py) — including that a reject with no code, or a code
no rule declares as advisory, still rejects.

Comment thread src/hyperloom/inference_optimizer/tests/test_critic_verdict_map.py Fixed
Comment thread src/hyperloom/inference_optimizer/tests/test_critic_verdict_map.py Fixed
The test asserts on the log line and the observation, not on the proposal, so
binding the seeded proposal left an unread local.

Co-authored-by: Cursor <cursoragent@cursor.com>
The downgrade keyed on an entry-level ``failure_reason_code``. That field
is an input descriptor: the Critic prompt hands the code to the model, but
nothing on the output side validates or requires it back. A field run's
verdict carried the code inside ``reasoning`` instead --

    {"verdict": "reject",
     "reasoning": "specialist_quantitative_claim_violation: the proposal
                   payload carries the forbidden predicted_gain_pct field.",
     "packet_evidence": ["payload.predicted_gain_pct"]}

-- so the reject stood and cost the round its FRAMEWORK_AGENT phase entry,
which is the case this branch exists to fix.

Recovering the citation from the verdict's own prose is therefore part of
reading the verdict, not a workaround. Reason codes are snake_case
identifiers, so the scan is word-bounded and cannot fire on a substring of
a longer token; an explicit ``failure_reason_code`` still wins, so prose
cannot soften a rule the Critic declared at ``reject``.

Co-authored-by: Cursor <cursoragent@cursor.com>
@zoroyihan7

Copy link
Copy Markdown
Contributor Author

Pushed ea3eab2e1, which closes a gap the field data on #1146 exposed in be260e193.

The downgrade keyed on an entry-level failure_reason_code. That field is an input descriptor — prompts/critic.md and specialists/patch_safety.py hand the code to the model, but nothing on the output side validates or requires it back. The verdict that motivated this branch does not carry it:

{
  "verdict": "reject",
  "reasoning": "specialist_quantitative_claim_violation: the proposal payload carries the forbidden predicted_gain_pct field.",
  "packet_evidence": ["payload.predicted_gain_pct"],
  "notes": ["Resubmit without predicted_gain_pct or other prohibited quantitative ranking fields."]
}

The code is in reasoning. So replaying the exact payload this PR exists to fix would have left the reject standing, and the FRAMEWORK_AGENT phase entry would still have been lost.

The commit adds cited_advisory_reason_code(): the explicit field first, then a word-bounded scan of the verdict's own prose (reasoning, notes). Two properties worth checking in review:

  • An explicit code still wins. A verdict declaring failure_reason_code: "specialist_patch_not_grounded" while mentioning an advisory code in passing stays a reject — prose cannot soften a rule the Critic cited at reject.
  • The scan is word-bounded. x_specialist_quantitative_claim_violation_v2 in a log key is not a citation.

Tests: the field payload's exact shape, an ordinary reject whose prose happens to mention predicted_gain_pct without citing a rule, the explicit-code-outranks-prose case, and the substring guard.

The pre-existing no_reason_code parametrisation of test_a_substantive_reject_still_rejects still passes unchanged, which is the property that matters most here: a reject citing nothing at all is still a reject.

@zoroyihan7

Copy link
Copy Markdown
Contributor Author

/retest

The e2e run for ea3eab2e1 was cancelled 16s after it started, by the comment posted alongside the push. Same defect as the earlier cancellation on this PR: ci-e2e answers issue_comment, its concurrency group is keyed on the PR number with cancel-in-progress, and GitHub resolves the group when the run is created — before resolve.if can decline the work. #1173 fixes it; until that lands, a comment on a PR kills that PR's in-flight run.

Verified ea3eab2e1 locally against the field payload: it downgrades to advise citing specialist_quantitative_claim_violation, x_specialist_quantitative_claim_violation_v2 in prose does not fire, an explicit non-advisory failure_reason_code keeps the reject even with an advisory code in the prose, and a plain reject is untouched. 82 tests green across the two suites; ruff clean.

@chaojhou

Copy link
Copy Markdown
Collaborator

Following up on my earlier review — the deterministic downgrade I asked for is here and built the right way. advisory_only_reason_codes() derives the set from the descriptors the Critic is handed rather than restating it (patch_safety.py:279), so a rule that changes its failure_verdict cannot leave a stale entry behind; the hold runs per verdict_map entry before the collapse (intent_router.py:216), so an advisory reject cannot out-rank its siblings; and the move is recorded as an observation instead of being silently corrected. test_a_rule_asking_for_a_reject_is_left_alone pins the derivation. That part I have no further comments on.

One blocking item, in the commit added after that review.

The prose scan downgrades on a mention, not on a citation

ea3eab2e1 extends the lookup to the verdict's own prose. Its premise is that failure_reason_code is an input descriptor that nothing on the output side requires back — which is correct, and which also means the prose scan is not a fallback: it is the primary path for most real verdicts.

    text = "\n".join(prose)
    # Sorted so a verdict citing several advisory rules still records one
    # deterministic code; every candidate is advisory, so the choice is only
    # about which one the audit trail names.
    cited = sorted(code for code in advisory if re.search(rf"(?<![\w-]){re.escape(code)}(?![\w-])", text))
    return cited[0] if cited else ""

The scan requires only that the code appear as a whole word somewhere in reasoning or notes. It does not require the code to be the reason for the verdict. So a Critic that rejects for an unrelated, genuine problem and merely names the rule in passing gets its reject downgraded:

Checked specialist_quantitative_claim_violation: clean. Rejecting because the patch rewrites a kernel with no before/after benchmark to stand on.

That has no failure_reason_code (the observed shape), contains the code word-bounded, and so returns advise. By the PR's own tests a downgrade means the proposal is materialised — test_a_rule_named_only_in_prose_still_holds_the_verdict asserts len(coord._materialise_calls) == 1 where the reject case asserts []. A proposal the Critic meant to block executes.

notes widens this further: they are remediation text, the place a model is most likely to write "this is not a <code> problem". The observed shape needs only reasoning.

The negative test aims at the adjacent case rather than this one. test_prose_that_cites_no_rule_leaves_the_reject_alone uses prose mentioning the field name predicted_gain_pct, not the reason code, so it passes without exercising the risk.

Suggested shape. Anchor to the citation form the field run actually produced — from your own commit message, "reasoning": "specialist_quantitative_claim_violation: the proposal payload carries the forbidden predicted_gain_pct field." — i.e. require ^\s*<code>\s*: rather than a free-floating match, and drop notes from _VERDICT_PROSE_KEYS. That still passes test_a_rule_named_only_in_prose_still_holds_the_verdict unchanged, since its fixture puts the code first with a colon, and it removes the whole mentioned-in-passing class. A test whose prose cites the code while rejecting for another reason is the one to add.

The precedence design already in place is the right instinct and worth keeping: an explicit non-advisory failure_reason_code blocks the prose scan entirely (coordinator_helpers.py:572-574, pinned by test_a_declared_reject_code_outranks_an_advisory_one_in_prose), so any Critic that does emit the field is unaffected either way.


One non-blocking note, since it touches the same seam. The downgrade is applied in the live verdict path, but the bus event keeps the raw payload, and the report renders straight from it:

        summary = f"verdict={payload.get('verdict')} reason={(payload.get('reasoning') or '')[:60]}"

So a downgraded reject still shows as verdict=reject in the final report's highlights while the proposal in fact ran, and reconstructing what happened means joining that against the separate observation record. That is the same class of mismatch #1178 is cleaning up, so it may be worth landing there rather than here.

zoroyihan7 and others added 6 commits August 13, 2026 10:18
The Critic prompt hands every review rule a ``failure_reason_code`` and asks
for it back, but the commit path dropped it: ``_commit_coordinator_inbox``
never passed it to ``build_review_verdict_intent`` and the payload had no slot
for it. So the field the Coordinator reads to tell "rejected on a formatting
rule that asked for advice" apart from "rejected on the merits" could never
arrive, leaving prose-scanning as the only signal in production.

Plumb it end to end and say so in the output schema the Critic is handed, so
the structured path is the one that carries the citation and the prose scan is
a fallback rather than the sole mechanism.

Co-authored-by: Cursor <cursoragent@cursor.com>
… reject

The hold moved any reject citing an advisory-only rule, so a verdict refusing
for two reasons -- "the proposal claims a 12% gain AND the patch has no
rollback plan" -- lost the second one and the proposal went through on a
formatting technicality.

Confine it to a reject that names one ground and requests no further evidence.
Severity is deliberately not the discriminator: risk_rules.md reserves
`blocker` for evidence and correctness failures and lists no format item, yet
the verdict this hold was built for graded its own format complaint `blocker`,
so reading severity would retire the hold on the very case it exists for.

Co-authored-by: Cursor <cursoragent@cursor.com>
`advise` is one of the two verdicts that let `integrate_patch` run, and the
hold mirrors whatever it produces onto `specialist_patch_verdicts`. So a
Critic reject held to a formatting rule came out the other side as the
Critic's approval to land the patch -- the one consequence of a verdict that
cannot be undone, granted by a mechanism built to stop a round's ideas being
thrown away over a field name.

Mirror the verdict the Critic wrote instead. A held proposal is still
materialised, so the round keeps its ideas; a patch that deserves to land
gets there through a fresh Critic verdict, which overwrites this one.

Co-authored-by: Cursor <cursoragent@cursor.com>
…'s field name

`expected_gain_pct` was stripped from a specialist's proposal_set but
`predicted_gain_pct` -- the same claim, spelled the way a propose_action
intent spells it -- went through untouched and reached the Critic, which is
one of the ways a round ended up rejected over a field nobody enforced.

The ban stays source-aware, because it is applied rather than declared to be:
strip_forbidden_proposal_fields runs on the specialist exit payload alone, so
the Coordinator's own required propose_action field is unaffected.

Co-authored-by: Cursor <cursoragent@cursor.com>
The prose scan fired on any word-bounded occurrence of an advisory code, and
since nothing on the output side requires `failure_reason_code`, prose is the
primary path rather than a fallback. So a Critic that checked the advisory
rule, found it clean, and refused for a real reason --

    Checked specialist_quantitative_claim_violation: clean. Rejecting because
    the patch rewrites a kernel with no before/after benchmark to stand on.

-- had the rule it *cleared* read as its grounds, and the proposal it meant to
block was materialised.

Require the citation form the field verdict actually used: the code opens the
clause and a colon introduces the finding. `notes` leaves the scan with it --
remediation text is where "this is not a <code> problem" gets written, and the
observed shape only ever needed `reasoning`.

Co-authored-by: Cursor <cursoragent@cursor.com>
@zoroyihan7

Copy link
Copy Markdown
Contributor Author

Both items handled at 4fb6b6cf3. The blocking one was right, and the non-blocking one turned out not to be a defect — evidence below, since it is the kind of thing worth being sure about rather than fixing twice.

The prose scan now reads a citation, not a mention

Fixed exactly as suggested, in 4fb6b6cf3. The match is anchored to the citation form the field run produced:

_CITATION_OPENER = r"[\s>*\-–—`\"'(\[]*"
...
if re.search(rf"^{_CITATION_OPENER}{re.escape(code)}`?\s*:", text, re.MULTILINE):

notes is out of _VERDICT_PROSE_KEYS; only reasoning is scanned. The opener tolerates the wrappers a model actually emits around the code — leading whitespace, a bullet, a blockquote marker, backticks — while still requiring the code to open its own clause and be followed by a colon. MULTILINE is there because the citation is often on the second line after a preamble.

Your example is now the test. test_the_rule_the_critic_cleared_is_not_the_grounds_for_its_reject uses your exact prose — clearing specialist_quantitative_claim_violation and rejecting over a missing benchmark — and asserts the reject stands and nothing is materialised. It fails against the previous commit. test_a_rule_named_only_in_prose_still_holds_the_verdict passes unchanged, as you predicted.

The weak negative test you flagged is replaced rather than kept: it asserted on prose naming the field predicted_gain_pct, which never exercised the risk.

Three more that came out of working through it

  • bd77bf225failure_reason_code never left the Critic runtime. build_review_verdict_intent() dropped it, so on the live path the declared-code branch you pinned with test_a_declared_reject_code_outranks_an_advisory_one_in_prose was unreachable and every verdict fell through to the prose scan. The precedence design was correct; it just had no input. Now threaded into both the intent payload and the session-memory decision record.
  • 109589533 — the hold now requires the cited rule to be the whole reject. A reject resting on several grounds, or one still asking for evidence, is no longer downgraded on the strength of one advisory rule among them.
  • 56cffd512a held reject was becoming a patch-landing permit. The downgraded advise was mirrored into specialist_patch_verdicts, where advise is an approval vector for integrate_patch. So holding a reject to a formatting rule did not just save the round's ideas, it let the patch the Critic refused land on the next tick. What is mirrored is now the authored verdict, never what the hold made of it; a held proposal that deserves to land gets there through a fresh verdict.

The report already renders the effective verdict

I checked report.py:1160 against the bus and it reads the held verdict, not the raw one. _handle_review_verdict applies the hold at intent_router.py:216, before _handle_single_verdict is called; the only review_verdict message on the bus is the rebroadcast that method builds, and it is constructed from the post-hold verdict variable. The Critic's raw intent is never appended. So there is no verdict=reject in the highlights for a proposal that ran, and nothing for #1178 to clean up here.

I also confirmed pending.verdict, which the same line assigns the held value to, has no reader anywhere in orchestrator/ — so the hold cannot leak into a permit by that route either.

zoroyihan7 and others added 3 commits August 13, 2026 16:47
`advise` means "dispatch may proceed", and the hold produced it for any
proposal whose reject cited an advisory-only rule. So a Critic that refused an
`integrate_patch` and named one of those rules had the patch applied for it:
`_handle_single_verdict` materialises on the held verdict, and the propose-time
`PolicyGate` patch gate does not run again on the verdict. The earlier mirror
fix only kept the downgrade out of `specialist_patch_verdicts`, which gates the
*next* proposal, not the one under review.

All four advisory codes belong to rules about a specialist-authored proposal
payload -- `proposal_set[*]` and `scope=domains` -- so scope the hold to the
proposal kinds those rules are written about. That is what makes the landing
case unreachable rather than a case handled after the fact.

The eligible kinds are named rather than derived: no ACTION_CATALOGUE field
separates them from `integrate_patch`, which shares their `exploration`
verdict class, `shallow` family and `workspace_write` side effect while being
the one action whose materialisation lands the patch under review. Deriving
the set from any of those would have re-admitted exactly the case this fixes.

`framework_agent` stays eligible: `critic.md` names it inside the
quantitative-claim rule ("never fire the rule on them", since its payload
always carries a hard-coded `predicted_gain_pct`), and the field verdict this
branch exists to fix is a reject of one on precisely that ground. Excluding it
would retire the hold on its only observed instance.

The rule descriptor now travels the same way: a bundle with no proposal the
rule can apply to is no longer handed it, so the Critic has one less code it
can cite where it is irrelevant.

Co-authored-by: Cursor <cursoragent@cursor.com>
…mbles one

The prose scan allowed a list marker, a quote marker and a dash before the
code, and matched any line of the reasoning. Those are precisely the markers a
model uses to enumerate or quote the rules it checked, so

    - specialist_quantitative_claim_violation: clean.
    - rollback: absent. Rejecting on the rollback.

read as a citation of the rule the verdict had just *cleared*, and the proposal
the Critic refused was dispatched. A quoted rule and a fenced example did the
same.

Narrow it to an unambiguous citation: the code opens the verdict's own prose,
optionally backticked, and a colon introduces the finding -- the shape the
field verdict used. The two errors are not symmetric. Failing to downgrade
costs the round proposals it can re-author; downgrading wrongly executes one
the Critic meant to block. So the scan does not try to learn every legitimate
citation format, which is the arms race that produced this bug; the explicit
`failure_reason_code` field is the path that is supposed to carry the citation.

Co-authored-by: Cursor <cursoragent@cursor.com>
The prose fallback read `reasoning`, but a per-variant entry is
`{verdict, rationale?}` -- the shape PolicyGate's own hint documents and every
fixture uses. So on the multi-variant path, which is the reason the hold runs
per entry at all, a variant citing its rule in prose was never read and the
reject stood.

Read both keys: `reasoning` for a single verdict, `rationale` for a map entry.

Co-authored-by: Cursor <cursoragent@cursor.com>
zoroyihan7 and others added 13 commits August 13, 2026 16:58
`failure_reason_code` is the field the verdict path reads to tell a reject
resting on an advisory-only rule apart from a substantive one, and it is
plumbed end to end -- but the schema the model is actually given,
`_REVIEW_OUTPUT_INSTRUCTIONS`, enumerates every other key of
`review_verdicts[]` while instructing a reply matching *exactly* that schema.
The field existed only in `references/verdict_schema.md`, which nothing loads,
and in prose in critic.md. So the explicit path was never populated in
production and the prose scan carried the whole mechanism.

List the key in the schema and say what fills it. The per-variant path had the
same gap: the entry shape is spelled out only in PolicyGate's hint, as
`{verdict, rationale?}`, so a variant had nowhere to name its rule either.

Co-authored-by: Cursor <cursoragent@cursor.com>
Three gaps, each one a place where the code could be wrong and stay green:

* The `verdict_map` mirror had no assertion at all -- replacing the authored
  collapse with the acted-on verdict left the suite passing, because every
  fixture used an `explore` proposal, which never reaches the mirror. Review a
  `specialist` proposal per variant, where the mirror is a landing permit for
  its patches.
* `collapse_verdicts` fell back to `needs_review` untested, on both an empty
  map and one carrying only `redirect` -- a legal verdict with no place in the
  collapse order. Its priority order is now pinned directly too, rather than
  only through the routing tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
The slot read "<the review_constraints rule this verdict rests on>", which a
model can satisfy with the rule's `rule_id`. The verdict path matches on
`failure_reason_code` values, so say so in the slot itself rather than only in
the rule beneath it.

Co-authored-by: Cursor <cursoragent@cursor.com>
`test_a_declared_reject_code_outranks_an_advisory_one_in_prose` names the
early return that stops a declared, non-advisory `failure_reason_code` from
falling through to the prose scan, but its `reasoning` fixture -- "unrelated
aside about <code>" -- is one the strict `_CITATION_OPENER` rejects on its own,
because the code neither opens the line nor meets a colon. So the test passed
either way: reverting the early return to a form that falls through left all 71
tests in the file green.

Put the code in citation form, which is the only shape that reaches the
conflict the test is about. The declared reject code is now the only thing
holding the verdict, and reverting the early return fails it on
`assert pending.verdict == "reject"`.

Co-authored-by: Cursor <cursoragent@cursor.com>
`test_a_reason_code_inside_a_longer_token_is_not_a_citation` said the prose
scan is word-bounded. It is not: `_CITATION_OPENER` anchors the code at the
start of the line and the pattern requires a colon adjacent to it. The property
held, but for a reason the test did not describe, and its fixture -- "see log
key x_<code>_v2 for the trace", which carries no colon at all -- was outside
the reach of any single-line change to the scanner: it took a loose opener
*and* dropping the colon together to make it fail.

Kept rather than deleted -- the parametrized enumeration cases cover a citation
shape a model writes, not an identifier that merely contains a code -- but
renamed to the real mechanism and split into one fixture per half, each of
which a single mutation now kills: a loose opener fails `x_<code>: ...`, and
dropping the colon requirement fails `<code>_v2: ...`.

Co-authored-by: Cursor <cursoragent@cursor.com>
`strip_forbidden_proposal_fields` returns the names it removed and the runner
threw them away, building the `patch_safety_forbidden_fields` note from
`scan_quantitative_claims` instead. The two lists agree today -- the scan
covers `proposal_set` entries as well as the top level -- so this was a
duplicate computation whose halves could drift apart silently.

Use the strip's own return for the note and drop the scan's copy of it, which
ties the audit to what was actually taken out and leaves the scan responsible
only for the numeric warnings it alone produces.

Co-authored-by: Cursor <cursoragent@cursor.com>
…dict

The hold's two safeguards read ``required_evidence``, ``risks`` and
``failure_reason_code`` off the entry they are handed. A per-variant entry is
``{verdict, rationale?, failure_reason_code?}`` -- the shape PolicyGate
documents -- so the first two were structurally absent on the batch path and
"the reject rests on one ground" could never fire, while a payload-level
declared code was invisible to declared-code precedence. A variant reject
listing two blocker risks and asking for a matched benchmark and a rollback
plan was downgraded to ``advise`` and dispatched, held to a rule its rationale
merely opened with.

Read each entry together with the grounds its payload states, the entry's own
statement of a ground winning. That is where the schema puts them and it is
already how the same verdict is serialised downstream, so the hold and
``serialize_verdict_advisory`` now agree on what the verdict's grounds are.

Prose is deliberately not inherited: grounds stated once bind every variant,
but a citation is a claim about the verdict making it, and reading the batch's
prose as one variant's grounds would downgrade a reject whose own rationale
refuses on something else.

Rejected: dropping the downgrade from the batch path, which would retire the
case the hold was built for -- one advisory-only variant reject sinking a whole
explore grid; and threading a second ``payload`` argument through
``verdict_held_to_its_rule``, which spreads "entry first, payload second" over
two call sites instead of naming it once.

Co-authored-by: Cursor <cursoragent@cursor.com>
``reasoning`` was tried first and any non-blank line ended the search, so an
entry that fills both keys -- ``reasoning: "See the per-variant notes."`` beside
a ``rationale`` opening with the advisory code -- had its citation go unread and
its reject stand. The two keys are one speaker's grounds for one verdict and
nothing ranks them, so whichever came first decided whether the other was seen.

Read the opening line of each key the entry fills. Only the reach widens: the
citation test itself is unchanged, and the per-field stop on the first non-blank
line is what keeps a fenced or after-the-fact mention out of the grounds.

Rejected: leaving it and documenting the short-circuit at the call site. It
fails safe, but the safe direction here costs the round every proposal in the
set, and the order of two fields is not a reason to spend them.

Co-authored-by: Cursor <cursoragent@cursor.com>
``x_<code>: ...`` was claimed to be what fails a loose opener, but a loose
opener still refuses it: ``x`` is in no widening of the character class, so
``re.match`` fails at position 0 either way. The only mutation it caught was
``re.match`` -> ``re.search``, which three other cases already catch, and the
end of the anchor it was there for -- nothing but whitespace or a backtick may
precede the code -- is covered by the list and quote markers a model actually
writes.

In its place, a non-breaking space between the code and the colon. That is the
one behavioural difference between ``[ \t]*:`` and ``\s*:``, the row of the
matrix no test reached: a line has already been through ``splitlines``, so
exotic unicode spaces are all a wider class would add. The gap stays ASCII, and
the case now says so.

Co-authored-by: Cursor <cursoragent@cursor.com>
``scan_quantitative_claims`` computed the same ``keys & FORBIDDEN_*``
intersection ``strip_forbidden_proposal_fields`` does, at both the payload and
the proposal level, and returned it to a runner that discarded it: one question,
two implementations, and nothing keeping them in step past the point where one
of them stopped being read.

Narrow it to the numeric prose warnings it alone produces, and name it for
them. The field list now has a single reader, so the ``confidence`` asymmetry
between the payload and proposal sets cannot drift.

Brute-forced the removal first: over every subset of size <= 2 of
``FORBIDDEN_PROPOSAL_FIELDS | {summary, other}`` at the top level crossed with
the same per proposal, with a nested ``proposal_set`` and the non-list shapes,
the intersection removed and the one that stays agreed on all 3136 cases and
raised the same ``TypeError`` on a scalar ``proposal_set``, so the audit note is
unchanged.

Rejected: extracting the intersection into a shared ``_forbidden_fields``
helper, which would have left the dead second return in place -- the question
only has one caller, so it wants one function, not two agreeing ones.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ten it

Co-authored-by: Cursor <cursoragent@cursor.com>
…ng none

``verdict_map`` is in no version of the Critic's output schema, so nothing asks
the Critic to spell a variant's key inside the ``risks`` and ``required_evidence``
a batch states once for the whole set. Reading the prose for a name answered that
by guess, and the guess ran the wrong way: a blocker written for the set that
names one variant as its example, or a finding whose remediation field points at
how a sibling was fixed, was struck off this variant's grounds. The reject then
rested on one ground, its rationale's advisory citation supplied the rule, and
the set was dispatched -- including variants the Critic never cleared.

Attribution says what a finding is about, never what it is not. Every finding the
payload states now binds every entry, and an entry filing findings under its own
key adds to them rather than answering for them. The two errors do not cost the
same: binding a finding that was about a sibling withholds a downgrade and leaves
the reject the Critic wrote, which is the pre-feature outcome, while dropping one
that did bind dispatches a proposal it refused.

``risks`` stated in some shape other than the schema's list has stated grounds
whose number cannot be read off -- "the patch does not apply and there is no
rollback plan" is two -- so it counts as more than one rather than as the single
ground the hold is confined to. That is the reading the serialiser's wrapping
had quietly changed; the grounds path no longer goes through it, since its job
is rendering a verdict and this one is counting it.

Known limitation: a batch stating two risks or any required evidence now holds
every reject in the set, including one resting on nothing but an advisory rule.
That entry keeps its reject and the set collapses to it. Recovering the downgrade
needs attribution the Critic is asked for -- the batch shape documented in the
output schema -- which is a prompt change this does not make.

Rejected: attributing positively off a leading ``"<name>:"``, which is the same
prose guess with a narrower failure and still rests on a convention nothing
states; and inheriting only codes a handed rule declares, which inherits nothing
at all, since every rule in ``review_constraints`` declares ``advise`` -- a batch
declaring a hard code would have its variants downgraded on the advisory rules
their rationales cite.

Co-authored-by: Cursor <cursoragent@cursor.com>
… none

``verdict_rests_on_one_ground`` allows a reject one stated risk beside its
citation. On the single path that is one statement by one author -- "the
proposal claims a 12% gain and has no rollback plan" names the rule and the
risk in the same breath -- so the risk is the cited rule's. A batch states its
risks once for the whole set while the citation belongs to the entry, and
nothing connects them, so the ``<= 1`` threshold was itself an attribution in
the direction that dispatches: a payload stating one blocker, "no variant in
this set supplies a rollback plan", with every entry citing the format rule,
downgraded both rejects and ran the whole grid. State the blocker twice and it
correctly held.

A finding the batch states now holds every reject in the set whatever its
count, and only an entry's own grounds can support a downgrade. That is the
rule the batch path already had -- what the batch states adds to the grounds a
variant is held on, never supplies the grounds it is softened on -- with
arithmetic that claimed more than it could tell taken out of it. Since
``required_evidence`` already withheld the downgrade on one item, counting a
risk the same way makes the two readings identical, so the findings are no
longer copied onto the entry at all: they are read where they are stated, which
is one veto instead of a merge feeding a count.

Restated limitation, measured against the shapes the repo teaches rather than
the shapes the suite happens to hold: the batch downgrade now needs a payload
that states no findings at all. That is the batch shape the runtime teaches --
``{target_proposal_msg_id, verdict_map: {name: {verdict, rationale?,
failure_reason_code?}}}``, the only batch payload spelled out anywhere in
``src`` (PolicyGate's repair hint) and all ``references/intent_envelope.md``
asks for. It is not the reject shape ``references/verdict_schema.md``
documents: that states one risk *and* one required-evidence item, as both
reject exemplars in ``critic/tests/expected_outputs.json`` do, so a batch
written in the single-verdict style keeps every reject it wrote, including one
resting on nothing but an advisory rule.

``test_a_variant_resting_only_on_the_cited_rule_still_gives_up_its_reject``
gives up its payload risk, deliberately. Its justification was that a single
risk restating the cited rule's complaint is what a verdict resting on one
advisory ground looks like -- true of that risk's prose, which nothing reads,
and false of "no variant supplies a rollback plan" stated in the same slot. The
half that generalises stays: notes and evidence pointers are not findings, and
a variant beside them still gives up its reject.

Also pinned, both previously unclaimed and unpinned: the single path moved when
the ground count stopped going through the serialiser. ``risks: 1`` and
``required_evidence: 1`` used to raise ``TypeError`` into the router's
catch-all, which recorded ``handle_intent_exception`` and dropped the intent,
leaving the proposal undecided for the rest of the session; the verdict the
Critic wrote is now what the proposal is decided on. And an empty list slot
states nothing -- one risk beside an empty one is one ground, an empty evidence
slot is not a request -- which is the reading ``serialize_verdict_advisory``
already takes of the same fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
AMD-melliott pushed a commit to AMD-melliott/Hyperloom that referenced this pull request Aug 14, 2026
``ci-e2e`` answers ``issue_comment`` so ``/retest`` can re-run it without a new
commit, and its concurrency group is keyed on the PR number with
``cancel-in-progress``. GitHub resolves ``concurrency`` when a run is *created*,
before the ``resolve`` job's ``if`` can decline the work, so **any** comment on a
PR joined that PR's group, cancelled the in-flight run, and only then skipped
itself. A review comment therefore threw away a multi-hour single-GPU run: PR
AMD-AGI#1161's e2e died 17s after a reply, 49s into the run, and the four skipped
``issue_comment`` runs in recent history are the same event.

Key a comment that cannot start a run to a group of its own, so it cancels
nothing. The ``/retest`` predicate is spelled exactly as in ``resolve.if``:
comments that can start a run are precisely the comments that can cancel one, and
that equality is what the fix rests on, so a test pins it in both directions --
a group looking for a command the gate dropped lets an ordinary comment cancel
again, and a gate honouring a command the group misses stops a real retest from
preempting the run it replaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread src/hyperloom/orchestrator/loop/coordinator_helpers.py Fixed
Comment thread src/hyperloom/orchestrator/loop/coordinator_helpers.py Fixed
The helper already imports tokenize_server_args_preserving_json inside
_dedupe_extra_server_args to avoid a cyclic import. The module-level
copy was unused and tripped CodeQL.

Co-authored-by: Cursor <cursoragent@cursor.com>

@ZhengGong-amd ZhengGong-amd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved

@ZhengGong-amd
ZhengGong-amd merged commit be8b762 into main Aug 17, 2026
28 checks passed
@ZhengGong-amd
ZhengGong-amd deleted the fix/1143-proposal-schema branch August 17, 2026 03:26
xiaofei-zheng pushed a commit that referenced this pull request Aug 25, 2026
``ci-e2e`` answers ``issue_comment`` so ``/retest`` can re-run it without a new
commit, and its concurrency group is keyed on the PR number with
``cancel-in-progress``. GitHub resolves ``concurrency`` when a run is *created*,
before the ``resolve`` job's ``if`` can decline the work, so **any** comment on a
PR joined that PR's group, cancelled the in-flight run, and only then skipped
itself. A review comment therefore threw away a multi-hour single-GPU run: PR
#1161's e2e died 17s after a reply, 49s into the run, and the four skipped
``issue_comment`` runs in recent history are the same event.

Key a comment that cannot start a run to a group of its own, so it cancels
nothing. The ``/retest`` predicate is spelled exactly as in ``resolve.if``:
comments that can start a run are precisely the comments that can cancel one, and
that equality is what the fix rests on, so a test pins it in both directions --
a group looking for a command the gate dropped lets an ordinary comment cancel
again, and a gate honouring a command the group misses stops a real retest from
preempting the run it replaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
xiaofei-zheng pushed a commit that referenced this pull request Aug 25, 2026
… ship broken (#1161)

* fix(specialist): stop shipping a proposal schema that fails its own guard

The Critic is instructed to reject a whole ``proposal_set`` that carries a
self-reported gain field, and the runner detected those fields, wrote an audit
note, and forwarded them anyway. So a format slip cost the round every idea the
specialist produced, with no budget left to resubmit -- the run's only novel
optimizations never reached a benchmark.

Two sides of the same defect:

* The runner now strips the forbidden fields before writing
  ``specialist_done.json``. The claim is worthless either way (measured gain is
  the Coordinator's), so dropping it makes the verdict unreachable rather than
  merely audited. The audit note still records what was there.
* Cold start actively instructed the violation: it told the specialist to flag
  each fallback proposal ``confidence: low``, which is in
  FORBIDDEN_PROPOSAL_FIELDS. A compliant specialist therefore tripped the guard
  on exactly the round where it was the only source of ideas.

Also splits the guard's scope to match the rule the Critic is given. The ban is
on ``proposal_set[*]``, but the scan applied it to the payload top level too,
where ``confidence`` is the round-level self-assessment our own output schema
asks for and two audit writers record -- not a per-proposal gain claim, and
unable to bias which variant gets benched.

Refs #1143

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(critic): make a proposal format slip advisory instead of fatal

The quantitative-claim guard lived only as a hand-copied field list in the
Critic prompt, with nothing in the codebase producing, consuming or testing its
reason code. So the Critic generalised it: the verdict that lost a real run
named ``predicted_gain_pct``, which is not on the enforced list at all -- and is
a required field on the ``propose_action`` channel, taught by our own prompt
template. Nothing could satisfy both readings of the same field name.

Delivers the rule as data instead: ``review_constraints`` now carries a
descriptor generated from FORBIDDEN_PROPOSAL_FIELDS, so the Critic's list cannot
drift from the one the runner strips. Follows the mechanism the cross-domain
rules already use rather than templating the verbatim-loaded prompt.

The verdict for this class becomes ``advise``. Detection breadth is deliberately
kept -- a gain claim smuggled under an unlisted name should still be noticed --
but noticing it must not reject the set: the fields are stripped before review,
so anything arriving is a format problem, and rejecting on format costs the
round every proposal in it with no chance to resubmit.

Refs #1143

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): hold a reject to the verdict its own rule declared

The rules that ban a self-reported gain field and the cross-domain strategy
hints all declare ``advise`` as their failure verdict, because a reject on a
format or strategy hint discards the round's whole proposal set. That
declaration reached the Critic as prose only, so a model that rejected anyway
lost the round exactly as before and nothing detected it: the reason code it
cites had no production consumer at all.

Enforce the declaration in the verdict path. ``advisory_only_reason_codes``
derives the code set from the descriptors the Critic is handed -- not a second
list -- so a rule that changes its verdict cannot leave a stale entry behind,
and a reject citing one of those codes is held to ``advise``, logged, and
recorded as an observation so the prompt drift stays visible.

The hold runs per ``verdict_map`` entry before the collapse, since the collapse
ranks reject above advise: one variant rejected on an advisory-only rule would
otherwise still discard its siblings' advice. ``advise`` is already permissive
for the integrate_patch gate, so the downgrade lands the proposal exactly where
an obedient Critic would have.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(critic): drop the unused binding in the downgrade audit test

The test asserts on the log line and the observation, not on the proposal, so
binding the seeded proposal left an unread local.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): read the cited rule from the verdict's prose too

The downgrade keyed on an entry-level ``failure_reason_code``. That field
is an input descriptor: the Critic prompt hands the code to the model, but
nothing on the output side validates or requires it back. A field run's
verdict carried the code inside ``reasoning`` instead --

    {"verdict": "reject",
     "reasoning": "specialist_quantitative_claim_violation: the proposal
                   payload carries the forbidden predicted_gain_pct field.",
     "packet_evidence": ["payload.predicted_gain_pct"]}

-- so the reject stood and cost the round its FRAMEWORK_AGENT phase entry,
which is the case this branch exists to fix.

Recovering the citation from the verdict's own prose is therefore part of
reading the verdict, not a workaround. Reason codes are snake_case
identifiers, so the scan is word-bounded and cannot fire on a substring of
a longer token; an explicit ``failure_reason_code`` still wins, so prose
cannot soften a rule the Critic declared at ``reject``.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(critic): carry the cited rule out of the runtime, not just into it

The Critic prompt hands every review rule a ``failure_reason_code`` and asks
for it back, but the commit path dropped it: ``_commit_coordinator_inbox``
never passed it to ``build_review_verdict_intent`` and the payload had no slot
for it. So the field the Coordinator reads to tell "rejected on a formatting
rule that asked for advice" apart from "rejected on the merits" could never
arrive, leaving prose-scanning as the only signal in production.

Plumb it end to end and say so in the output schema the Critic is handed, so
the structured path is the one that carries the citation and the prose scan is
a fallback rather than the sole mechanism.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): hold a reject to its rule only when that rule is the whole reject

The hold moved any reject citing an advisory-only rule, so a verdict refusing
for two reasons -- "the proposal claims a 12% gain AND the patch has no
rollback plan" -- lost the second one and the proposal went through on a
formatting technicality.

Confine it to a reject that names one ground and requests no further evidence.
Severity is deliberately not the discriminator: risk_rules.md reserves
`blocker` for evidence and correctness failures and lists no format item, yet
the verdict this hold was built for graded its own format complaint `blocker`,
so reading severity would retire the hold on the very case it exists for.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): a held reject is not a patch-landing permit

`advise` is one of the two verdicts that let `integrate_patch` run, and the
hold mirrors whatever it produces onto `specialist_patch_verdicts`. So a
Critic reject held to a formatting rule came out the other side as the
Critic's approval to land the patch -- the one consequence of a verdict that
cannot be undone, granted by a mechanism built to stop a round's ideas being
thrown away over a field name.

Mirror the verdict the Critic wrote instead. A held proposal is still
materialised, so the round keeps its ideas; a patch that deserves to land
gets there through a fresh Critic verdict, which overwrites this one.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(specialists): strip the gain claim that hid under the Coordinator's field name

`expected_gain_pct` was stripped from a specialist's proposal_set but
`predicted_gain_pct` -- the same claim, spelled the way a propose_action
intent spells it -- went through untouched and reached the Critic, which is
one of the ways a round ended up rejected over a field nobody enforced.

The ban stays source-aware, because it is applied rather than declared to be:
strip_forbidden_proposal_fields runs on the specialist exit payload alone, so
the Coordinator's own required propose_action field is unaffected.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): read a rule citation, not a rule mention

The prose scan fired on any word-bounded occurrence of an advisory code, and
since nothing on the output side requires `failure_reason_code`, prose is the
primary path rather than a fallback. So a Critic that checked the advisory
rule, found it clean, and refused for a real reason --

    Checked specialist_quantitative_claim_violation: clean. Rejecting because
    the patch rewrites a kernel with no before/after benchmark to stand on.

-- had the rule it *cleared* read as its grounds, and the proposal it meant to
block was materialised.

Require the citation form the field verdict actually used: the code opens the
clause and a colon introduces the finding. `notes` leaves the scan with it --
remediation text is where "this is not a <code> problem" gets written, and the
observed shape only ever needed `reasoning`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): hold a reject only where the cited rule has jurisdiction

`advise` means "dispatch may proceed", and the hold produced it for any
proposal whose reject cited an advisory-only rule. So a Critic that refused an
`integrate_patch` and named one of those rules had the patch applied for it:
`_handle_single_verdict` materialises on the held verdict, and the propose-time
`PolicyGate` patch gate does not run again on the verdict. The earlier mirror
fix only kept the downgrade out of `specialist_patch_verdicts`, which gates the
*next* proposal, not the one under review.

All four advisory codes belong to rules about a specialist-authored proposal
payload -- `proposal_set[*]` and `scope=domains` -- so scope the hold to the
proposal kinds those rules are written about. That is what makes the landing
case unreachable rather than a case handled after the fact.

The eligible kinds are named rather than derived: no ACTION_CATALOGUE field
separates them from `integrate_patch`, which shares their `exploration`
verdict class, `shallow` family and `workspace_write` side effect while being
the one action whose materialisation lands the patch under review. Deriving
the set from any of those would have re-admitted exactly the case this fixes.

`framework_agent` stays eligible: `critic.md` names it inside the
quantitative-claim rule ("never fire the rule on them", since its payload
always carries a hard-coded `predicted_gain_pct`), and the field verdict this
branch exists to fix is a reject of one on precisely that ground. Excluding it
would retire the hold on its only observed instance.

The rule descriptor now travels the same way: a bundle with no proposal the
rule can apply to is no longer handed it, so the Critic has one less code it
can cite where it is irrelevant.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): read a citation that opens a verdict, not a line that resembles one

The prose scan allowed a list marker, a quote marker and a dash before the
code, and matched any line of the reasoning. Those are precisely the markers a
model uses to enumerate or quote the rules it checked, so

    - specialist_quantitative_claim_violation: clean.
    - rollback: absent. Rejecting on the rollback.

read as a citation of the rule the verdict had just *cleared*, and the proposal
the Critic refused was dispatched. A quoted rule and a fenced example did the
same.

Narrow it to an unambiguous citation: the code opens the verdict's own prose,
optionally backticked, and a colon introduces the finding -- the shape the
field verdict used. The two errors are not symmetric. Failing to downgrade
costs the round proposals it can re-author; downgrading wrongly executes one
the Critic meant to block. So the scan does not try to learn every legitimate
citation format, which is the arms race that produced this bug; the explicit
`failure_reason_code` field is the path that is supposed to carry the citation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): read the grounds a verdict_map entry actually states

The prose fallback read `reasoning`, but a per-variant entry is
`{verdict, rationale?}` -- the shape PolicyGate's own hint documents and every
fixture uses. So on the multi-variant path, which is the reason the hold runs
per entry at all, a variant citing its rule in prose was never read and the
reject stood.

Read both keys: `reasoning` for a single verdict, `rationale` for a map entry.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(critic): put the cited rule in the schema the Critic is handed

`failure_reason_code` is the field the verdict path reads to tell a reject
resting on an advisory-only rule apart from a substantive one, and it is
plumbed end to end -- but the schema the model is actually given,
`_REVIEW_OUTPUT_INSTRUCTIONS`, enumerates every other key of
`review_verdicts[]` while instructing a reply matching *exactly* that schema.
The field existed only in `references/verdict_schema.md`, which nothing loads,
and in prose in critic.md. So the explicit path was never populated in
production and the prose scan carried the whole mechanism.

List the key in the schema and say what fills it. The per-variant path had the
same gap: the entry shape is spelled out only in PolicyGate's hint, as
`{verdict, rationale?}`, so a variant had nowhere to name its rule either.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(loop): cover the verdict paths a mutation could delete unnoticed

Three gaps, each one a place where the code could be wrong and stay green:

* The `verdict_map` mirror had no assertion at all -- replacing the authored
  collapse with the acted-on verdict left the suite passing, because every
  fixture used an `explore` proposal, which never reaches the mirror. Review a
  `specialist` proposal per variant, where the mirror is a landing permit for
  its patches.
* `collapse_verdicts` fell back to `needs_review` untested, on both an empty
  map and one carrying only `redirect` -- a legal verdict with no place in the
  collapse order. Its priority order is now pinned directly too, rather than
  only through the routing tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(critic): name the field that fills the schema slot, not the rule

The slot read "<the review_constraints rule this verdict rests on>", which a
model can satisfy with the rule's `rule_id`. The verdict path matches on
`failure_reason_code` values, so say so in the slot itself rather than only in
the rule beneath it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(loop): cite a rule in the prose the declared code has to outrank

`test_a_declared_reject_code_outranks_an_advisory_one_in_prose` names the
early return that stops a declared, non-advisory `failure_reason_code` from
falling through to the prose scan, but its `reasoning` fixture -- "unrelated
aside about <code>" -- is one the strict `_CITATION_OPENER` rejects on its own,
because the code neither opens the line nor meets a colon. So the test passed
either way: reverting the early return to a form that falls through left all 71
tests in the file green.

Put the code in citation form, which is the only shape that reaches the
conflict the test is about. The declared reject code is now the only thing
holding the verdict, and reverting the early return fails it on
`assert pending.verdict == "reject"`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(loop): name the mechanism the citation scan actually uses

`test_a_reason_code_inside_a_longer_token_is_not_a_citation` said the prose
scan is word-bounded. It is not: `_CITATION_OPENER` anchors the code at the
start of the line and the pattern requires a colon adjacent to it. The property
held, but for a reason the test did not describe, and its fixture -- "see log
key x_<code>_v2 for the trace", which carries no colon at all -- was outside
the reach of any single-line change to the scanner: it took a loose opener
*and* dropping the colon together to make it fail.

Kept rather than deleted -- the parametrized enumeration cases cover a citation
shape a model writes, not an identifier that merely contains a code -- but
renamed to the real mechanism and split into one fixture per half, each of
which a single mutation now kills: a loose opener fails `x_<code>: ...`, and
dropping the colon requirement fails `<code>_v2: ...`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(specialists): audit the fields the strip removed, not a second scan

`strip_forbidden_proposal_fields` returns the names it removed and the runner
threw them away, building the `patch_safety_forbidden_fields` note from
`scan_quantitative_claims` instead. The two lists agree today -- the scan
covers `proposal_set` entries as well as the top level -- so this was a
duplicate computation whose halves could drift apart silently.

Use the strip's own return for the note and drop the scan's copy of it, which
ties the audit to what was actually taken out and leaves the scan responsible
only for the numeric warnings it alone produces.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): read the grounds a verdict_map entry inherits from its verdict

The hold's two safeguards read ``required_evidence``, ``risks`` and
``failure_reason_code`` off the entry they are handed. A per-variant entry is
``{verdict, rationale?, failure_reason_code?}`` -- the shape PolicyGate
documents -- so the first two were structurally absent on the batch path and
"the reject rests on one ground" could never fire, while a payload-level
declared code was invisible to declared-code precedence. A variant reject
listing two blocker risks and asking for a matched benchmark and a rollback
plan was downgraded to ``advise`` and dispatched, held to a rule its rationale
merely opened with.

Read each entry together with the grounds its payload states, the entry's own
statement of a ground winning. That is where the schema puts them and it is
already how the same verdict is serialised downstream, so the hold and
``serialize_verdict_advisory`` now agree on what the verdict's grounds are.

Prose is deliberately not inherited: grounds stated once bind every variant,
but a citation is a claim about the verdict making it, and reading the batch's
prose as one variant's grounds would downgrade a reject whose own rationale
refuses on something else.

Rejected: dropping the downgrade from the batch path, which would retire the
case the hold was built for -- one advisory-only variant reject sinking a whole
explore grid; and threading a second ``payload`` argument through
``verdict_held_to_its_rule``, which spreads "entry first, payload second" over
two call sites instead of naming it once.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): read a citation in every prose field the entry states

``reasoning`` was tried first and any non-blank line ended the search, so an
entry that fills both keys -- ``reasoning: "See the per-variant notes."`` beside
a ``rationale`` opening with the advisory code -- had its citation go unread and
its reject stand. The two keys are one speaker's grounds for one verdict and
nothing ranks them, so whichever came first decided whether the other was seen.

Read the opening line of each key the entry fills. Only the reach widens: the
citation test itself is unchanged, and the per-field stop on the first non-blank
line is what keeps a fenced or after-the-fact mention out of the grounds.

Rejected: leaving it and documenting the short-circuit at the call site. It
fails safe, but the safe direction here costs the round every proposal in the
set, and the order of two fields is not a reason to spend them.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(loop): drop a citation case no mutation kills on its own

``x_<code>: ...`` was claimed to be what fails a loose opener, but a loose
opener still refuses it: ``x`` is in no widening of the character class, so
``re.match`` fails at position 0 either way. The only mutation it caught was
``re.match`` -> ``re.search``, which three other cases already catch, and the
end of the anchor it was there for -- nothing but whitespace or a backtick may
precede the code -- is covered by the list and quote markers a model actually
writes.

In its place, a non-breaking space between the code and the colon. That is the
one behavioural difference between ``[ \t]*:`` and ``\s*:``, the row of the
matrix no test reached: a line has already been through ``splitlines``, so
exotic unicode spaces are all a wider class would add. The gap stays ASCII, and
the case now says so.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(specialists): leave one reader of the forbidden field list

``scan_quantitative_claims`` computed the same ``keys & FORBIDDEN_*``
intersection ``strip_forbidden_proposal_fields`` does, at both the payload and
the proposal level, and returned it to a runner that discarded it: one question,
two implementations, and nothing keeping them in step past the point where one
of them stopped being read.

Narrow it to the numeric prose warnings it alone produces, and name it for
them. The field list now has a single reader, so the ``confidence`` asymmetry
between the payload and proposal sets cannot drift.

Brute-forced the removal first: over every subset of size <= 2 of
``FORBIDDEN_PROPOSAL_FIELDS | {summary, other}`` at the top level crossed with
the same per proposal, with a nested ``proposal_set`` and the non-list shapes,
the intersection removed and the one that stays agreed on all 3136 cases and
raised the same ``TypeError`` on a scalar ``proposal_set``, so the audit note is
unchanged.

Rejected: extracting the intersection into a shared ``_forbidden_fields``
helper, which would have left the dead second return in place -- the question
only has one caller, so it wants one function, not two agreeing ones.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): let the batch's grounds hold a variant's reject, never soften it

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): hold a variant on every ground the batch states, attributing none

``verdict_map`` is in no version of the Critic's output schema, so nothing asks
the Critic to spell a variant's key inside the ``risks`` and ``required_evidence``
a batch states once for the whole set. Reading the prose for a name answered that
by guess, and the guess ran the wrong way: a blocker written for the set that
names one variant as its example, or a finding whose remediation field points at
how a sibling was fixed, was struck off this variant's grounds. The reject then
rested on one ground, its rationale's advisory citation supplied the rule, and
the set was dispatched -- including variants the Critic never cleared.

Attribution says what a finding is about, never what it is not. Every finding the
payload states now binds every entry, and an entry filing findings under its own
key adds to them rather than answering for them. The two errors do not cost the
same: binding a finding that was about a sibling withholds a downgrade and leaves
the reject the Critic wrote, which is the pre-feature outcome, while dropping one
that did bind dispatches a proposal it refused.

``risks`` stated in some shape other than the schema's list has stated grounds
whose number cannot be read off -- "the patch does not apply and there is no
rollback plan" is two -- so it counts as more than one rather than as the single
ground the hold is confined to. That is the reading the serialiser's wrapping
had quietly changed; the grounds path no longer goes through it, since its job
is rendering a verdict and this one is counting it.

Known limitation: a batch stating two risks or any required evidence now holds
every reject in the set, including one resting on nothing but an advisory rule.
That entry keeps its reject and the set collapses to it. Recovering the downgrade
needs attribution the Critic is asked for -- the batch shape documented in the
output schema -- which is a prompt change this does not make.

Rejected: attributing positively off a leading ``"<name>:"``, which is the same
prose guess with a narrower failure and still rests on a convention nothing
states; and inheriting only codes a handed rule declares, which inherits nothing
at all, since every rule in ``review_constraints`` declares ``advise`` -- a batch
declaring a hard code would have its variants downgraded on the advisory rules
their rationales cite.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(loop): hold a variant on every finding the batch states, counting none

``verdict_rests_on_one_ground`` allows a reject one stated risk beside its
citation. On the single path that is one statement by one author -- "the
proposal claims a 12% gain and has no rollback plan" names the rule and the
risk in the same breath -- so the risk is the cited rule's. A batch states its
risks once for the whole set while the citation belongs to the entry, and
nothing connects them, so the ``<= 1`` threshold was itself an attribution in
the direction that dispatches: a payload stating one blocker, "no variant in
this set supplies a rollback plan", with every entry citing the format rule,
downgraded both rejects and ran the whole grid. State the blocker twice and it
correctly held.

A finding the batch states now holds every reject in the set whatever its
count, and only an entry's own grounds can support a downgrade. That is the
rule the batch path already had -- what the batch states adds to the grounds a
variant is held on, never supplies the grounds it is softened on -- with
arithmetic that claimed more than it could tell taken out of it. Since
``required_evidence`` already withheld the downgrade on one item, counting a
risk the same way makes the two readings identical, so the findings are no
longer copied onto the entry at all: they are read where they are stated, which
is one veto instead of a merge feeding a count.

Restated limitation, measured against the shapes the repo teaches rather than
the shapes the suite happens to hold: the batch downgrade now needs a payload
that states no findings at all. That is the batch shape the runtime teaches --
``{target_proposal_msg_id, verdict_map: {name: {verdict, rationale?,
failure_reason_code?}}}``, the only batch payload spelled out anywhere in
``src`` (PolicyGate's repair hint) and all ``references/intent_envelope.md``
asks for. It is not the reject shape ``references/verdict_schema.md``
documents: that states one risk *and* one required-evidence item, as both
reject exemplars in ``critic/tests/expected_outputs.json`` do, so a batch
written in the single-verdict style keeps every reject it wrote, including one
resting on nothing but an advisory rule.

``test_a_variant_resting_only_on_the_cited_rule_still_gives_up_its_reject``
gives up its payload risk, deliberately. Its justification was that a single
risk restating the cited rule's complaint is what a verdict resting on one
advisory ground looks like -- true of that risk's prose, which nothing reads,
and false of "no variant supplies a rollback plan" stated in the same slot. The
half that generalises stays: notes and evidence pointers are not findings, and
a variant beside them still gives up its reject.

Also pinned, both previously unclaimed and unpinned: the single path moved when
the ground count stopped going through the serialiser. ``risks: 1`` and
``required_evidence: 1`` used to raise ``TypeError`` into the router's
catch-all, which recorded ``handle_intent_exception`` and dropped the intent,
leaving the proposal undecided for the rest of the session; the verdict the
Critic wrote is now what the proposal is decided on. And an empty list slot
states nothing -- one risk beside an empty one is one ground, an empty evidence
slot is not a request -- which is the reading ``serialize_verdict_advisory``
already takes of the same fields.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop the unused top-level tokenize import from coordinator_helpers

The helper already imports tokenize_server_args_preserving_json inside
_dedupe_extra_server_args to avoid a cyclic import. The module-level
copy was unused and tripped CodeQL.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

4 participants