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 |
|---|---|---|
| `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 |
| `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
**The cap follows the size class:** `small` 1 review iteration (no
follow-up question: what fails is written into `## Open Questions /
Assumptions` and `## Brief Quality`), `bounded` 2 (one follow-up),
follow-up question: its gaps become assumptions after the stamp on a passed gate
(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
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.
@ -723,7 +723,8 @@ stumble.)
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
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:**
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');
});