fix(plan): every exploration agent runs; the brief is reviewed again only when it changed
voyage-lekerepo trial (2026-10-06) point 14, operator decision 2026-10-08
(«alle agenter, ny briefsjekk bare ved endring»):
- trekplan: the "Adaptive" hard rule (a 10-file project does not need 7
agents) contradicted Phase 5 ("All agents run for all codebase sizes");
it is gone, in line with quality before cost.
- brief-review-stamp.mjs: /trekbrief stamps the brief body when its
brief-reviewer gate passes; /trekplan Phase 4b skips its own review only
while the stamp is current. Stale (edited) or absent (another producer,
or a gate that did not pass) reviews as before. Additive optional
frontmatter fields, documented in Handover 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
393b8e5a0a
commit
1b5218e8f4
6 changed files with 231 additions and 4 deletions
117
lib/validators/brief-review-stamp.mjs
Normal file
117
lib/validators/brief-review-stamp.mjs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// lib/validators/brief-review-stamp.mjs
|
||||
// The brief-review stamp: /trekplan reviews a brief again only when it changed.
|
||||
//
|
||||
// Operator decision 2026-10-08 (voyage-lekerepo trial, point 14): /trekplan
|
||||
// Phase 4b re-ran brief-reviewer right after /trekbrief had run it on the same
|
||||
// text. Now /trekbrief, when its brief-reviewer gate passes, stamps the brief's
|
||||
// BODY (everything after the frontmatter) into two optional frontmatter fields:
|
||||
//
|
||||
// brief_reviewed_hash: "sha256:<64 hex>"
|
||||
// brief_reviewed_at: "<ISO-8601>"
|
||||
//
|
||||
// /trekplan Phase 4b runs `--check`: `current` skips the second review,
|
||||
// `stale` (the body changed after the review) or `absent` (no stamp: a gate
|
||||
// that did not pass, or a brief from another producer) runs it as before.
|
||||
// Frontmatter is outside the hash, so the intent approval stamp written after
|
||||
// the review keeps it current; CRLF and trailing whitespace do not count.
|
||||
//
|
||||
// WHAT THE STAMP DOES NOT PROVE: the same trust class as the intent marker
|
||||
// (lib/validators/intent-approval.mjs) — any session that can edit the brief
|
||||
// can write it. It is a trace that the review ran against this body, never a
|
||||
// signature; the worst a forged stamp does is skip one duplicate review.
|
||||
//
|
||||
// CLI:
|
||||
// node lib/validators/brief-review-stamp.mjs --stamp <brief.md> exit 0 stamped, 1 refused
|
||||
// node lib/validators/brief-review-stamp.mjs --check [--json] <brief.md>
|
||||
// exit 0 = current (skip the review); 1 = stale or absent (review); 2 = usage / unreadable
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export const REVIEWED_HASH_FIELD = 'brief_reviewed_hash';
|
||||
export const REVIEWED_AT_FIELD = 'brief_reviewed_at';
|
||||
|
||||
const FRONTMATTER = /^(?---\r?\n)([\s\S]*?)(\r?\n---)(?:\r?\n|$)/;
|
||||
|
||||
function split(text) {
|
||||
const m = FRONTMATTER.exec(String(text ?? ''));
|
||||
if (!m) return null;
|
||||
return { m, front: m[2], body: String(text).slice(m.index + m[0].length) };
|
||||
}
|
||||
|
||||
/** sha256 over the body: CRLF -> LF, trailing whitespace per line and at the end dropped. */
|
||||
export function computeBodyHash(body) {
|
||||
const norm = String(body ?? '').replace(/\r\n?/g, '\n').split('\n').map((l) => l.replace(/\s+$/, '')).join('\n').replace(/\n+$/, '');
|
||||
return `sha256:${createHash('sha256').update(norm, 'utf8').digest('hex')}`;
|
||||
}
|
||||
|
||||
function fieldValue(front, name) {
|
||||
const line = front.split(/\r?\n/).find((l) => l.startsWith(`${name}:`));
|
||||
if (!line) return null;
|
||||
return line.slice(name.length + 1).trim().replace(/^["']|["']$/g, '');
|
||||
}
|
||||
|
||||
/** @returns {{ stamped: boolean, text: string, hash?: string, reason?: string }} */
|
||||
export function stampBriefReview(text, now = new Date()) {
|
||||
const parts = split(text);
|
||||
if (!parts) return { stamped: false, text, reason: 'no frontmatter block' };
|
||||
const hash = computeBodyHash(parts.body);
|
||||
const kept = parts.front.split(/\r?\n/).filter((l) =>
|
||||
!l.startsWith(`${REVIEWED_HASH_FIELD}:`) && !l.startsWith(`${REVIEWED_AT_FIELD}:`));
|
||||
kept.push(`${REVIEWED_HASH_FIELD}: "${hash}"`, `${REVIEWED_AT_FIELD}: "${now.toISOString()}"`);
|
||||
const { m } = parts;
|
||||
const out = m[1] + kept.join('\n') + m[3] + '\n' + parts.body;
|
||||
return { stamped: true, text: out, hash };
|
||||
}
|
||||
|
||||
/** @returns {{ state: 'current'|'stale'|'absent', stamped: string|null, actual: string|null }} */
|
||||
export function checkBriefReview(text) {
|
||||
const parts = split(text);
|
||||
if (!parts) return { state: 'absent', stamped: null, actual: null };
|
||||
const stamped = fieldValue(parts.front, REVIEWED_HASH_FIELD);
|
||||
const actual = computeBodyHash(parts.body);
|
||||
if (!stamped) return { state: 'absent', stamped: null, actual };
|
||||
return { state: stamped === actual ? 'current' : 'stale', stamped, actual };
|
||||
}
|
||||
|
||||
function isMain() {
|
||||
try {
|
||||
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain()) {
|
||||
const args = process.argv.slice(2);
|
||||
const mode = args.includes('--stamp') ? 'stamp' : args.includes('--check') ? 'check' : null;
|
||||
const path = args.find((a) => !a.startsWith('--'));
|
||||
if (!mode || !path) {
|
||||
process.stderr.write('Usage: brief-review-stamp.mjs --stamp <brief.md> | --check [--json] <brief.md>\n');
|
||||
process.exit(2);
|
||||
}
|
||||
let text;
|
||||
try {
|
||||
text = readFileSync(path, 'utf-8');
|
||||
} catch (e) {
|
||||
process.stderr.write(`brief-review-stamp: cannot read ${path}: ${e.message}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
if (mode === 'stamp') {
|
||||
const r = stampBriefReview(text);
|
||||
if (!r.stamped) {
|
||||
process.stderr.write(`brief-review-stamp: not stamped: ${r.reason}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const tmp = `${path}.tmp-${process.pid}`;
|
||||
writeFileSync(tmp, r.text);
|
||||
renameSync(tmp, path);
|
||||
process.stdout.write(`brief-review-stamp: stamped ${path} (${r.hash})\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
const r = checkBriefReview(text);
|
||||
if (args.includes('--json')) process.stdout.write(JSON.stringify(r) + '\n');
|
||||
else process.stdout.write(`brief-review-stamp: ${r.state} ${path}\n`);
|
||||
process.exit(r.state === 'current' ? 0 : 1);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue