fix(trekbrief): the review stamp covers only text the reviewer saw

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-12 20:03:26 +02:00
commit 94de23ca3b
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
2 changed files with 61 additions and 4 deletions

View file

@ -177,7 +177,7 @@ Put the task in one of three size classes. In doubt, take the heaviest that fits
| Class | Typical task | What the pipeline does | | Class | Typical task | What the pipeline does |
|---|---|---| |---|---|---|
| `small` | one change in one place, no new dependency, no interface change, easy to undo | one round, no follow-up; brief-reviewer runs once and its gaps become written assumptions, not questions; Phase 3.5 is skipped | | `small` | one change in one place, no new dependency, no interface change, easy to undo | one round, no follow-up; brief-reviewer runs once and its gaps become written assumptions, not questions, written after the review stamp (Step 4e, passed gate, step 4); Phase 3.5 is skipped |
| `bounded` | several files or one module behind a clear boundary, no architecture decision | the round + at most one follow-up; at most one brief-review follow-up; Phase 3.5 asks one question for all four phases | | `bounded` | several files or one module behind a clear boundary, no architecture decision | the round + at most one follow-up; at most one brief-review follow-up; Phase 3.5 asks one question for all four phases |
| `architectural` | a new module, interface, dependency, data model or migration; anything hard to undo | follow-up rounds while answers open new branches; the full brief-review loop; Phase 3.5 asks per phase | | `architectural` | a new module, interface, dependency, data model or migration; anything hard to undo | follow-up rounds while answers open new branches; the full brief-review loop; Phase 3.5 asks per phase |
@ -590,8 +590,8 @@ Read the brief template first:
### Loop bound ### Loop bound
**The cap follows the size class:** `small` 1 review iteration (no **The cap follows the size class:** `small` 1 review iteration (no
follow-up question: what fails is written into `## Open Questions / follow-up question: its gaps become assumptions after the stamp on a passed gate
Assumptions` and `## Brief Quality`), `bounded` 2 (one follow-up), (Step 4e, step 4), into `## Brief Quality` on an exhausted loop), `bounded` 2 (one follow-up),
`architectural` 3 (two rounds of targeted follow-ups). A follow-up question `architectural` 3 (two rounds of targeted follow-ups). A follow-up question
counts against the class budget; when the reviewer shows the task is bigger counts against the class budget; when the reviewer shows the task is bigger
than its class, raise the class first (Step 3b) and use the new cap. than its class, raise the class first (Step 3b) and use the new cap.
@ -723,7 +723,8 @@ stumble.)
Any later edit to the body makes the stamp stale and `/trekplan` reviews Any later edit to the body makes the stamp stale and `/trekplan` reviews
again. Exit 1 (not stamped) is said in one line; it only means the review again. Exit 1 (not stamped) is said in one line; it only means the review
runs twice. Only a passed gate is stamped. runs twice. Only a passed gate is stamped.
4. Break the loop and proceed to Step 4g. 4. Class `small` only: now write the reviewer's gaps into `## Open Questions / Assumptions` (and `## Brief Quality` when it applies). They come after the stamp on purpose: the reviewer never saw them, so the stamp reads stale and `/trekplan` reviews the brief once more.
5. Break the loop and proceed to Step 4g.
**If gate fails AND iteration count < the class cap:** **If gate fails AND iteration count < the class cap:**
1. Identify the weakest dimension (lowest score; tie broken by priority: 1. Identify the weakest dimension (lowest score; tie broken by priority:

View file

@ -0,0 +1,56 @@
// tests/validators/brief-review-stamp-order.test.mjs
// /trekbrief stamps only text the reviewer saw. A `small` brief gets one
// review; its gaps are written as assumptions. When that writing came before
// the stamp, the stamp covered text no reviewer read and /trekplan skipped its
// own review. The gaps are now written after the stamp, on purpose, so the
// stamp reads stale and /trekplan reviews the brief once more.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { stampBriefReview, checkBriefReview } from '../../lib/validators/brief-review-stamp.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const brief = readFileSync(join(ROOT, 'commands', 'trekbrief.md'), 'utf8');
const STAMP = 'brief-review-stamp.mjs --stamp';
const GAPS = "Class `small` only: now write the reviewer's gaps";
const PASS_START = '**If gate passes:**';
const PASS_END = '**If gate fails AND iteration count < the class cap:**';
function unique(text, needle) {
const at = text.indexOf(needle);
assert.ok(at >= 0, `found: ${needle}`);
assert.equal(text.lastIndexOf(needle), at, `unique in commands/trekbrief.md: ${needle}`);
return at;
}
test('trekbrief: the stamp comes before the small-class gaps are written, both in the passed-gate branch', () => {
const start = unique(brief, PASS_START);
const end = unique(brief, PASS_END);
const stamp = unique(brief, STAMP);
const gaps = unique(brief, GAPS);
assert.ok(start < end, 'passed-gate branch precedes the failed-gate branch');
assert.ok(start < stamp && stamp < end, 'the stamp sits in the passed-gate branch');
assert.ok(start < gaps && gaps < end, 'the gaps instruction sits in the passed-gate branch');
assert.ok(stamp < gaps, 'order: stamp, then the small-class gaps');
const after = brief.slice(gaps, end);
assert.match(after, /They come after the stamp on purpose/);
assert.match(after, /## Open Questions \/ Assumptions/);
});
test('guard: stamping a brief and then appending an assumption reads as stale', () => {
const text = [
'---', 'type: trekbrief', 'brief_version: "2.2"', 'task: t', 'slug: s', 'ceremony: small', '---', '',
'## TL;DR', '', 'A toy.', '', '## Intent', '', 'Why it matters.', '', '## Goal', '', 'What.', '',
'## Open Questions / Assumptions', '', '- None.', '',
].join('\n');
const s = stampBriefReview(text);
assert.equal(s.stamped, true);
assert.equal(checkBriefReview(s.text).state, 'current');
const withGap = s.text + '- Assumed, not asked: the setting defaults to off (reviewer gap).\n';
assert.equal(checkBriefReview(withGap).state, 'stale');
});