fix(review): the coordinator gets the brief's choices and writes the brief-conflict mark

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-12 18:28:54 +02:00
commit 11315d04eb
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 239 additions and 6 deletions

View file

@ -66,10 +66,15 @@ You will receive a prompt containing:
the /trekreview triage gate.
- **Brief metadata** — `task`, `slug`, `project_dir`, `brief_path` from
the brief frontmatter.
- **Brief choices** — the brief's `## Non-Goals`, `## Constraints` and
`## Preferences` sections and the operator decisions in
`## Open Questions / Assumptions`, verbatim. Read only by the
brief-conflict rule (Pass 3).
- **Scope SHA range** — `scope_sha_start`, `scope_sha_end`,
`reviewed_files_count`.
- **Mode** — `default` or `quick`. In `quick` mode, skip Pass 3
(reasonableness filter); Passes 1, 2, 4 still run.
(reasonableness filter) except the brief-conflict rule, which applies in every mode;
Passes 1, 2, 4 still run.
- **Rule catalogue** — `lib/review/rule-catalogue.mjs`. Findings whose
`rule_key` is not in this set are dropped by Pass 3.
@ -124,6 +129,7 @@ over-long `detail` sent a finding to `unverified`, and S118's most serious
finding fell out of the Handover 6 list that way. Length is packaging, never
a claim: only small findings are trimmed, and the trims are counted
(`trimmed` in the JSON block). The trim decision reads the **catalogue** tier,
except a lowerable brief conflict, which is judged as MINOR (decide the mark before trimming),
so a mislabelled MAJOR is never cut. Borrowed: "report at most five nits …
summarize the rest as a count" — cap only the nits (ai-native-sdlc
`assets/REVIEW.md:7-23`, MIT).
@ -157,8 +163,32 @@ Remove findings that fail ANY of these tests:
catalogue tier (e.g., `MISSING_TEST` emitted as MINOR). Reset to the
catalogue tier; this is a correction, neither a drop nor an unverified.
**The brief-conflict rule (one fixed rule).** Read the **Brief choices** input.
An engine finding (`engine: code-review` or `security-review`) whose recommended action
contradicts a passed Non-Goal or stated choice gets `contradicts_brief: "<verbatim>"`
(that Non-Goal or choice, word for word) and is kept, never removed;
only a `/code-review` finding at the MAJOR tier and at severity MAJOR then drops to MINOR, with `brief_lowered_from: MAJOR`
(in place of the catalogue reset above);
a BLOCKER-tier, a MINOR-tier and any `/security-review` finding only get the mark,
and no engine finding is otherwise written below its catalogue tier.
The rule reads engine findings alone, so only an engine finding is ever marked:
`engine` is written only on a finding whose kept Pass 1 survivor itself came from an engine payload
(`claude-code-review` → `code-review`, `claude-security-review` → `security-review`),
and a reviewer-agent finding (no `engine`) and a finding whose `rule_key` is not in the catalogue are never marked.
Remove any `brief_lowered_from` an incoming finding carries, and any mark on a finding
without an `engine`: only this rule writes them.
The rule is about the finding's recommended action, not the rule key `NON_GOAL_VIOLATED`
(a reviewer's claim that the delivered code breaks a Non-Goal is judged like any other finding).
The mark is the brief-conflict reason (`contradicts_brief`), a field, never a `suppressed_reason`:
the finding stays kept and counted at the severity written, never `unverified`.
`lib/review/coordinator-contract.mjs` (`lowerableBriefConflict`, `illegalBriefLowering`)
holds the same rule, and on an ALLOW or WARN review
the verdict gate refuses (exit 2) every other marked or lowered shape.
In `quick` mode, skip this pass entirely. Note the skip in the
Executive Summary so the reader knows reasonableness was not applied.
The brief-conflict rule above still applies in quick mode (the mark and the
one lowering) and changes nothing else.
**High-effort normalization (v5.1.1):** When the review is invoked
under high-effort mode (`phase_signals[review].effort: high`), Pass 3
@ -170,7 +200,8 @@ category in the catalogue). The original rule_key is
preserved in the finding's `original_rule_key` field for diagnostic
purposes. This normalization happens BEFORE writing review.md,
ensuring all `rule_key` values in the final review match the
catalogue.
catalogue. The brief-conflict rule (Pass 3) still applies under high-effort
mode (the mark and the one lowering) and changes nothing else.
### Suppression is two-valued (fail-closed)
@ -310,6 +341,9 @@ followed by these fields:
- `- title: {short imperative title}`
- `- detail: {what is wrong, with citation}`
- `- recommended_action: {one imperative step}`
- `- engine: {code-review | security-review, when the finding came from an engine}`
- `- contradicts_brief: {verbatim Non-Goal or choice, only when set}`
- `- brief_lowered_from: {MAJOR, only when lowered for the brief}`
### Trailing JSON block
@ -333,6 +367,20 @@ The LAST fenced block in the file is a `json` block:
"title": "...",
"detail": "...",
"recommended_action": "..."
},
{
"id": "<40-char-hex>",
"severity": "MINOR",
"rule_key": "CODE_REVIEW_DEFECT",
"file": "lib/http.mjs",
"line": 42,
"brief_ref": "Non-Goal 2",
"title": "...",
"detail": "...",
"recommended_action": "...",
"engine": "code-review",
"contradicts_brief": "No retry layer around the HTTP client",
"brief_lowered_from": "MAJOR"
}
]
}
@ -346,13 +394,17 @@ for the ID list.
## Hard rules
- **Bounded operations only.** You do NOT read the diff. You do NOT
re-evaluate findings against the brief. You do NOT generate new
re-evaluate findings against the brief, with one named exception: the
brief-conflict rule (Pass 3), which marks an engine finding against the
**Brief choices** and lowers only the one legal shape. You do NOT generate new
findings. The reviewers' outputs are your sole input. Synthesis-level
inference (e.g., "these 3 findings together suggest a pattern") is
forbidden in v1.0.
- **Verdict is mechanical.** No "ALLOW with caveats" or other custom
verdicts. Only BLOCK / WARN / ALLOW per the threshold table.
- **Severity floor is the catalogue.** Pass 3 corrects mismatches by
- **Severity floor is the catalogue.** It holds for every finding
except a lowerable brief conflict, which is MINOR (Pass 3, `brief_lowered_from: MAJOR`).
Pass 3 corrects mismatches by
resetting to the catalogue tier — never by dropping. Pass 1's severity
tiebreak uses the catalogue tier, not the reviewer's emitted value.
- **Block-style YAML for findings list.** The frontmatter parser
@ -380,6 +432,7 @@ for the ID list.
reviewer outputs. Never escalate a finding's severity beyond what the
catalogue specifies.
- **Quick mode is documented.** When mode is `quick`, the Executive
Summary says so, and Pass 3 is skipped — no other changes.
Summary says so, and Pass 3 is skipped — no other changes,
except the brief-conflict rule, which applies in every mode.
- **Honesty in fallback paths.** If `scope_sha_start` is null (mtime
fallback), the Executive Summary names this limitation explicitly.

View file

@ -480,6 +480,15 @@ from Phase 5 AND the Phase 4.7 engine payloads, plus the triage map, brief
metadata, SHA range, the expected reviewer set (engines included), and each
not-run engine with its reason.
**Brief choices.** Next to the brief metadata, pass the brief's choices
verbatim: extract the `## Non-Goals`, `## Constraints` and `## Preferences`
sections and the operator decisions in `## Open Questions / Assumptions` from
the brief, word for word (a section the brief does not carry is passed as
absent, never written for it). The coordinator reads them for the
brief-conflict rule only (`agents/review-coordinator.md` Pass 3): an engine
finding whose recommended action contradicts one of them is kept and marked
`contradicts_brief`, never removed.
The coordinator runs the 4-pass process documented in
`agents/review-coordinator.md`:
@ -552,7 +561,11 @@ Contract (verified in S10 part B — follow exactly):
Then continue at **Phase 7** exactly as the prose path does — Phase 7 rendering, Phase 8
validation, and the operator gate are **shared** and substrate-independent (both paths
return the same `{verdict, findings}` shape).
return the same `{verdict, findings}` shape). The workflow's returned findings keep
`engine` (set by the script from the payload a finding came from, never from a label),
`contradicts_brief` and `brief_lowered_from`, and Phase 7 writes them into each
finding's field list and the trailing JSON block of review.md exactly as the prose path
does.
**Known limitation (per bake-off §Posture, surfaced not hidden).** Classifier interference
was measured **0** at 9-agent concurrency in the session's default permission mode; an

View file

@ -461,6 +461,22 @@ Since 1.1 (ordre 5):
- `## Declined to Judge` — required at `review_version` ≥ 1.1 (strict error, soft warning; 1.0 reviews exempt): every finding set aside without a ruling, with its reason (including engine findings outside the review scope, `engine:out-of-scope`), plus `Trimmed: N`. **No finding is struck for length**: only MINOR/SUGGESTION text is trimmed, and counted (`trimmed` in the JSON block); a long BLOCKER/MAJOR keeps every character.
- Findings from Claude Code's own `/code-review` and `/security-review` enter under the catalogue keys `CODE_REVIEW_DEFECT` (MAJOR), `CODE_REVIEW_IMPROVEMENT` (MINOR: a `/code-review` finding whose category is simplification, efficiency, reuse, style or readability) and `SECURITY_REVIEW_VULNERABILITY` (BLOCKER), or a narrower key, with `reviewer` `claude-code-review` / `claude-security-review`.
**The brief-conflict mark (fields `engine`, `contradicts_brief`, `brief_lowered_from`).**
Each finding may carry three more fields, in its body field list and in the trailing JSON block:
`engine` (`code-review` | `security-review`, on a finding that came from an engine),
`contradicts_brief` (the brief's Non-Goal or stated choice the finding's recommended action contradicts, verbatim)
and `brief_lowered_from` (`MAJOR`, written only by the one lowering).
The mark is a field on a kept finding, never a `suppressed_reason`.
Only an engine finding is ever marked; a reviewer-agent finding (no `engine`) and a finding whose `rule_key` is not in the catalogue never are.
The one legal lowering: a `/code-review` MAJOR-tier finding at severity MAJOR drops to MINOR, with `brief_lowered_from: MAJOR`;
a BLOCKER-tier, a MINOR-tier and any `/security-review` finding keep their severity and only carry the mark.
On an ALLOW or WARN review the verdict gate exits 2 on every finding `illegalBriefLowering` (`lib/review/coordinator-contract.mjs`) names:
any other `brief_lowered_from`, a mark on a reviewer-agent finding, a marked finding below its tier, a marked finding with an unknown key,
and an engine finding below its tier, marked or not (a BLOCK review exits 1 as before).
The gate cannot check an `engine` label on the prose path: there the coordinator writes the label, so a finding wrongly labelled `engine` is judged as an engine finding;
the workflow path (`--workflow`) takes `engine` from the payload source and writes it into the returned finding.
A plan made from the review skips a lowered finding: it is MINOR, and `/trekplan` takes BLOCKER and MAJOR findings only.
**Validation strategy:**
| Layer | When | What |

View file

@ -306,3 +306,154 @@ test('verdict gate — imports illegalBriefLowering from coordinator-contract.mj
assert.doesNotMatch(src, /function\s+illegalBriefLowering\b/,
'verdict-gate.mjs must not carry its own copy of the rule');
});
// ---- the prose states the same rule (Step 10) --------------------------------
//
// The coordinator is a model reading agents/review-coordinator.md; the gate
// above enforces the rule on what it writes. These pins keep the prose saying
// the same rule the code enforces. Every pin reads a bounded cut, never the
// whole file, so a phrase elsewhere in the file cannot satisfy it.
const readDoc = (...rel) => readFileSync(join(ROOT, ...rel), 'utf-8');
/** The text from `startMarker` up to (not including) the first `endMarker` after it. */
function cut(text, startMarker, endMarker, label) {
const start = text.indexOf(startMarker);
assert.ok(start >= 0, `${label}: start marker missing: ${JSON.stringify(startMarker)}`);
const end = text.indexOf(endMarker, start + startMarker.length);
assert.ok(end > start, `${label}: end marker ${JSON.stringify(endMarker)} missing after the start`);
return text.slice(start, end);
}
/** The sentence from `startPhrase` to its full stop (the first '.' after it). */
function sentenceFrom(text, startPhrase, label) {
const start = text.indexOf(startPhrase);
assert.ok(start >= 0, `${label}: sentence start missing: ${JSON.stringify(startPhrase)}`);
const stop = text.indexOf('.', start + startPhrase.length);
assert.ok(stop > start, `${label}: the sentence has no full stop`);
return text.slice(start, stop + 1);
}
/** One hard-rule bullet, from its bold lead to the next bullet. */
function hardRule(coord, lead) {
const at = coord.indexOf('\n## Hard rules');
assert.ok(at >= 0, 'the ## Hard rules section exists');
const rules = coord.slice(at);
return cut(`${rules}\n- **`, `- **${lead}**`, '\n- **', `hard rule "${lead}"`);
}
test('trekreview Phase 6 — passes the brief\'s Non-Goals, Constraints, Preferences and Open Questions decisions to the coordinator', () => {
const cmd = readDoc('commands', 'trekreview.md');
const p6 = cut(cmd, '## Phase 6 — Coordinator dedup + verdict', '\n## ', 'Phase 6');
for (const section of ['`## Non-Goals`', '`## Constraints`', '`## Preferences`', '`## Open Questions / Assumptions`']) {
assert.ok(p6.includes(section), `Phase 6 names the brief section ${section}`);
}
assert.match(p6, /operator decisions in `## Open Questions \/ Assumptions`/, 'only the operator decisions of Open Questions');
assert.match(p6, /verbatim/, 'the choices are passed verbatim');
assert.match(p6, /brief choices/i, 'Phase 6 names the input the coordinator reads');
});
test('trekreview workflow path — engine, contradicts_brief and brief_lowered_from are carried into review.md', () => {
const cmd = readDoc('commands', 'trekreview.md');
const wf = cut(cmd, 'Then continue at **Phase 7**', '\n\n', 'workflow paragraph');
for (const field of ['`engine`', '`contradicts_brief`', '`brief_lowered_from`']) {
assert.ok(wf.includes(field), `the workflow paragraph names ${field}`);
}
assert.match(wf, /review\.md/, 'the fields are written into review.md');
});
test('review-coordinator Input — names "Brief choices" (the four sections) and the mode line names the exception', () => {
const coord = readDoc('agents', 'review-coordinator.md');
const input = cut(coord, '## Input', '\n## Your 4-pass process', 'Input');
assert.match(input, /\*\*Brief choices\*\*/);
const choices = cut(input, '**Brief choices**', '\n- **', 'Brief choices bullet');
for (const section of ['`## Non-Goals`', '`## Constraints`', '`## Preferences`', '`## Open Questions / Assumptions`']) {
assert.ok(choices.includes(section), `Brief choices names ${section}`);
}
assert.match(choices, /verbatim/);
const mode = cut(input, '- **Mode**', '\n- **', 'mode line');
assert.match(mode, /except the brief-conflict rule, which applies in every mode/);
});
test('review-coordinator Pass 3 — only an engine finding is marked, and the one lowering needs severity MAJOR', () => {
const coord = readDoc('agents', 'review-coordinator.md');
const p3 = cut(coord, '### Pass 3', 'In `quick` mode', 'Pass 3');
assert.match(p3, /`contradicts_brief: "<verbatim>"`/, 'the mark carries the Non-Goal or choice verbatim');
assert.match(p3, /engine finding \(`engine: code-review` or `security-review`\)/, 'only an engine finding is marked');
assert.match(p3, /only an engine finding is ever marked/);
assert.match(p3, /only a `\/code-review` finding at the MAJOR tier and at severity MAJOR then drops to MINOR, with `brief_lowered_from: MAJOR`/,
'the one lowering, severity MAJOR required');
assert.match(p3, /a BLOCKER-tier, a MINOR-tier and any `\/security-review` finding only get the mark/);
assert.match(p3, /no engine finding is otherwise written below its catalogue tier/);
assert.match(p3, /`engine` is written only on a finding whose kept Pass 1 survivor itself came from an engine payload/);
assert.match(p3, /a reviewer-agent finding \(no `engine`\) and a finding whose `rule_key` is not in the catalogue are never marked/);
assert.match(p3, /not the rule key `NON_GOAL_VIOLATED`/);
assert.ok(p3.includes('the brief-conflict reason (`contradicts_brief`), a field, never a `suppressed_reason`'),
'the mark is named as a field, never a suppression reason');
assert.match(p3, /verdict gate refuses \(exit 2\) every other marked or lowered shape/);
});
test('review-coordinator — quick mode and high-effort normalization each still apply the brief-conflict rule', () => {
const coord = readDoc('agents', 'review-coordinator.md');
const pass3 = coord.indexOf('### Pass 3');
assert.ok(pass3 >= 0, 'Pass 3 heading');
const quick = cut(coord.slice(pass3), 'In `quick` mode, skip this pass entirely.', '\n\n', 'quick-mode paragraph');
assert.match(quick, /brief-conflict rule[^.]*still applies/, 'the quick-mode paragraph says the rule still applies');
const high = cut(coord, '**High-effort normalization (v5.1.1):**', '\n### ', 'high-effort paragraph');
assert.match(high, /brief-conflict rule[^.]*still applies/, 'the high-effort paragraph says the rule still applies');
});
test('review-coordinator Pass 2 — the trim rule judges a lowerable brief conflict as MINOR, inside its own sentence', () => {
const coord = readDoc('agents', 'review-coordinator.md');
const s = sentenceFrom(coord, 'The trim decision reads the **catalogue** tier', 'trim rule');
assert.match(s, /except a lowerable brief conflict, which is judged as MINOR \(decide the mark before trimming\)/);
assert.match(s, /so a mislabelled MAJOR is never cut\.$/, 'the sentence still ends on its own guarantee');
});
test('review-coordinator hard rules — the brief re-evaluation, the severity floor and quick mode each name the exception', () => {
const coord = readDoc('agents', 'review-coordinator.md');
assert.match(hardRule(coord, 'Bounded operations only.'), /brief-conflict rule/,
'"You do NOT re-evaluate findings against the brief" names its one exception');
assert.match(hardRule(coord, 'Severity floor is the catalogue.'), /except a lowerable brief conflict, which is MINOR/);
assert.match(hardRule(coord, 'Quick mode is documented.'), /except the brief-conflict rule, which applies in every mode/);
});
test('review-coordinator output — the field list and the trailing JSON example carry engine, contradicts_brief, brief_lowered_from', () => {
const coord = readDoc('agents', 'review-coordinator.md');
const fields = cut(coord, 'Each Findings subsection uses the', '### Trailing JSON block', 'field list');
assert.ok(fields.includes('- `- engine: {code-review | security-review, when the finding came from an engine}`'));
assert.ok(fields.includes('- `- contradicts_brief: {verbatim Non-Goal or choice, only when set}`'));
assert.ok(fields.includes('- `- brief_lowered_from: {MAJOR, only when lowered for the brief}`'));
assert.ok(!fields.includes('original_severity'), 'the brief rule does not read original_severity');
const block = coord.match(/### Trailing JSON block\n\nThe LAST fenced block in the file is a `json` block:\n\n```json\n([\s\S]*?)\n```/);
assert.ok(block, 'the trailing JSON example is present');
const objects = block[1].match(/\{[^{}]*\}/g) ?? [];
const lowered = objects.find((o) => /"brief_lowered_from":\s*"MAJOR"/.test(o));
assert.ok(lowered, 'the JSON example shows a finding with brief_lowered_from');
assert.match(lowered, /"engine":\s*"code-review"/);
assert.match(lowered, /"contradicts_brief":\s*"[^"]+"/);
assert.match(lowered, /"severity":\s*"MINOR"/);
assert.match(lowered, /"rule_key":\s*"CODE_REVIEW_DEFECT"/);
});
test('Handover 6 — names the fields, engine findings only, the one lowering and the five shapes the gate refuses', () => {
const contracts = readDoc('docs', 'HANDOVER-CONTRACTS.md');
const h6 = cut(contracts, '## Handover 6 — `review.md` → plan', '\n## Handover 7', 'Handover 6');
for (const field of ['`engine`', '`contradicts_brief`', '`brief_lowered_from`']) {
assert.ok(h6.includes(field), `Handover 6 names ${field}`);
}
assert.match(h6, /only an engine finding is (?:ever )?marked/i, 'engine findings only');
assert.match(h6, /`\/code-review` MAJOR-tier finding[^.]*MINOR[^.]*`brief_lowered_from: MAJOR`/, 'the one legal lowering');
assert.match(h6, /exits 2/);
const shapes = [
/any other `brief_lowered_from`/,
/a mark on a reviewer-agent finding/,
/a marked finding below its tier/,
/a marked finding with an unknown key/,
/an engine finding below its tier, marked or not/,
];
for (const shape of shapes) assert.match(h6, shape, `Handover 6 names the refused shape ${shape}`);
assert.match(h6, /cannot check an `engine` label on the prose path/);
assert.match(h6, /skips a lowered finding/, 'a plan made from the review skips a lowered (MINOR) finding');
});