fix: close the six defects an independent review found in the trial fixes
Independent read-only review of c56f852..2d29966 (2026-10-08), each defect reproduced by the reviewer; each new test fails on that code. - test-lock green ran ANY allowlisted command: `green --cmd "git status"` marked a red step green and the stop guard let it stop. The lock now stores the step's test command (`lock --cmd`); `green` runs only that one (a different --cmd, or a lock without one, exits 2). trekexecute D1 calls `green` with no command, which also removes the quoting hazard. - plan-gate --accept-open worked at round 1 and passed the gate; it now refuses while rounds are left. - findings-schema --repo-root swallowed a following flag, or took a root that does not exist, and silently skipped the line check: exit 2. - brief-review-stamp hashed only the body, so a changed `framing` or `research_topics` stayed "current"; it now covers the reviewed frontmatter too (only the post-review stamps are left out). - red-proof red_kind read an assertion message naming SyntaxError as a load error; the signatures are now specific. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2d29966f51
commit
5f9faeed4a
12 changed files with 192 additions and 35 deletions
|
|
@ -10,10 +10,13 @@
|
|||
// 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
|
||||
// `stale` (the brief 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.
|
||||
// The hash covers the body AND the frontmatter the reviewer judges
|
||||
// (`framing`, `research_topics`, ...): an independent review showed a
|
||||
// body-only hash staying current after `framing` changed. Only the stamps
|
||||
// written after the review are left out (this one and the intent approval
|
||||
// marker); 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
|
||||
|
|
@ -40,10 +43,15 @@ function split(text) {
|
|||
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')}`;
|
||||
// Frontmatter written AFTER the review; everything else is what was reviewed.
|
||||
const POST_REVIEW_FIELDS = Object.freeze([REVIEWED_HASH_FIELD, REVIEWED_AT_FIELD, 'intent_approved_hash', 'intent_approved_at']);
|
||||
|
||||
const normText = (t) => String(t ?? '').replace(/\r\n?/g, '\n').split('\n').map((l) => l.replace(/\s+$/, '')).join('\n').replace(/\n+$/, '');
|
||||
|
||||
/** sha256 over the reviewed frontmatter + the body: CRLF -> LF, trailing whitespace dropped. */
|
||||
export function computeReviewHash(front, body) {
|
||||
const kept = normText(front).split('\n').filter((l) => !POST_REVIEW_FIELDS.some((f) => l.startsWith(`${f}:`))).join('\n');
|
||||
return `sha256:${createHash('sha256').update(`${kept}\n---\n${normText(body)}`, 'utf8').digest('hex')}`;
|
||||
}
|
||||
|
||||
function fieldValue(front, name) {
|
||||
|
|
@ -56,7 +64,7 @@ function fieldValue(front, name) {
|
|||
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 hash = computeReviewHash(parts.front, 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()}"`);
|
||||
|
|
@ -70,7 +78,7 @@ 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);
|
||||
const actual = computeReviewHash(parts.front, parts.body);
|
||||
if (!stamped) return { state: 'absent', stamped: null, actual };
|
||||
return { state: stamped === actual ? 'current' : 'stale', stamped, actual };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@
|
|||
// After the operator's explicit yes to "build as is": records the open
|
||||
// MAJORs of the current round as accepted (`accepted_open` in the verdict
|
||||
// block, bound to the round and the plan hash). 0 = written; 1 = refused
|
||||
// (a BLOCKER is open, no MAJOR is open, or the verdict is stale); 2 = could
|
||||
// not run.
|
||||
// (rounds left, a BLOCKER is open, no MAJOR is open, or the verdict is
|
||||
// stale); 2 = could not run.
|
||||
//
|
||||
// Open MAJORs (operator decision 2026-10-08, voyage-lekerepo trial point 6):
|
||||
// they used to pass as warnings, and after the last round there was no way to
|
||||
|
|
@ -271,6 +271,10 @@ export function acceptOpen(rawText, opts = {}) {
|
|||
if (v.findings.some(f => sevOf(f) !== 'major' && sevOf(f) !== 'minor')) return { code: 1, message: 'a BLOCKER is open; a blocker is never accepted' };
|
||||
const majors = v.findings.filter(f => sevOf(f) === 'major');
|
||||
if (majors.length === 0) return { code: 1, message: 'no MAJOR is open; nothing to accept' };
|
||||
// Only the last round: with rounds left the answer is to revise, not accept
|
||||
// (found by the independent review: acceptance at round 1 passed the gate).
|
||||
const left = v.max_rounds + (v.extra_rounds ?? 0) - v.round;
|
||||
if (left > 0) return { code: 1, message: `${left} critic round(s) left: revise the plan and run the next round; acceptance is only for after the last round` };
|
||||
const accepted = {
|
||||
...v,
|
||||
accepted_open: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue