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>
117 lines
5 KiB
JavaScript
117 lines
5 KiB
JavaScript
// 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);
|
||
}
|