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

@ -4,6 +4,8 @@
// Each command has its own valid-flag set; passing flags from another command
// produces an `unknown_flags` array but does not error — the caller decides.
import { isMain } from '../util/is-main.mjs';
const FLAG_SCHEMA = {
trekbrief: {
boolean: ['--quick', '--fg'],
@ -140,7 +142,7 @@ const REQUIRED = {
// CLI: node lib/parsers/arg-parser.mjs --command <command> -- $ARGUMENTS
// stdout: the parse result as JSON. exit 0, or exit 1 with each error as
// "Error: <message>" plus the usage line on stderr.
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const argv = process.argv.slice(2);
const ci = argv.indexOf('--command');
const command = ci >= 0 ? argv[ci + 1] : undefined;

View file

@ -293,19 +293,22 @@ export function onFailureAction(text) {
*
* - A field with a backticked path names its backticked spans that pass the
* path test (a `.`, `/` or `\`; no URL, option or call like `parse()`),
* wherever they sit (`a.mjs`/`b.mjs`, inside a note), and a backticked name
* with no extension that stands alone in its entry (`Makefile`). Its descriptions are
* not read: "covers pass/fail", "JSON, YAML" or a directory named in passing
* is no path. An entry of its own with no backticks (", .gitignore") counts
* by its first word, and only when that is clearly a file or a directory.
* wherever they sit (`a.mjs`/`b.mjs`, inside a note), a backticked build
* file (`Makefile`, `Dockerfile`), and a backticked capitalised name with no
* extension that stands alone in its entry. Beside them a plain word counts
* only when it is clearly a file or a directory: a name with an extension, a
* dotfile, a path with a slash that ends in a file or a `/`, or a build file
* (", Dockerfile", "the CLI entry src/cli.mjs", "and Makefile"; round 7: they
* were dropped, and a step that builds something read as tests-only).
* "covers pass/fail", "JSON, YAML" or "v6.3.0 format" is no path.
* - A field with no backticked path names every path-like word: a name with an
* extension, a dotfile, a path with a slash, a directory (`docs/`) or a glob
* (`src/*.mjs`); a parenthesised note is not read. A word with no slash and
* no extension counts only alone in its entry and capitalised (`Makefile`);
* "format" or "v6.3.0" in a description is no path.
* extension, a dotfile, a path with a slash, a directory (`docs/`), a glob
* (`src/*.mjs`) or a build file; a parenthesised note is not read. Another
* word with no slash and no extension counts only alone in its entry and
* capitalised; "format" or "v6.3.0" in a description is no path.
*
* A backslash is read as a slash, a leading `./` is dropped. Release review of
* 6.3.0, rounds 1-5.
* 6.3.0, rounds 1-7.
*/
const SEG = '[\\w@.*?()-]';
const PATH_WORD = new RegExp(`^(?:${SEG}+\\/)+${SEG}*$|^[\\w@*?-][\\w@.*?-]*\\.[A-Za-z*][A-Za-z0-9*]*$|^\\.[\\w-][\\w.-]*$`);
@ -314,37 +317,46 @@ const NOT_A_PATH = /^(?:e\.g|i\.e|n\.b|cf|etc|vs)$/i;
const BARE_NAME = /^[A-Z][\w.-]*$/;
// The last segment is a dotfile or a name with an extension, or the path is a directory.
const FILE_OR_DIR = /(?:^|\/)(?:\.[\w-][\w.-]*|[^/]*\.[A-Za-z*][A-Za-z0-9*]*)$|\/$/;
// A build file named without an extension: a capitalised name that ends in
// `file` (Makefile, Dockerfile, Gemfile, Jenkinsfile, ...), not the word Profile.
const BUILD_FILE = /^(?!Profile$)(?:[A-Z][a-z]+|GNUmake)file$/;
const lastSegment = (p) => p.replace(/\/$/, '').split('/').pop();
const norm = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '');
// A backticked span that is a path, not a URL, an option or a call (`parse()`, `p.parse()`).
const tickedPath = (s) => /[./\\]/.test(s) && !/^https?:/.test(s) && !s.startsWith('-') && !/^[\w$.]+\(.*\)$/.test(s);
// A plain word without the brackets and punctuation around it, `\` read as `/`.
const plainWord = (w) => w.replace(/^[(\[]+/, '').replace(/[.,;:)\]]+$/, '').replace(/\\/g, '/');
const isPathWord = (w) => PATH_WORD.test(w) && !NOT_A_PATH.test(w);
// Clearly a file or a directory: what a plain word beside backticked paths must be.
const isFileWord = (w) => BUILD_FILE.test(w) || (isPathWord(w) && (FILE_OR_DIR.test(w) || BUILD_FILE.test(lastSegment(w))));
export function fieldPaths(text) {
const spans = [];
const masked = String(text ?? '')
.replace(/`([^`\n]+)`/g, (_, p) => `\u0000${spans.push(p.trim()) - 1}\u0000`)
.replace(/(^|\s)\(([^()\n]*)\)/g, (m, pre, inner) => (inner.includes('\u0000') ? m : `${pre} `));
const isTicked = spans.map(tickedPath);
const isTicked = spans.map((p) => tickedPath(p) || BUILD_FILE.test(p));
const tickedField = isTicked.some(Boolean);
const out = [];
for (const raw of masked.split(/[,\n]/)) {
const entry = raw.trim().replace(/^[-*]\s+/, '');
if (entry === '') continue;
const inEntry = [...entry.matchAll(/\u0000(\d+)\u0000/g)].map((m) => Number(m[1]));
const ticked = inEntry.filter((i) => isTicked[i]);
if (ticked.length) { out.push(...ticked.map((i) => spans[i])); continue; }
// A backticked `Makefile` alone in its entry is a file (round 6).
if (inEntry.length === 1 && /^\u0000\d+\u0000$/.test(entry) && BARE_NAME.test(spans[inEntry[0]])) { out.push(spans[inEntry[0]]); continue; }
const words = entry.split(/\s+/).filter((w) => !w.includes('\u0000')).map(plainWord).filter(Boolean);
const tokens = entry.split(/\s+/);
// A backticked capitalised name alone in its entry is a file (round 6).
const alone = /^\u0000(\d+)\u0000$/.exec(entry);
if (alone && BARE_NAME.test(spans[Number(alone[1])])) { out.push(spans[Number(alone[1])]); continue; }
if (tickedField) {
const first = plainWord(entry.split(/\s+/)[0]);
if (!first.includes('\u0000') && isPathWord(first) && FILE_OR_DIR.test(first)) out.push(first);
// In the order they stand: the backticked paths, and a plain word only
// when it is clearly a file or a directory (round 7).
for (const t of tokens) {
if (t.includes('\u0000')) out.push(...[...t.matchAll(/\u0000(\d+)\u0000/g)].map((m) => Number(m[1])).filter((i) => isTicked[i]).map((i) => spans[i]));
else if (isFileWord(plainWord(t))) out.push(plainWord(t));
}
continue;
}
const found = words.filter(isPathWord);
if (found.length === 0 && words.length === 1 && entry.split(/\s+/).length === 1 && BARE_NAME.test(words[0])) found.push(words[0]);
const words = tokens.filter((w) => !w.includes('\u0000')).map(plainWord).filter(Boolean);
const found = words.filter((w) => isPathWord(w) || BUILD_FILE.test(w));
if (found.length === 0 && words.length === 1 && tokens.length === 1 && BARE_NAME.test(words[0])) found.push(words[0]);
out.push(...found);
}
return out.filter(Boolean).map(norm);
@ -377,10 +389,10 @@ export function stepTestsOnly(body) {
/**
* Whether a path counts as a test for this step: its own `Test first:` file, or
* a path that is clearly a test. red-proof.mjs uses it on the step's diff.
* a path that is clearly a test.
* @returns {(path: string) => boolean}
*/
export function stepTestFile(f) {
function stepTestFile(f) {
const testFirst = f.has('Test first')
? [...f.get('Test first').text.matchAll(/\bFile:\**[ \t]*`([^`\n]+)`/gi)].map((m) => norm(m[1].trim()))
: [];

View file

@ -29,6 +29,7 @@
import { readFileSync, existsSync } from 'node:fs';
import { issue, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
// Origin tag for every synthesised finding (Phase 7 rule 7: codebase vs research).
export const ORIGIN_VALUES = Object.freeze(['codebase', 'research']);
@ -160,7 +161,7 @@ export function validateAgentOutput(rawText) {
// ---- CLI shim ----------------------------------------------------------------
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,6 +13,7 @@ import { readFileSync, existsSync } from 'node:fs';
import { parseDocument } from '../util/frontmatter.mjs';
import { PHASE_SIGNAL_PHASES, EFFORT_LEVELS } from '../validators/brief-validator.mjs';
import { BASE_ALLOWED_MODELS } from '../validators/profile-validator.mjs';
import { isMain } from '../util/is-main.mjs';
/**
* Resolve a brief's phase_signal entry for one phase.
@ -72,7 +73,7 @@ export function resolvePhaseSignalFromFile(briefPath, phase) {
// CLI (`resolver.mjs --resolve-phase-model`, brief > profile > default) is the
// single resolution source for {effort, model}. Do not re-wire commands/*.md
// Bash blocks back to this shim.
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const getArg = (name) => {
const i = args.indexOf(name);

View file

@ -42,6 +42,7 @@ import { homedir } from 'node:os';
import { parseDocument } from '../util/frontmatter.mjs';
import { validateProfile } from '../validators/profile-validator.mjs';
import { resolvePhaseSignal } from './phase-signal-resolver.mjs';
import { isMain } from '../util/is-main.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const BUILTIN_PROFILES_DIR = __dirname; // lib/profiles/
@ -297,7 +298,7 @@ export function resolvePhaseModel(phase, briefPath, argv, env = process.env) {
// CLI shim — invoked by commands/trek*.md via Bash.
// Usage: node lib/profiles/resolver.mjs --resolve-phase-model --phase plan --brief-path foo.md --json
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
if (args.includes('--resolve-phase-model')) {
const getArg = (name) => {

View file

@ -34,8 +34,7 @@
import { SEVERITY_VALUES, RULE_KEYS, getRule } from './rule-catalogue.mjs';
import { computeFindingId } from '../parsers/finding-id.mjs';
import { validateFindings } from './findings-schema.mjs';
import { realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { isMain } from '../util/is-main.mjs';
export const JUDGE_TITLE_MAX = 100;
export const JUDGE_DETAIL_MAX = 800;
@ -460,15 +459,7 @@ export function runContract(reviewerPayloads, options = {}) {
// ---- CLI shim ----------------------------------------------------------------
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

@ -43,11 +43,11 @@
// CLI: node lib/review/engine-findings.mjs [--scope <file-list.txt>] [--repo-root <dir>] <engine-findings.json>
// exit 0 = payload on stdout; 1 = input invalid (errors on stderr); 2 = could not run.
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync, existsSync } from 'node:fs';
import { computeFindingId } from '../parsers/finding-id.mjs';
import { RULE_KEYS, getRule, SEVERITY_VALUES } from './rule-catalogue.mjs';
import { validateFindings } from './findings-schema.mjs';
import { isMain } from '../util/is-main.mjs';
export const ENGINES = Object.freeze({
'code-review': Object.freeze({
@ -146,15 +146,7 @@ export function toReviewerPayload(input, opts = {}) {
return { ok: true, errors: [], payload, out_of_scope };
}
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 valueOf = (name) => { const i = args.indexOf(name); return i === -1 ? null : args[i + 1]; };
const scopePath = valueOf('--scope');

View file

@ -16,6 +16,7 @@ import { parseDocument } from '../util/frontmatter.mjs';
import { jaccardSimilarity } from '../parsers/jaccard.mjs';
import { computeFindingId } from '../parsers/finding-id.mjs';
import { extractFindingsBlock } from './findings-schema.mjs';
import { isMain } from '../util/is-main.mjs';
export const DEFAULT_JACCARD_TOLERANCE = 0.7;
@ -152,7 +153,7 @@ export function fidelityDiffStructured(armA, armB, opts = {}) {
// ---- CLI shim ----------------------------------------------------------------
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const { readFileSync } = await import('node:fs');
const args = process.argv.slice(2);
const files = args.filter((x) => !x.startsWith('--'));

View file

@ -18,10 +18,10 @@
// 1 = unresolved placeholder / wrong id (errors on stderr, file untouched);
// 2 = could not run (unreadable file, no JSON block).
import { readFileSync, writeFileSync, renameSync, realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync, writeFileSync, renameSync } from 'node:fs';
import { computeFindingId } from '../parsers/finding-id.mjs';
import { extractFindingsBlock } from './findings-schema.mjs';
import { isMain } from '../util/is-main.mjs';
const PLACEHOLDER = /@@ID_(\d+)@@/g;
const HEX40 = /^[0-9a-f]{40}$/;
@ -66,15 +66,7 @@ export function fillFindingIds(text) {
return { ok: true, text: out, errors: [] };
}
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const path = process.argv[2];
if (!path) {
process.stderr.write('Usage: fill-finding-ids.mjs <review.md>\n');

View file

@ -21,11 +21,11 @@
//
// 3-layer pattern (Content → Raw-text → CLI shim) mirroring the other validators.
import { readFileSync, existsSync, realpathSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync, existsSync, statSync } from 'node:fs';
import { resolve, sep } from 'node:path';
import { issue, fail } from '../util/result.mjs';
import { RULE_KEYS, SEVERITY_VALUES } from './rule-catalogue.mjs';
import { isMain } from '../util/is-main.mjs';
// The fields main + the coordinator depend on. Descriptive fields are not here
// on purpose: a missing recommended_action should not trigger a re-ask.
@ -38,7 +38,12 @@ export const FINDING_REQUIRED_FIELDS = Object.freeze([
// Last fenced ```json … ``` block in a reviewer's output. The contract pins the
// JSON block as the LAST fence so prose above it never confuses the parser.
const JSON_FENCE_GLOBAL = /```json[ \t]*\r?\n([\s\S]*?)```/gi;
// Both fences stand on a line of their own, as in Markdown and in the verdict
// gate's reader: a JSON string cannot hold a line break, so three backticks
// inside one ("a fenced ```bash block") never end the block. Ending it at the
// first triple backtick anywhere made fill-finding-ids exit 2 (release review
// of 6.3.0, found while writing round 7's review.md; present in 6.2.0).
const JSON_FENCE_GLOBAL = /^[ \t]*```json[ \t]*\r?\n([\s\S]*?)^[ \t]*```[ \t]*\r?$/gim;
/**
* Extract the inner body of the LAST fenced `json` block in `text`.
@ -109,8 +114,7 @@ function validateFinding(finding, index, errors) {
// and the claim itself was right. With a repo root, a line past the end of a
// file that exists is an error, so Phase 5's bounded re-ask fixes it before the
// coordinator ever sees it. A file absent from the tree is not judged here.
function lineCount(repoRoot, file) {
const root = resolve(repoRoot);
function readLineCount(root, file) {
const abs = resolve(root, file);
if (abs !== root && !abs.startsWith(root + sep)) return null;
try {
@ -123,9 +127,24 @@ function lineCount(repoRoot, file) {
}
}
function checkLineRange(finding, index, repoRoot, errors) {
/**
* Line counts for one validation, each file read once however often it is
* cited: a reviewer that cited the 3,000-line commands/trekexecute.md 40
* times read it 40 times, again on every re-ask (release review of 6.3.0,
* round 7). The map lives for one call, so an edited file is read afresh.
*/
function lineCounter(repoRoot) {
const root = resolve(repoRoot);
const counts = new Map();
return (file) => {
if (!counts.has(file)) counts.set(file, readLineCount(root, file));
return counts.get(file);
};
}
function checkLineRange(finding, index, lineCount, errors) {
if (!finding || typeof finding.file !== 'string' || !Number.isInteger(finding.line) || finding.line <= 0) return;
const n = lineCount(repoRoot, finding.file);
const n = lineCount(finding.file);
if (n === null || finding.line <= n) return;
errors.push(issue(
'FINDING_LINE_OUT_OF_RANGE',
@ -147,14 +166,14 @@ function checkLineRange(finding, index, repoRoot, errors) {
const PROSE_CITE = /(?<![\w./\\:-])((?:\.{0,2}\/)?[\w.-]+(?:\/[\w.-]+)*\.[A-Za-z0-9]+):(\d+)(?:[-–—](\d+))?(?![\w])/g;
const PROSE_FIELDS = Object.freeze(['title', 'detail', 'recommended_action', 'brief_ref']);
function checkProseCitations(text, where, repoRoot, errors, seen) {
function checkProseCitations(text, where, lineCount, errors, seen) {
if (typeof text !== 'string' || text === '') return;
PROSE_CITE.lastIndex = 0;
let m;
while ((m = PROSE_CITE.exec(text)) !== null) {
const line = Math.max(Number(m[2]), m[3] === undefined ? 0 : Number(m[3]));
if (line <= 0) continue;
const n = lineCount(repoRoot, m[1]);
const n = lineCount(m[1]);
if (n === null || line <= n) continue;
const key = `${where}\u0000${m[0]}`;
if (seen.has(key)) continue;
@ -169,10 +188,10 @@ function checkProseCitations(text, where, repoRoot, errors, seen) {
}
}
function checkFindingProse(finding, index, repoRoot, errors, seen) {
function checkFindingProse(finding, index, lineCount, errors, seen) {
if (!finding || typeof finding !== 'object') return;
for (const field of PROSE_FIELDS) {
checkProseCitations(finding[field], `findings[${index}].${field}`, repoRoot, errors, seen);
checkProseCitations(finding[field], `findings[${index}].${field}`, lineCount, errors, seen);
}
}
@ -182,6 +201,11 @@ function checkFindingProse(finding, index, repoRoot, errors, seen) {
* each finding's `file:line`, and every `path:line` in its prose fields
*/
export function validateFindings(payload, opts = {}) {
return validatePayload(payload, opts.repoRoot ? lineCounter(opts.repoRoot) : null);
}
// One line counter for one validation: the JSON block and the prose around it share it.
function validatePayload(payload, lineCount) {
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
return fail(issue('FINDINGS_NOT_OBJECT', `Reviewer output must be a JSON object, got ${Array.isArray(payload) ? 'array' : typeof payload}`));
}
@ -201,9 +225,9 @@ export function validateFindings(payload, opts = {}) {
const seen = new Set();
for (let i = 0; i < payload.findings.length; i++) {
validateFinding(payload.findings[i], i, errors);
if (opts.repoRoot) {
checkLineRange(payload.findings[i], i, opts.repoRoot, errors);
checkFindingProse(payload.findings[i], i, opts.repoRoot, errors, seen);
if (lineCount) {
checkLineRange(payload.findings[i], i, lineCount, errors);
checkFindingProse(payload.findings[i], i, lineCount, errors, seen);
}
}
@ -232,8 +256,9 @@ export function validateReviewerOutput(rawText, opts = {}) {
} catch (e) {
return fail(issue('FINDINGS_PARSE_ERROR', `Reviewer JSON block did not parse: ${e.message}`));
}
const r = validateFindings(parsed, opts);
if (!opts.repoRoot) return r;
const lineCount = opts.repoRoot ? lineCounter(opts.repoRoot) : null;
const r = validatePayload(parsed, lineCount);
if (!lineCount) return r;
// The prose around the block (the text outside the LAST json fence; the
// block itself was checked field by field above) is a WARNING, not an error:
// the bounded re-ask asks for the JSON block only, which cannot correct that
@ -241,7 +266,7 @@ export function validateReviewerOutput(rawText, opts = {}) {
// review over text no one uses (independent review, 2026-10-08).
const at = rawText.lastIndexOf(block);
const found = [];
checkProseCitations(rawText.slice(0, at) + rawText.slice(at + block.length), 'reviewer prose', opts.repoRoot, found, new Set());
checkProseCitations(rawText.slice(0, at) + rawText.slice(at + block.length), 'reviewer prose', lineCount, found, new Set());
return found.length === 0 ? r : { ...r, warnings: [...r.warnings, ...found] };
}
@ -251,15 +276,7 @@ function isDir(p) {
try { return statSync(p).isDirectory(); } catch { return false; }
}
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 rootIdx = args.indexOf('--repo-root');
const repoRoot = rootIdx >= 0 ? args[rootIdx + 1] : undefined;

View file

@ -34,10 +34,10 @@
// blocks here. Malformed stdin exits NON-ZERO — a broken hand-off must surface
// loudly, never collapse into a silent empty merge.
import { readFileSync, realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import { jaccardSimilarity, meetsThreshold } from '../parsers/jaccard.mjs';
import { computeFindingId } from '../parsers/finding-id.mjs';
import { isMain } from '../util/is-main.mjs';
export const DEFAULT_THRESHOLD = 0.7;
export const SEVERITIES = Object.freeze(['blocker', 'major', 'minor']);
@ -282,17 +282,7 @@ function readStdinSourcesOrExit() {
];
}
// realpath on both sides: `file://${argv[1]}` silently no-ops under a symlink
// or a path with a space.
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 = parseArgs(process.argv.slice(2));
const sources = args.stdin
? readStdinSourcesOrExit()

View file

@ -41,10 +41,10 @@ import {
} from 'node:fs';
import { join, relative, dirname } from 'node:path';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { validateReviewContent } from '../validators/review-validator.mjs';
import { extractFindingsBlock } from './findings-schema.mjs';
import { getRule } from './rule-catalogue.mjs';
import { isMain } from '../util/is-main.mjs';
export const LESSONS_DIR = join('.claude', 'rules');
export const LESSONS_FILE_NAME = 'voyage-review-lessons.md';
@ -347,20 +347,12 @@ export function applyLesson(repo, ruleKey, opts = {}) {
// ---- CLI --------------------------------------------------------------------
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
function flag(args, name) {
const i = args.indexOf(name);
return i === -1 ? undefined : args[i + 1];
}
if (isMain()) {
if (isMain(import.meta.url)) {
const [cmd, ...args] = process.argv.slice(2);
const repo = flag(args, '--repo');
const out = (o) => process.stdout.write(JSON.stringify(o, null, 2) + '\n');

View file

@ -26,10 +26,10 @@
// `templates/checklist-template.md:8-9`, MIT). The blocking itself is Voyage's
// own design — neither source blocks on its own.
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { readFileSync, existsSync } from 'node:fs';
import { validateReviewContent, VERDICT_VALUES } from '../validators/review-validator.mjs';
import { extractFindingsBlock } from './findings-schema.mjs';
import { isMain } from '../util/is-main.mjs';
export const EXIT = Object.freeze({ PASS: 0, BLOCK: 1, UNDECIDED: 2 });
@ -168,15 +168,7 @@ export function decideVerdictFile(filePath) {
return decideVerdict(text);
}
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 files = args.filter((a) => !a.startsWith('--'));
const filePath = files[0];

View file

@ -34,10 +34,11 @@
// Osmani. Reimplemented, no code copied; the reader here takes multi-line
// (`description: |`) descriptions, which the original reads as "|".
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { descriptionOf, readFrontmatterFields } from './skill-frontmatter.mjs';
import { isMain } from '../util/is-main.mjs';
export const THRESHOLDS = Object.freeze({
COLLISION_ERROR: 0.75,
@ -280,12 +281,7 @@ export function checkRouting(root, t = THRESHOLDS) {
// ---- CLI ----
function isMain() {
if (!process.argv[1]) return false;
try { return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]); } catch { return false; }
}
if (isMain()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const at = args.indexOf('--root');
const root = resolve(at >= 0 ? args[at + 1] || '' : join(dirname(fileURLToPath(import.meta.url)), '..', '..'));

View file

@ -40,10 +40,11 @@
// docs/skill-anatomy.md - MIT, Copyright (c) 2025 Addy Osmani. Reimplemented,
// no code copied.
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFrontmatterFields, splitFile } from './skill-frontmatter.mjs';
import { isMain } from '../util/is-main.mjs';
/** Claude Code's skill frontmatter fields (skills.md, Frontmatter reference). */
export const KNOWN_FIELDS = new Set([
@ -227,12 +228,7 @@ export function lintSkillsRoot(pluginRoot) {
// ---- CLI ----
function isMain() {
if (!process.argv[1]) return false;
try { return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]); } catch { return false; }
}
if (isMain()) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const at = args.indexOf('--root');
const root = resolve(at >= 0 ? args[at + 1] || '' : join(dirname(fileURLToPath(import.meta.url)), '..', '..'));

View file

@ -9,6 +9,7 @@
// Zero npm dependencies. Node stdlib only.
import { readFileSync, existsSync } from 'node:fs';
import { isMain } from '../util/is-main.mjs';
function usage() {
return `cache-analyzer.mjs — summarize trekexecute-stats.jsonl
@ -123,7 +124,7 @@ export function summarizeFile(path) {
return summarize(text.split('\n'));
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = process.argv.slice(2);
const jsonIdx = args.indexOf('--json');
if (jsonIdx === -1 || !args[jsonIdx + 1]) {

View file

@ -30,10 +30,10 @@
// Import direction: event-emit imports stats-append; stats-append must never
// import event-emit (that would be a cycle).
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveStatsDir } from './stats-append.mjs';
import { isMain } from '../util/is-main.mjs';
export const KNOWN_EVENTS = Object.freeze(new Set([
'brief-approved',
@ -110,17 +110,7 @@ function parseArgs(argv) {
return out;
}
// Run as a CLI? Compared as real paths: `file://${argv[1]}` misses a path with a
// space (URL-encoded) and a symlink (import.meta.url is the realpath).
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 = parseArgs(process.argv.slice(2));
let result;
if (!args.event) {

View file

@ -23,11 +23,11 @@
// JSON
// → prints {"written":true,"path":...} exit 0 | {"written":false,"reason":...} exit 1
import { appendFileSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { attributeOrder } from './claimed-order.mjs';
import { isMain } from '../util/is-main.mjs';
export const STATS_KINDS = Object.freeze([
'trekbrief', 'trekresearch', 'trekplan', 'trekexecute', 'trekreview', 'trekcontinue',
@ -68,17 +68,7 @@ export function appendStatsRecord(kind, record, env = process.env, { cwd = proce
return { written: true, path };
}
// Run as a CLI? Compared as real paths: `file://${argv[1]}` misses a path with a
// space (URL-encoded) and a symlink (import.meta.url is the realpath).
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const kind = process.argv[2];
let record = null;
let reason = null;

View file

@ -45,7 +45,7 @@ import {
appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmdirSync, statSync,
} from 'node:fs';
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isMain } from './is-main.mjs';
export const APPROVAL_KINDS = Object.freeze(['intent', 'plan', 'review', 'merge', 'release']);
export const LOG_RELPATH = '.claude/voyage/approvals.jsonl';
@ -278,15 +278,7 @@ function shaFromArgs(args) {
return { sha: raw === undefined ? undefined : normalizeHex(raw), raw };
}
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const [cmd, ...args] = process.argv.slice(2);
if (cmd === 'answer') {
let text = opt(args, '--text');

View file

@ -21,8 +21,8 @@
import { spawnSync } from 'node:child_process';
import { appendFileSync, existsSync, readFileSync, realpathSync } from 'node:fs';
import { isAbsolute, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { LOG_RELPATH, logPath, normalizeSubject } from './approval-log.mjs';
import { isMain } from './is-main.mjs';
export const ARTIFACT_STORES = Object.freeze(['git', 'local']);
const DEFAULT_MESSAGE = 'docs(voyage): record approval';
@ -85,15 +85,7 @@ export function commitArtifacts({ repo, store, artifacts = [], message = DEFAULT
return { ...res, exit: 0, committed: true, commit: git(repo, ['rev-parse', 'HEAD']).stdout.trim() };
}
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 opt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
const artifacts = args.flatMap((a, i) => (a === '--artifact' && args[i + 1] !== undefined ? [args[i + 1]] : []));

View file

@ -22,6 +22,8 @@
//
// Pure data; no I/O. Re-entry to `completed` is idempotent.
import { isMain } from './is-main.mjs';
export const STATES = Object.freeze({
IDLE: 'idle',
GATES_ON: 'gates_on',
@ -114,7 +116,7 @@ function parseArgs(argv) {
return out;
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = parseArgs(process.argv.slice(2));
if (!args.state || !args.event) {
process.stdout.write(JSON.stringify({

26
lib/util/is-main.mjs Normal file
View file

@ -0,0 +1,26 @@
// lib/util/is-main.mjs
// Whether a module is the script node was started with, so a module can be both
// a library and a CLI: `if (isMain(import.meta.url)) { ... }`.
//
// Real paths on both sides. The older guard, import.meta.url === `file://${argv[1]}`,
// is false under a symlinked path (import.meta.url is the realpath) and under a
// path with a space (URL-encoded), and the CLI then exits 0 having done nothing
// (measured 2026-10-04). One helper instead of a copy per CLI (release review of
// 6.3.0, round 7: 31 copies of the fixed guard and 20 of the old one).
import { realpathSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
/**
* @param {string} url - the calling module's import.meta.url
* @param {string|undefined} [argv1] - the started script (process.argv[1])
* @returns {boolean}
*/
export function isMain(url, argv1 = process.argv[1]) {
if (!argv1) return false;
try {
return realpathSync(argv1) === realpathSync(fileURLToPath(url));
} catch {
return false;
}
}

View file

@ -42,6 +42,7 @@
import { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
import { isMain } from './is-main.mjs';
export const MAX_CONV_TURNS = 3;
export const MAX_TOTAL_DIMENSIONS = 8; // settings.json:16 maxDimensions — whole list, not discovered-only
@ -325,7 +326,7 @@ function parseArgs(argv) {
return out;
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = parseArgs(process.argv.slice(2));
// The dimension ceiling is a Phase 4.5 concern, and Phase 4.5 never calls the

View file

@ -29,6 +29,7 @@
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, rmSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join, dirname, basename, extname, isAbsolute } from 'node:path';
import { isMain } from './is-main.mjs';
function sectionBody(text, heading) {
const lines = text.split('\n');
@ -301,7 +302,7 @@ const NEEDS = {
'record-merged': ['plan', 'wave'], 'next-wave': ['plan'], clear: [],
};
if (import.meta.url === `file://${process.argv[1]}`) {
if (isMain(import.meta.url)) {
const args = parseArgs(process.argv.slice(2));
const usage = 'usage: session-orders.mjs write|status|archive|record-merged --plan <repo-relative plan> --wave <W> --session-dir <dir> [--branch <b>]\n'
+ ' session-orders.mjs next-wave --plan <repo-relative plan> --session-dir <dir>\n'

View file

@ -37,14 +37,13 @@
// Credit: locking tests during a fix is Claude Code's own hook pattern
// (hooks-guide "Block edits to protected files").
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, realpathSync, readdirSync } from 'node:fs';
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync } from 'node:fs';
import { join, resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { resolveRepo, canonicalPath } from '../verification/repo-tools.mjs';
import { allowedCommand, runCommand, screenCommand } from '../verification/criteria-runner.mjs';
import { DEFAULT_TIMEOUT_MS } from '../verification/red-proof.mjs';
import { DEFAULT_TIMEOUT_MS, allowedCommand, runCommand, screenCommand } from '../verification/criteria-runner.mjs';
import { isMain } from './is-main.mjs';
export const LOCK_VERSION = 1;
@ -225,12 +224,4 @@ function main(argv) {
process.exit(0);
}
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) main(process.argv.slice(2));
if (isMain(import.meta.url)) main(process.argv.slice(2));

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) {

View file

@ -46,7 +46,7 @@ import {
} from 'node:fs';
import { spawnSync } from 'node:child_process';
import { join, resolve, dirname, basename, relative, isAbsolute, delimiter, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isMain } from '../util/is-main.mjs';
const TIMEOUT_MS = 180_000;
const DEPCRUISE_CONFIGS = ['.dependency-cruiser.cjs', '.dependency-cruiser.js', '.dependency-cruiser.mjs', '.dependency-cruiser.json'];
@ -255,14 +255,6 @@ export function proveRule({ repo, targets = [], plants, rule }) {
// ---- CLI --------------------------------------------------------------------
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
function parseArgs(argv) {
const o = { targets: [], plants: [], prove: false, repo: null, rule: null, bad: null };
const value = (i, flag) => {
@ -287,7 +279,7 @@ function parseArgs(argv) {
return o;
}
if (isMain()) {
if (isMain(import.meta.url)) {
const o = parseArgs(process.argv.slice(2));
let result;
if (o.bad || !o.repo) {

View file

@ -56,8 +56,12 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
export const PLAN_HEADING = '## Verification';
export const BRIEF_HEADING = '## Success Criteria';
export const DEFAULT_HOOK = join(REPO_ROOT, 'hooks', 'scripts', 'pre-bash-executor.mjs');
// The one timeout every check Voyage runs is stopped by (15 minutes): red
// proof, the done line, the test lock, Phase 7 criteria, regression-check and
// the release check's suite (release review of 6.3.0, round 7).
export const DEFAULT_TIMEOUT_MS = 900_000;
export const DEFAULT_MAX_OUTPUT = 4000;
const MAX_BUFFER = 64 * 1024 * 1024;
// --- parsing ----------------------------------------------------------------
@ -151,24 +155,35 @@ export function looksLikeCommand(span) {
export function firstCommand(text) {
const m = text.match(/`([^`]+)`/);
if (!m) return { command: null, reason: 'no-command' };
const raw = m[1].trim();
const raw = dropComment(m[1]);
// Template placeholders (`{exact command}`) are not commands.
if (raw === '' || /^\{.*\}$/.test(raw)) return { command: null, reason: 'placeholder' };
if (!looksLikeCommand(raw)) return { command: null, reason: 'not-a-command' };
return { command: raw, reason: '' };
}
/**
* A command without its trailing comment (`npm test # all green`): a shell
* drops it, and with no shell (runCommand) its words would reach the screen
* and make an honest check NOT RUN (release review of 6.3.0, round 6). One
* reading for every place a command comes from: a fence line, a backticked
* span (`Verify:`, `Regression:`, `End-to-end:`, a `## Verification` bullet)
* and a command passed as written (red proof, the done line, the test lock),
* so the words screened are the words run (round 7). Only a `#` that opens a
* word on the same line is a comment (`tests/a#b.test.mjs` is a path).
*/
export function dropComment(command) {
return String(command ?? '').replace(/[ \t]+#[^\n]*$/, '').trim();
}
// A command line inside a shell-tagged fence. A console block may prefix the
// line with a `$` or `>` prompt; blank and comment-only lines declare nothing.
// A `#` root prompt is NOT stripped: it is indistinguishable from a comment,
// and running a comment is the worse of the two mistakes. A trailing comment
// (`npm test # all green`) is not part of the command: a shell dropped it,
// and with no shell (runCommand) its words would reach the screen and make an
// honest check NOT RUN (release review of 6.3.0, round 6).
// and running a comment is the worse of the two mistakes.
function fencedCommand(text) {
const line = text.trim();
if (line === '' || line.startsWith('#')) return null;
return line.replace(/^[$>]\s+/, '').replace(/\s+#.*$/, '');
return dropComment(line.replace(/^[$>]\s+/, ''));
}
function parseSection(markdown, heading, prefix) {
@ -233,7 +248,7 @@ export function parseSuccessCriteria(markdown) {
export function screenCommand(command, opts = {}) {
const hookPath = opts.hookPath ?? DEFAULT_HOOK;
const r = spawnSync(process.execPath, [hookPath], {
input: JSON.stringify({ tool_name: 'Bash', tool_input: { command } }),
input: JSON.stringify({ tool_name: 'Bash', tool_input: { command: dropComment(command) } }),
encoding: 'utf8',
timeout: 30_000,
});
@ -456,8 +471,8 @@ export function runnableCheck(criterion, opts = {}) {
function screenShape(command, opts, writeTime) {
const cwd = resolve(opts.cwd ?? process.cwd());
const text = String(command ?? '');
if (text.trim() === '') return deny('the command is empty');
const text = dropComment(command);
if (text === '') return deny('the command is empty');
if (/\n/.test(text)) return deny('a newline: one criterion declares one command');
const operator = text.match(SHELL_OPERATORS);
if (operator) return deny(`a shell operator (${operator[0]}) — one criterion declares one command, unpiped`);
@ -492,7 +507,7 @@ function screenShape(command, opts, writeTime) {
/** The words of a command: what the screen checks is exactly what runs. */
export function commandWords(command) {
return String(command ?? '').trim().split(/\s+/);
return dropComment(command).split(/\s+/);
}
/**
@ -505,7 +520,29 @@ export function commandWords(command) {
*/
export function runCommand(command, { cwd, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
const [program, ...args] = commandWords(command);
return spawnSync(program, args, { cwd, env: commandEnv(), encoding: 'utf8', timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
return spawnSync(program, args, { cwd, env: commandEnv(), encoding: 'utf8', timeout: timeoutMs, maxBuffer: MAX_BUFFER });
}
/**
* Why a spawnSync run has no exit code of its own, or null when it has one.
* spawnSync sets `error` AND `signal` together when it kills the program
* itself: ETIMEDOUT for the timeout, ENOBUFS for output past `maxBuffer`. So
* the error code is read first, and a bare `error` with no signal is a program
* that never started (not installed). Red proof called a timeout "could not
* start" and the release check called an overflow a timeout (release review of
* 6.3.0, round 7). `started` is false only for a program that never ran.
* @returns {{started: boolean, why: string}|null}
*/
export function stoppedBy(r, { timeoutMs = DEFAULT_TIMEOUT_MS, maxBuffer = MAX_BUFFER } = {}) {
const signal = r.signal ?? 'SIGTERM';
if (r.error?.code === 'ETIMEDOUT') return { started: true, why: `was killed (${signal}) after the ${Math.round(timeoutMs / 1000)} s timeout` };
if (r.error?.code === 'ENOBUFS') {
const size = maxBuffer >= 1024 * 1024 ? `${Math.round(maxBuffer / (1024 * 1024))} MB` : `${maxBuffer} bytes`;
return { started: true, why: `was killed (${signal}): its output passed the ${size} that is read from it` };
}
if (r.signal) return { started: true, why: `was killed (${r.signal})` };
if (r.error || r.status === null) return { started: false, why: `could not start: ${r.error?.message ?? 'no exit code'}` };
return null;
}
// --- running ----------------------------------------------------------------
@ -515,8 +552,9 @@ export function runCommand(command, { cwd, timeoutMs = DEFAULT_TIMEOUT_MS } = {}
// One killed by the timeout did start, and a hang is a red (`failed`).
function defaultExec(command, { cwd, timeoutMs }) {
const r = runCommand(command, { cwd, timeoutMs });
if (r.status === null && r.signal === null && r.error) return { status: null, stdout: '', stderr: '', startError: r.error.message };
const why = r.signal ? `killed (${r.signal})\n` : '';
const stopped = stoppedBy(r, { timeoutMs });
if (stopped && !stopped.started) return { status: null, stdout: '', stderr: '', startError: r.error?.message ?? stopped.why };
const why = stopped ? `${stopped.why}\n` : '';
return { status: r.status, stdout: r.stdout ?? '', stderr: `${r.stderr ?? ''}${why}` };
}

View file

@ -20,12 +20,12 @@
// skills/executing-plans/scripts/task-done: the completion line is appended
// only when the test command exits 0.
import { appendFileSync, existsSync, readFileSync, realpathSync } from 'node:fs';
import { appendFileSync, existsSync, readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { allowedCommand, runCommand, screenCommand } from './criteria-runner.mjs';
import { DEFAULT_TIMEOUT_MS, repoRoot } from './red-proof.mjs';
import { DEFAULT_TIMEOUT_MS, allowedCommand, runCommand, screenCommand } from './criteria-runner.mjs';
import { repoRoot } from './red-proof.mjs';
import { isMain } from '../util/is-main.mjs';
export function doneLineRe(step, commit) {
const sha = commit ? String(commit).replace(/[^0-9a-f]/g, '') : '[0-9a-f]{40}';
@ -100,9 +100,4 @@ export function main(argv) {
return r.status === 'written' ? 0 : r.status === 'failed' ? 1 : 2;
}
function isMain() {
try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); }
catch { return false; }
}
if (process.argv[1] && isMain()) process.exitCode = main(process.argv.slice(2));
if (isMain(import.meta.url)) process.exitCode = main(process.argv.slice(2));

View file

@ -32,9 +32,9 @@
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { resolve, relative, dirname, isAbsolute, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sliceSteps, parseStepFields } from '../parsers/plan-schema.mjs';
import { parseManifest } from '../parsers/manifest-yaml.mjs';
import { isMain } from '../util/is-main.mjs';
// Files Voyage's own process writes while planning and executing.
export const PROCESS_FILES = Object.freeze([
@ -174,15 +174,7 @@ export function sweep({ plan, base, repo = process.cwd(), process: extra = [] })
// ---- CLI --------------------------------------------------------------------
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const argv = process.argv.slice(2);
const o = { process: [] };
let bad = null;

View file

@ -15,9 +15,9 @@
// run, and only the full run is authoritative.
//
// `--plan <plan.md> --step <N>` adds `tests_only` to a `not-red-at-base`: true
// when every file in the step's `Files:` is a test and so is every file the
// step changed since its start point, so the step can never be red and goes
// up to the plan (see `testsOnly` below).
// when every file in the step's `Files:` is a test, so the step can never be
// red and goes up to the plan. The plan's `Files:` alone decides (see
// `testsOnly` below).
//
// Exit 0 = proven, 1 = rejected (`not-red-at-base` or `not-green-at-result`),
// 2 = could not run (`not-run`: no test files, an unknown start point, a command
@ -55,16 +55,17 @@
// Borrowed: GitHub Spec Kit (MIT, github/spec-kit@2e941fe) - its test-first
// rule is an instruction there; here a command proves it.
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { allowedCommand, runCommand, screenCommand } from './criteria-runner.mjs';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { DEFAULT_TIMEOUT_MS, allowedCommand, runCommand, screenCommand, stoppedBy } from './criteria-runner.mjs';
import { commandEnv } from '../util/command-env.mjs';
import { parseStepFields, sliceSteps, stepTestFile, stepTestsOnlyFromFields } from '../parsers/plan-schema.mjs';
import { parseStepFields, sliceSteps, stepTestsOnlyFromFields } from '../parsers/plan-schema.mjs';
import { isMain } from '../util/is-main.mjs';
export const DEFAULT_TIMEOUT_MS = 900_000;
// The one timeout, defined in criteria-runner.mjs (release review of 6.3.0, round 7).
export { DEFAULT_TIMEOUT_MS };
const TAIL = 2000;
function tap(output) {
@ -96,7 +97,8 @@ const LOAD_ERROR = /^\s*SyntaxError: |ERR_MODULE_NOT_FOUND|does not provide an e
function run(cmd, cwd, timeoutMs) {
const r = runCommand(cmd, { cwd, timeoutMs });
const output = `${r.stdout ?? ''}${r.stderr ?? ''}`;
return { exit: r.status, signal: r.signal ?? null, tap: tap(output), tail: output.slice(-TAIL), spawnError: r.error ? r.error.message : null, loadError: LOAD_ERROR.test(output) };
const stopped = stoppedBy(r, { timeoutMs });
return { exit: r.status, signal: r.signal ?? null, tap: tap(output), tail: output.slice(-TAIL), spawnError: r.error ? r.error.message : null, stopped: stopped?.why ?? null, loadError: LOAD_ERROR.test(output) };
}
/** What the red at the start point was: the test file failing to load, or a test failing. */
@ -112,42 +114,31 @@ const notRun = (reason, extra = {}) => ({ status: 'not-run', reason, ...extra })
// (voyage-lekerepo trial 3, 2026-10-09). `null` = the plan or the step could
// not be read; without `--plan` the field is absent and nothing new happens.
//
// The plan's `Files:` is prose, read by a reader that can misread it; the
// step's own diff is a fact. So a step the plan calls tests-only is so only
// when every file it changed since the start point is a test too (release
// review of 6.3.0, round 6). Before building (`--red-only`) the diff holds the
// step's test alone, so there the plan decides. The plan file itself and
// Voyage's own files under .claude/ (progress, approvals) are not the step's change.
function testsOnly(o, cwd, baseSha) {
// The plan's `Files:` alone decides (operator 2026-10-11): the step's diff is
// not read. In the documented flow the helper writes only the test before this
// gate, so the diff could never overrule the plan, and a change already in the
// tree when the step began (a dirty tree is allowed, trekexecute Sub-step 0)
// made a true tests-only step look like it built something (release review of
// 6.3.0, round 7). A path the step changed outside `Files:` is the
// controller's scope finding (trekexecute Sub-step C, Hard rule 2), not this check's.
function testsOnly(o) {
if (!o.plan) return {};
let text;
try { text = readFileSync(o.plan, 'utf8'); }
catch (e) { return { tests_only: null, tests_only_note: `cannot read the plan: ${e.message}` }; }
const step = sliceSteps(text).find((s) => Number(s.n) === Number(o.step));
if (!step) return { tests_only: null, tests_only_note: `no "### Step ${o.step}:" in the plan` };
const fields = parseStepFields(step.body);
if (!stepTestsOnlyFromFields(fields).testsOnly) return { tests_only: false };
const isTest = stepTestFile(fields);
const given = new Set(o.tests ?? []);
const changed = [
...lines(git(cwd, ['diff', '--name-only', '--no-renames', baseSha]).stdout),
...lines(git(cwd, ['ls-files', '--others', '--exclude-standard']).stdout),
];
const planRel = relative(cwd, realpathSync(o.plan)).split(sep).join('/');
const other = [...new Set(changed)].filter((p) => p !== planRel && !p.startsWith('.claude/') && !given.has(p) && !isTest(p));
if (other.length === 0) return { tests_only: true };
return { tests_only: false, tests_only_note: `the plan's Files: names only tests, but the step's diff also changes ${other.slice(0, 5).join(', ')}` };
return { tests_only: stepTestsOnlyFromFields(parseStepFields(step.body)).testsOnly };
}
// A program that could not start (a spawn error: not installed) or a signal
// (the timeout) is not a test verdict at all. With no shell, an exit 126/127
// A program that could not start (a spawn error: not installed), or one this
// run stopped (the timeout, output past the buffer, a signal), is not a test
// verdict at all (criteria-runner `stoppedBy`). With no shell, an exit 126/127
// is the test program's own (a script whose tool the step has not built yet):
// a red like any other, and the control run tells an environment fault apart
// (release review of 6.3.0, round 6).
function unrunnable(r) {
if (r.spawnError) return `the command could not start: ${r.spawnError}`;
if (r.exit === null) return `the command was killed (${r.signal ?? 'timeout'})`;
return null;
return r.stopped ? `the command ${r.stopped}` : null;
}
const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
@ -241,7 +232,7 @@ export function proveRed(o) {
const baseBroken = unrunnable(base);
if (baseBroken) return notRun(`at the start point: ${baseBroken}`, { base, result, linked });
const out = { base_sha: baseSha, linked, base, result };
if (base.exit === 0) return { status: 'rejected', reason: 'not-red-at-base', ...testsOnly(o, cwd, baseSha), ...out };
if (base.exit === 0) return { status: 'rejected', reason: 'not-red-at-base', ...testsOnly(o), ...out };
if (o.redOnly) return { status: 'proven', reason: 'red at the start point (--red-only: the result was not run)', red_kind: redKind(base), base_sha: baseSha, linked, base };
if (result.exit !== 0) return { status: 'rejected', reason: 'not-green-at-result', ...out };
@ -249,6 +240,8 @@ export function proveRed(o) {
// copy itself cannot run the test green, so its red proved nothing.
applyResult(cwd, tree, baseSha);
const control = run(o.cmd, tree, timeoutMs);
const controlBroken = unrunnable(control);
if (controlBroken) return notRun(`in the control run: ${controlBroken}`, { ...out, control });
if (control.exit !== 0) {
return notRun('the control run (start-point copy + the change) stayed red: the start point failed for an environment reason, not for the missing change', { ...out, control });
}
@ -302,9 +295,4 @@ export function main(argv) {
return r.status === 'proven' ? 0 : r.status === 'rejected' ? 1 : 2;
}
function isMain() {
try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); }
catch { return false; }
}
if (process.argv[1] && isMain()) process.exitCode = main(process.argv.slice(2));
if (isMain(import.meta.url)) process.exitCode = main(process.argv.slice(2));

View file

@ -34,11 +34,11 @@
// left K10 regressions running differently from Phase 7 criteria (release
// review of 6.3.0, round 5).
import { readFileSync, realpathSync } from 'node:fs';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sliceSteps, parseStepFields } from '../parsers/plan-schema.mjs';
import { firstCommand, runCriteria } from './criteria-runner.mjs';
import { isMain } from '../util/is-main.mjs';
export const REGRESSION_FIELD = 'Regression';
const EXIT = Object.freeze({ passed: 0, absent: 0, failed: 1, blocked: 1, unrunnable: 1, error: 2 });
@ -99,12 +99,4 @@ export function main(argv) {
return say(checkRegression({ planText, step, cwd }));
}
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) process.exitCode = main(process.argv.slice(2));
if (isMain(import.meta.url)) process.exitCode = main(process.argv.slice(2));

View file

@ -28,7 +28,7 @@
import { existsSync, readFileSync, statSync, realpathSync, accessSync, constants } from 'node:fs';
import { dirname, join, resolve, isAbsolute, extname, delimiter } from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { isMain } from '../util/is-main.mjs';
// ---------------------------------------------------------------------------
// The repository
@ -248,15 +248,7 @@ export function runTool(tool, files, { cwd, timeout = 120_000, env = process.env
// CLI
// ---------------------------------------------------------------------------
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const i = process.argv.indexOf('--repo');
const start = i !== -1 ? process.argv[i + 1] : null;
if (!start) {

View file

@ -23,13 +23,13 @@
// Credit: the tiers and "not measured" are agent-skills' constraint-driven
// development (MIT, `9d0c60d`); the stop at step end is Cherny's and Kiro's.
import { existsSync, realpathSync } from 'node:fs';
import { existsSync } from 'node:fs';
import { join, extname } from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { resolveRepo, detectTools, runTool } from './repo-tools.mjs';
import { latestPostEditRecords } from '../../hooks/lib/post-edit.mjs';
import { runContract } from '../../hooks/lib/contract-cli.mjs';
import { isMain } from '../util/is-main.mjs';
// NUL-separated and unquoted, so a name with spaces or non-ASCII letters
// comes back as it is on disk (git quotes such names by default).
@ -105,15 +105,7 @@ export function stepCheck({ cwd, full = false }) {
return { status, root: repo.root, full, files, not_covered: notCovered, checks };
}
function isMain() {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
}
if (isMain()) {
if (isMain(import.meta.url)) {
const argv = process.argv.slice(2);
const i = argv.indexOf('--cwd');
const cwd = i !== -1 ? argv[i + 1] : null;

View file

@ -23,11 +23,11 @@
// constraint-driven development - mechanized here as a diff reader so a
// command, not the model's word, says whether a test was weakened.
import { readFileSync, realpathSync } from 'node:fs';
import { readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isTestPath } from '../util/test-path.mjs';
import { isMain } from '../util/is-main.mjs';
export { isTestPath };
@ -275,9 +275,4 @@ function out(r, json) {
return r.status === 'clean' ? 0 : r.status === 'findings' ? 1 : 2;
}
function isMain() {
try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); }
catch { return false; }
}
if (process.argv[1] && isMain()) process.exitCode = main(process.argv.slice(2));
if (isMain(import.meta.url)) process.exitCode = main(process.argv.slice(2));