fix: round 7 of the release review of 6.3.0, for 6.3.1

Round 7 (516dffa..45aa9e1, WARN: 12 MAJOR, 0 BLOCKER) and the fence defect
found while writing its review.md. Each fix began with a test that failed on
0f400d3 (v6.3.0); suite 2208 (2206/0/2), 14 tests more.

- Tests-only (operator 2026-10-11): the plan's Files: alone decides, and the
  text says so (red-proof, trekexecute Sub-step B, step-implementer,
  HANDOVER-CONTRACTS). red-proof no longer reads the step's diff, so a change
  already in the tree when the step began no longer counts against it, and a
  plan placed outside .claude/ reads the same. The round-6 test that had the
  diff overrule the plan is rewritten to the new rule.
- The Files: reader keeps a plain file beside a backticked path
  (", Dockerfile", "the CLI entry src/cli.mjs", "and Makefile"). Measured on
  the 520 Files: fields in 182 plans: no field reads differently.
- Red proof reports a command its timeout killed as killed after the timeout,
  not "could not start"; the release check tells a timeout from output past
  its buffer and from another signal. One reading of a stop
  (criteria-runner stoppedBy) for red proof, the criteria runner and the
  release check.
- A trailing `# comment` is dropped in a backticked check too, and in a
  command passed as written, so the words screened are the words run.
- The release check reads a relative --review in --repo.
- The 15-minute timeout is defined once (criteria-runner.mjs).
- The findings check reads each cited file once per validation.
- One isMain helper (lib/util/is-main.mjs) for every CLI: the 31 copies of
  the realpath guard and the 20 modules still on the file:// guard, which
  exited 0 having done nothing under a symlink or a path with a space.
- The Bash guard writes its no-one-to-ask refusal once, on a direct condition.
- The findings check ends a ```json block only at a fence on its own line, so
  three backticks inside a JSON string no longer end it (also in 6.2.0).
- Brief-conformance notes: a test for test-lock green on exit 127 (it
  passed at once; it fails when 127 is read as could-not-run); the prose now
  names the git revision `@~1` as the one leading-@ exception.

Not changed: the example plan's steps 1-2 keep their tests-only warning
(operator decision 2026-10-09; the r5 test records it). Brief note 4 (a
Regression: whose runner is not installed goes to the fix rounds) is a
routing question for the operator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-11 09:11:57 +02:00
commit 29b4ac91b3
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
65 changed files with 582 additions and 485 deletions

View file

@ -10,6 +10,7 @@
import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { issue } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
const CANONICAL_OVERVIEW = 'overview.md';
const CANONICAL_GAPS = 'gaps.md';
@ -76,7 +77,7 @@ export function discoverArchitecture(projectDir) {
return result;
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const projectDir = process.argv[2];
const wantJson = process.argv.includes('--json');
if (!projectDir) {

View file

@ -29,8 +29,8 @@
// 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';
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
import { isMain } from '../util/is-main.mjs';
export const REVIEWED_HASH_FIELD = 'brief_reviewed_hash';
export const REVIEWED_AT_FIELD = 'brief_reviewed_at';
@ -83,15 +83,7 @@ export function checkBriefReview(text) {
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()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const mode = args.includes('--stamp') ? 'stamp' : args.includes('--check') ? 'check' : null;
const path = args.find((a) => !a.startsWith('--'));

View file

@ -6,12 +6,12 @@
// optional fields (jf. the source_findings precedent on trekreview) can be
// added without a brief_version bump.
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync, existsSync } from 'node:fs';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue, ok, fail } from '../util/result.mjs';
import { BASE_ALLOWED_MODELS } from './profile-validator.mjs';
import { parseSuccessCriteria, runnableCheck } from '../verification/criteria-runner.mjs';
import { isMain } from '../util/is-main.mjs';
export const BRIEF_REQUIRED_FRONTMATTER = ['type', 'brief_version', 'task', 'slug', 'research_topics', 'research_status'];
export const REVIEW_AS_BRIEF_REQUIRED_FRONTMATTER = ['type', 'task', 'slug', 'project_dir', 'findings'];
@ -328,15 +328,7 @@ export function validateBrief(filePath, opts = {}) {
return { ...r, parsed: { ...r.parsed, filePath } };
}
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const strict = !args.includes('--soft');
const minIdx = args.indexOf('--min-version');

View file

@ -39,13 +39,13 @@
// what `/trekbrief --approve` approves: {brief_path, project_dir, plan_command}; exit 1 = no brief
import { createHash } from 'node:crypto';
import { existsSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
import { basename, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue } from '../util/result.mjs';
import { emit } from '../stats/event-emit.mjs';
import { resolveStatsDir } from '../stats/stats-append.mjs';
import { isMain } from '../util/is-main.mjs';
export const INTENT_HASH_FIELD = 'intent_approved_hash';
export const INTENT_APPROVED_AT_FIELD = 'intent_approved_at';
@ -234,18 +234,7 @@ export function resolveApproveTarget(arg) {
return { brief_path: briefPath, project_dir: projectDir, plan_command: planCommand };
}
// Main-module guard on real paths: comparing against `file://` + argv[1] fails
// when the plugin sits behind a symlink or in a path with a space; the CLI then
// exits 0 having checked nothing.
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const file = args.find((a) => !a.startsWith('--'));
if (args.includes('--resolve')) {

View file

@ -11,6 +11,7 @@
import { readFileSync, existsSync } from 'node:fs';
import { issue, fail } from '../util/result.mjs';
import { splitFrontmatter, parseFrontmatter } from '../util/frontmatter.mjs';
import { isMain } from '../util/is-main.mjs';
export const NEXT_SESSION_PROMPT_REQUIRED_FIELDS = Object.freeze(['produced_by', 'produced_at']);
@ -150,7 +151,7 @@ export function validateNextSessionPromptConsistency(a, b, opts = {}) {
return { valid: errors.length === 0, errors, warnings, parsed: { a: aFm || null, b: bFm || null } };
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const positionals = args.filter(a => !a.startsWith('--'));
const wantJson = args.includes('--json');

View file

@ -54,12 +54,12 @@
// The verdict is a trace of a process, not a signature: anything that can
// write the plan can write the block.
import { readFileSync, existsSync, realpathSync, writeFileSync, renameSync, rmSync } from 'node:fs';
import { readFileSync, existsSync, writeFileSync, renameSync, rmSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { validatePlanContent } from './plan-validator.mjs';
import { splitSections, fenceTracker, extractPlanVersion, versionAtLeast } from '../parsers/plan-schema.mjs';
import { normalizeSeverity } from '../review/plan-review-dedup.mjs';
import { isMain } from '../util/is-main.mjs';
export const MAX_ROUNDS = 3;
export const VERDICT_SCHEMA = 'voyage.plan-verdict/1';
@ -343,19 +343,11 @@ export function gatePlan(rawText) {
// ---- CLI --------------------------------------------------------------------
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
function out(obj) {
process.stdout.write(JSON.stringify(obj) + '\n');
}
if (isMain()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const usage = 'Usage: plan-gate.mjs <plan.md> [--json] | --hash <plan.md> | --record <plan.md> --reviewed <sha256> [--extra-round] < dedup.json | --accept-open <plan.md>';
const known = new Set(['--json', '--hash', '--record', '--reviewed', '--extra-round', '--accept-open']);

View file

@ -34,8 +34,7 @@
// warning PLAN_STEP_TESTS_ONLY: its tests pass at its start point, so it can
// never be proven red (voyage-lekerepo trial 3, 2026-10-09).
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync, existsSync } from 'node:fs';
import {
sliceSteps, validatePlanHeadings, extractPlanVersion, extractPlanCeremony,
parseStepFields, findSection, splitSections, findModuleCycle, parseMermaid, fencedBlocks,
@ -46,6 +45,7 @@ import { validateAllManifests } from '../parsers/manifest-yaml.mjs';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue, fail } from '../util/result.mjs';
import { parsePlanVerification, firstCommand, runnableCheck } from '../verification/criteria-runner.mjs';
import { isMain } from '../util/is-main.mjs';
export const OPTIONAL_STEP_FIELDS = Object.freeze(['Interface in', 'Interface out', 'Review focus']);
export const DESIGN_SUBSECTIONS = Object.freeze([
@ -371,17 +371,7 @@ export function validatePlan(filePath, opts = {}) {
return { ...r, parsed: { ...r.parsed, filePath } };
}
// realpath on both sides: `file://${argv[1]}` silently no-ops under a symlink
// or a path with a space (the CLI then exits 0 having checked nothing).
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const strict = !args.includes('--soft');
const at = args.indexOf('--steps-from');

View file

@ -28,6 +28,7 @@
import { readFileSync, existsSync } from 'node:fs';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue, ok, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
export const PROFILE_REQUIRED_FIELDS = Object.freeze([
'name',
@ -175,7 +176,7 @@ export function validateProfile(filePath, opts = {}) {
return { ...r, parsed: { ...(r.parsed || {}), filePath } };
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const filePath = args.find(a => !a.startsWith('--'));
if (!filePath) {

View file

@ -13,9 +13,9 @@
// simplify, review) and the key of plan_verification.failed_criteria: the runner
// says `exitCode`, progress says `exit_code`, and only `exit_code` is accepted (C15).
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync, existsSync } from 'node:fs';
import { issue, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
export const PROGRESS_REQUIRED_TOP = ['schema_version', 'plan', 'plan_version', 'mode', 'status', 'total_steps', 'current_step', 'steps'];
export const PROGRESS_VALID_STATUSES = ['pending', 'in_progress', 'completed', 'failed', 'partial', 'stopped'];
@ -212,17 +212,7 @@ export function validateProgress(filePath, opts = {}) {
return validateProgressContent(text, opts);
}
// The real paths on both sides: a `file://` comparison fails when the path holds a
// space or runs through a symlink, and the CLI then exits 0 having checked nothing.
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const filePath = args.find(a => !a.startsWith('--'));
if (!filePath) {

View file

@ -19,6 +19,7 @@
// → JSON {valid, errors, warnings}; exit 0 valid, 1 invalid.
import { issue } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
// WARN tier — absolute filesystem paths (leaks local directory layout).
export const ABSOLUTE_PATH_PATTERNS = Object.freeze([
@ -105,7 +106,7 @@ export function validateOutboundQuery(text, opts = {}) {
// ---- CLI shim ----------------------------------------------------------------
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const strict = !args.includes('--soft');
const text = args.find(a => !a.startsWith('--'));

View file

@ -5,6 +5,7 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
export const RESEARCH_REQUIRED_FRONTMATTER = ['type', 'created', 'question'];
export const RESEARCH_BODY_SECTIONS = ['Executive Summary', 'Dimensions'];
@ -77,7 +78,7 @@ export function validateResearchDir(dirPath, opts = {}) {
return { valid: errors.length === 0, errors, warnings, parsed: { files: results } };
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const strict = !args.includes('--soft');
const dirIdx = args.indexOf('--dir');

View file

@ -13,10 +13,10 @@
// gate that decides on the verdict is lib/review/verdict-gate.mjs, which never
// reads a missing verdict as ALLOW.
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync, existsSync } from 'node:fs';
import { parseDocument } from '../util/frontmatter.mjs';
import { issue, ok, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
export const REVIEW_REQUIRED_FRONTMATTER = [
'type',
@ -180,15 +180,7 @@ export function validateReview(filePath, opts = {}) {
return { ...r, parsed: { ...r.parsed, filePath } };
}
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const strict = !args.includes('--soft');
const filePath = args.find(a => !a.startsWith('--'));

View file

@ -4,6 +4,7 @@
import { readFileSync, existsSync } from 'node:fs';
import { issue, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
export const SESSION_STATE_REQUIRED_TOP = [
'schema_version',
@ -98,7 +99,7 @@ export function validateSessionState(filePath, opts = {}) {
return validateSessionStateContent(text, opts);
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const filePath = args.find(a => !a.startsWith('--'));
if (!filePath) {