fix(specialist): stop rejecting our own proposals on a format rule we ship broken - #1161
Conversation
…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>
CI E2E report — ✅ Succeeded
|
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # src/hyperloom/orchestrator/prompts/critic.md
chaojhou
left a comment
There was a problem hiding this comment.
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, andconfidenceis inFORBIDDEN_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, whereconfidenceis 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()derivesforbidden_proposal_fieldsfromsorted(FORBIDDEN_PROPOSAL_FIELDS)andcritic.mdno longer hardcodes the six names. Worth noting the prompt copy had already drifted — it was missingexpected_gain_pct. advisegenuinely lets the proposal through:intent_router.pymaterializes onverdict in ("approve", "advise"), so switching the verdict is the real fix and not just a softer label.- The
framework_agentcarve-out forpredicted_gain_pct/prior_score/prior_rankis preserved and clarified. That matters, because the verdict that lost the run namedpredicted_gain_pctand anyone fixing this from the issue text alone would plausibly have deleted a field thatpropose_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.
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>
|
Thanks — both changes are in, as 1. The remedy is no longer prose-onlyTook this in this PR rather than as a follow-up: the cost of the failure is a Generalised it one step beyond Two details worth flagging:
The downgrade is logged and recorded as a I left 2.
|
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>
|
Pushed The downgrade keyed on an entry-level {
"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 The commit adds
Tests: the field payload's exact shape, an ordinary reject whose prose happens to mention The pre-existing |
|
/retest The e2e run for Verified |
|
Following up on my earlier review — the deterministic downgrade I asked for is here and built the right way. One blocking item, in the commit added after that review. The prose scan downgrades on a mention, not on a citation
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
That has no
The negative test aims at the adjacent case rather than this one. Suggested shape. Anchor to the citation form the field run actually produced — from your own commit message, The precedence design already in place is the right instinct and worth keeping: an explicit non-advisory 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 |
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>
|
Both items handled at The prose scan now reads a citation, not a mentionFixed exactly as suggested, in _CITATION_OPENER = r"[\s>*\-–—`\"'(\[]*"
...
if re.search(rf"^{_CITATION_OPENER}{re.escape(code)}`?\s*:", text, re.MULTILINE):
Your example is now the test. The weak negative test you flagged is replaced rather than kept: it asserted on prose naming the field Three more that came out of working through it
The report already renders the effective verdictI checked I also confirmed |
`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>
`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>
``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>
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
``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>
… 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>
Refs #1143.
Deliberately not
Closes: #1143's agreed fix had two halves, and only the firstis here. (a) strip the forbidden fields at source instead of only auditing them —
done. (b) pass
approved_variant_namesso a partial reject stops discardingevery variant — untouched, and
_handle_review_verdict's collapse still ranksrejectaboveadvise, so a multi-variant proposal with one genuinely rejectedvariant still loses the set. The reported failure no longer produces a
rejectat 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_claimsfinds the forbidden fields, records them in an auditnote, and forwards the payload unchanged; the Critic is then instructed to reject
the whole
proposal_setover 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 inFORBIDDEN_PROPOSAL_FIELDS. A compliant specialist therefore tripped the guardon 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, whereconfidenceis the round-level self-assessment our own output schema asks forand 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_violationappears 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_actionchannel taught by our own prompt template. No output couldsatisfy both readings of that field name.
Changes
Commit 1 —
fix(specialist): stop shipping a proposal schema that fails its own guardspecialist_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.
confidence.Commit 2 —
fix(critic): make a proposal format slip advisory instead of fatalreview_constraintsnow carries a rule descriptor generated fromFORBIDDEN_PROPOSAL_FIELDS, so the Critic's list cannot drift from theenforced one. Uses the mechanism the cross-domain rules already use rather than
templating a verbatim-loaded prompt.
advise. Detection breadth is deliberatelykept — 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 declaredCommit 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 theCritic is handed — the quantitative-claim rule plus every cross-domain rule
declaring
advise— rather than restating them, so a rule that changes itsverdict cannot leave a stale entry behind.
rejectciting one of those codes is held toadvisein the verdict path,logged, and recorded as an observation, so prompt drift stays visible instead
of being silently corrected.
verdict_mapentry before the collapse, since the collapseranks
rejectaboveadvise: one variant rejected on an advisory-only rulewould otherwise still discard its siblings' advice. This narrows the blast
radius for this class only; half (b) above is still the general fix.
adviseis already permissive for theintegrate_patchgate, so the downgradelands 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
confidencepreserved, descriptor matches the enforced list andcarries
advise, and the advisory-code set covers every rule that declaredadvisewhile leaving a rule that keptrejectout of ittest_critic_verdict_map.py— a reject citing an advisory-only rule is heldto
adviseand materialises; the downgrade leaves a log line and anobservation; 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—_finalizewrites a strippedartifact and still emits the audit note
test_specialist_prompt_builder_coverage_unit.py— cold start no longerasks for a forbidden field
test_critic_agent_backend.py— the bundle delivered to the model carriesthe rule
suites; the only 2 failures are pre-existing (
claude-agent-sdknotinstalled in this environment, identical on the base commit)
Made with Cursor