vigiles 30.0.1 → 31.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,8 +14,7 @@ exports.assertAdapterLoadsHooks = assertAdapterLoadsHooks;
14
14
  */
15
15
  const node_fs_1 = require("node:fs");
16
16
  const node_path_1 = require("node:path");
17
- const compile_js_1 = require("./core/compile.js");
18
- const spec_js_1 = require("./core/spec.js");
17
+ const tool_contract_js_1 = require("./core/tool-contract.js");
19
18
  const plugin_loader_js_1 = require("./plugin-loader.js");
20
19
  const vocabulary_consistency_js_1 = require("./core/vocabulary-consistency.js");
21
20
  const event_capability_js_1 = require("./core/event-capability.js");
@@ -190,21 +189,16 @@ function checkAdapterConformance(adapter) {
190
189
  catch (e) {
191
190
  need(false, `layout.settings ("${adapter.layout.settings.label}") threw on its own output: ${String(e)}`);
192
191
  }
193
- // Behavioural: the dialect drives the compiler — its own built-in tool must
194
- // pass the subagent tool-contract check under this dialect.
192
+ // Behavioural: the dialect drives the subagent tool-contract check — its own built-in tool
193
+ // must pass that check under this dialect. This calls the SAME validator `compileAgent` uses
194
+ // (`verifyToolContract` + `authoringIssues`) rather than `compileAgent` itself: the tool
195
+ // contract is what this line claims to check, and compiling a whole agent would tie this
196
+ // synchronous public kit to the compiler's (async) reference validation for nothing.
195
197
  const tool = adapter.dialect.builtinAgentTools[0];
196
198
  if (tool) {
197
- const spec = (0, spec_js_1.experimental_agent)({
198
- name: "conformance",
199
- description: "conformance probe",
200
- tools: [tool],
201
- body: "probe",
202
- });
203
- const r = (0, compile_js_1.compileAgent)(spec, {
204
- specFile: "conformance.md.spec.ts",
205
- dialect: adapter.dialect,
206
- });
207
- need(!r.errors.some((e) => e.type === "unknown-tool"), `dialect rejects its own built-in tool "${tool}"`);
199
+ const issues = (0, tool_contract_js_1.authoringIssues)((0, tool_contract_js_1.verifyToolContract)([tool], adapter.dialect));
200
+ need(issues.length === 0, `dialect rejects its own built-in tool "${tool}"` +
201
+ (issues.length ? `: ${issues.map((i) => i.message).join("; ")}` : ""));
208
202
  }
209
203
  return { ok: failures.length === 0, failures };
210
204
  }
@@ -37,7 +37,19 @@ export interface ScriptRunResult {
37
37
  */
38
38
  export declare const SKIP_EXIT_CODE = 77;
39
39
  /**
40
- * Classify one script's run from its exit code and its reported check count.
40
+ * What the runner's load hook saw of one script (`harness-resolve-hooks.mts`).
41
+ *
42
+ * `marked` — the hook saw the script's module as an ES module and planted a
43
+ * marker import at the top of it. `linked` — that marker evaluated, which ESM
44
+ * only does once the WHOLE import graph was found, parsed and linked.
45
+ */
46
+ export interface LoadEvidence {
47
+ readonly marked: boolean;
48
+ readonly linked: boolean;
49
+ }
50
+ /**
51
+ * Classify one script's run from its exit code, its reported check count and
52
+ * what the load hook saw.
41
53
  *
42
54
  * 🔴 THE FOURTH STATE, AND WHY. Exit codes answer "did it fail?", never "did it
43
55
  * do anything?". Measured 2026-08-08: a file whose whole body is
@@ -60,8 +72,36 @@ export declare const SKIP_EXIT_CODE = 77;
60
72
  * none of it says zero. This is the `undefined`-vs-`[]` distinction
61
73
  * `assertNoWrite` already draws: "nobody looked" must not read as "nothing
62
74
  * happened".
75
+ *
76
+ * 🔴 DID IT LOAD? A non-zero exit from a script whose module was marked and
77
+ * never linked is did-not-load: `"skip"` with the loader's exit code, which
78
+ * keeps its coverage (`fail` RETRACTS it, see `runsFromResults`). A module that
79
+ * never linked executed no assertion — the same "did not run" as a missing
80
+ * `claude`, arriving through a different door. MEASURED, twice on 2026-08-20: a
81
+ * container restore left an old `node_modules`, every harness in a consumer repo
82
+ * died on `Named export 'recordCheck' not found`, and its ledger fell from 48
83
+ * records to 34 and from 47 to 33 for surfaces nothing had touched. The run is
84
+ * still red: `cli-main.ts` fails on any skip the author did not declare.
85
+ *
86
+ * This used to be decided by grepping the child's OUTPUT for loader phrases,
87
+ * and that was wrong in the dangerous direction (#243): a harness that printed
88
+ * a hook transcript containing "Cannot find module", then failed an assertion,
89
+ * was read as did-not-load and kept its coverage. Output is no longer an input.
90
+ * Anything without a marker (CommonJS, an entry the hook never saw) can never
91
+ * claim did-not-load — it is a `fail`, the side that retracts rather than hides.
92
+ * A syntax error in the script itself also never links, so it too is
93
+ * did-not-load; that is the owner's call, not an accident.
94
+ */
95
+ export declare function statusFor(code: number, checks: number | undefined, load?: LoadEvidence): ScriptStatus;
96
+ /**
97
+ * Did this script fail to LOAD — as opposed to declaring a skip (exit 77)?
98
+ * The one predicate behind both the CLI's "never ran" failure and `--min`.
99
+ *
100
+ * Deliberately NOT a fifth `ScriptStatus`: coverage retraction reads the status
101
+ * as a bare string (`coverage-artifact.ts`), so a new member would start
102
+ * retracting silently with no type error.
63
103
  */
64
- export declare function statusFor(code: number, checks: number | undefined, output?: string): ScriptStatus;
104
+ export declare function loadFailed(r: ScriptRunResult): boolean;
65
105
  /** Filename extensions accepted for harness/eval scripts (JS and TS). */
66
106
  export declare const SCRIPT_EXTS: readonly ["mjs", "cjs", "js", "mts", "cts", "ts"];
67
107
  /** Glob suffix matching every accepted script extension, e.g. `harness`. */
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.canRunTypeScript = exports.detectNodeCaps = exports.SCRIPT_EXTS = exports.SKIP_EXIT_CODE = void 0;
4
4
  exports.statusFor = statusFor;
5
+ exports.loadFailed = loadFailed;
5
6
  exports.scriptGlob = scriptGlob;
6
7
  exports.interpreterArgs = interpreterArgs;
7
8
  exports.discoverScripts = discoverScripts;
@@ -36,7 +37,8 @@ const check_count_js_1 = require("../../check-count.js");
36
37
  */
37
38
  exports.SKIP_EXIT_CODE = 77;
38
39
  /**
39
- * Classify one script's run from its exit code and its reported check count.
40
+ * Classify one script's run from its exit code, its reported check count and
41
+ * what the load hook saw.
40
42
  *
41
43
  * 🔴 THE FOURTH STATE, AND WHY. Exit codes answer "did it fail?", never "did it
42
44
  * do anything?". Measured 2026-08-08: a file whose whole body is
@@ -59,74 +61,47 @@ exports.SKIP_EXIT_CODE = 77;
59
61
  * none of it says zero. This is the `undefined`-vs-`[]` distinction
60
62
  * `assertNoWrite` already draws: "nobody looked" must not read as "nothing
61
63
  * happened".
64
+ *
65
+ * 🔴 DID IT LOAD? A non-zero exit from a script whose module was marked and
66
+ * never linked is did-not-load: `"skip"` with the loader's exit code, which
67
+ * keeps its coverage (`fail` RETRACTS it, see `runsFromResults`). A module that
68
+ * never linked executed no assertion — the same "did not run" as a missing
69
+ * `claude`, arriving through a different door. MEASURED, twice on 2026-08-20: a
70
+ * container restore left an old `node_modules`, every harness in a consumer repo
71
+ * died on `Named export 'recordCheck' not found`, and its ledger fell from 48
72
+ * records to 34 and from 47 to 33 for surfaces nothing had touched. The run is
73
+ * still red: `cli-main.ts` fails on any skip the author did not declare.
74
+ *
75
+ * This used to be decided by grepping the child's OUTPUT for loader phrases,
76
+ * and that was wrong in the dangerous direction (#243): a harness that printed
77
+ * a hook transcript containing "Cannot find module", then failed an assertion,
78
+ * was read as did-not-load and kept its coverage. Output is no longer an input.
79
+ * Anything without a marker (CommonJS, an entry the hook never saw) can never
80
+ * claim did-not-load — it is a `fail`, the side that retracts rather than hides.
81
+ * A syntax error in the script itself also never links, so it too is
82
+ * did-not-load; that is the owner's call, not an accident.
62
83
  */
63
- function statusFor(code, checks, output) {
84
+ function statusFor(code, checks, load) {
64
85
  if (code === exports.SKIP_EXIT_CODE)
65
86
  return "skip";
66
- // 🔴 `checks === undefined` GUARDS THE TEXT MATCH, and it is the load-bearing
67
- // half. A script that REPORTED a count executed: the counter is written by an
68
- // exit handler that exists only once the module was linked and run
69
- // (`check-count.ts`), so the count is a STRUCTURAL fact about the child, while
70
- // `didNotLoad` is a guess about its text.
71
- //
72
- // MEASURED: `statusFor(1, 3, "<hook stderr: Cannot find module …>\nAssertionError")`
73
- // returned `"skip"` and the run exited 0 — a harness that ran, recorded three
74
- // checks and FAILED an assertion, reported as skipped. That is not an exotic
75
- // input: vigiles harnesses drive hooks and print their transcripts, so a
76
- // loader phrase in the output is ordinary EVIDENCE about the thing under test,
77
- // not a diagnosis of the harness. Watching the child from outside cannot tell
78
- // those apart. The count can, and it was already in hand.
79
- if (code !== 0)
80
- return checks === undefined && didNotLoad(output) ? "skip" : "fail";
87
+ if (code !== 0) {
88
+ // A reported count is written by an exit handler that exists only once the
89
+ // module ran (`check-count.ts`), so it overrules a missing marker file.
90
+ const neverLinked = load?.marked === true && !load.linked && checks === undefined;
91
+ return neverLinked ? "skip" : "fail";
92
+ }
81
93
  return checks === 0 ? "vacuous" : "pass";
82
94
  }
83
95
  /**
84
- * Did the script fail to LOAD, rather than fail?
85
- *
86
- * 🔴 The distinction is not cosmetic, because `fail` RETRACTS coverage while
87
- * `skip` does not (see the table on `runsFromResults`). The rule for `fail` —
88
- * "it ran and proved nothing, so it must not keep yesterday's green record
89
- * alive" — is right, and it does not describe a file the runtime never
90
- * evaluated. A module that cannot resolve executed no assertion; it is the same
91
- * "did not run" as a missing `claude` CLI, arriving through a different door.
92
- *
93
- * MEASURED, twice in one session on 2026-08-20: a container restore left an old
94
- * `node_modules` behind, every harness in the consumer repo died on `Named export
95
- * 'recordCheck' not found`, and the ledger dropped from 48 records to 34 and from
96
- * 47 to 33. Nothing about those surfaces had changed — the machine had.
97
- *
98
- * ⚠️ This does NOT make a broken environment quiet: a script classified here
99
- * fails the run by default (`cli-main.ts`, right after `anyFailed`), because a
100
- * skip the AUTHOR never declared is not a skip. What this classification buys is
101
- * only that a machine problem may not delete a measurement taken on a machine
102
- * that worked.
96
+ * Did this script fail to LOAD — as opposed to declaring a skip (exit 77)?
97
+ * The one predicate behind both the CLI's "never ran" failure and `--min`.
103
98
  *
104
- * 🔴 That default is new, and the sentence it replaces was false. It read
105
- * «`--no-skip` — which this repo's own CI passes — still fails the run».
106
- * Measured: `--no-skip` appears ZERO times under `.github/`, `package.json`,
107
- * `scripts/` and `.claude/`; CI passes `--min=14` and nothing else. The
108
- * safety net the non-fatal classification leaned on was never strung.
109
- *
110
- * Deliberately literal, and only the loader's own vocabulary: these strings come
111
- * from Node's module resolution, not from user code. A test that legitimately
112
- * asserts on one of them exits 0 or asserts, and never reaches here.
99
+ * Deliberately NOT a fifth `ScriptStatus`: coverage retraction reads the status
100
+ * as a bare string (`coverage-artifact.ts`), so a new member would start
101
+ * retracting silently with no type error.
113
102
  */
114
- function didNotLoad(output) {
115
- if (output === undefined || output === "")
116
- return false;
117
- return (output.includes("ERR_MODULE_NOT_FOUND") ||
118
- output.includes("Cannot find package") ||
119
- output.includes("Cannot find module") ||
120
- /SyntaxError: Named export '[^']*' not found/.test(output) ||
121
- // Same event, ESM spelling. Node phrases a missing named export one way for
122
- // a CommonJS target and another for an ES module, and only the first was
123
- // listed — so the 2026-08-20 class below still RETRACTED coverage whenever
124
- // the dependency happened to be ESM. Measured on Node 22:
125
- // CJS: SyntaxError: Named export 'recordCheck' not found. The requested module …
126
- // ESM: SyntaxError: The requested module './x.mjs' does not provide an export named 'recordCheck'
127
- /SyntaxError: The requested module '[^']*' does not provide an export named/.test(output) ||
128
- output.includes("ERR_UNSUPPORTED_DIR_IMPORT") ||
129
- output.includes("ERR_PACKAGE_PATH_NOT_EXPORTED"));
103
+ function loadFailed(r) {
104
+ return r.status === "skip" && r.code !== exports.SKIP_EXIT_CODE;
130
105
  }
131
106
  /** Filename extensions accepted for harness/eval scripts (JS and TS). */
132
107
  exports.SCRIPT_EXTS = ["mjs", "cjs", "js", "mts", "cts", "ts"];
@@ -240,6 +215,23 @@ function readCheckReport(path) {
240
215
  return undefined;
241
216
  }
242
217
  }
218
+ /**
219
+ * What the load hook left behind for one child. A missing or malformed
220
+ * `seenFile` means the hook never saw the script, so `marked` is false and the
221
+ * run can never be read as did-not-load.
222
+ */
223
+ function readLoadEvidence(seenFile, loadedFile) {
224
+ return { marked: readMarked(seenFile), linked: (0, node_fs_1.existsSync)(loadedFile) };
225
+ }
226
+ function readMarked(seenFile) {
227
+ try {
228
+ const seen = JSON.parse((0, node_fs_1.readFileSync)(seenFile, "utf8"));
229
+ return seen.marked === true;
230
+ }
231
+ catch {
232
+ return false;
233
+ }
234
+ }
243
235
  /**
244
236
  * Run each script as `node <file>` (or `node <entry> <file>`, see
245
237
  * {@link RunScriptsOptions}), inheriting stdio so the script's own report
@@ -293,6 +285,8 @@ async function runScripts(files, cwd, env = {}, opts = {}) {
293
285
  return;
294
286
  }
295
287
  const countFile = (0, node_path_1.join)(countDir, `${String(i)}.count`);
288
+ const seenFile = (0, node_path_1.join)(countDir, `${String(i)}.seen`);
289
+ const loadedFile = (0, node_path_1.join)(countDir, `${String(i)}.loaded`);
296
290
  // Resolve a harness's bare `vigiles` import from the CLI's OWN install, so
297
291
  // running the gate does not require installing the package into the
298
292
  // project — which, in a repo that already has a package.json, drags in the
@@ -301,7 +295,21 @@ async function runScripts(files, cwd, env = {}, opts = {}) {
301
295
  // fails, so a locally installed copy still wins.
302
296
  const selfRoot = (0, node_path_1.resolve)(__dirname, "..", "..", "..");
303
297
  const hook = (0, node_url_1.pathToFileURL)((0, node_path_1.join)(selfRoot, "dist", "harness-resolve-hooks.mjs")).href;
304
- const child = (0, node_child_process_1.spawn)("node", ["--import", hookImport(hook), ...argv], {
298
+ // 🔴 OUR HOOK IS REGISTERED BEFORE `--import tsx`, and the order is
299
+ // load-bearing. Hooks chain last-in-first-out, so this makes tsx the
300
+ // OUTER hook: it transpiles the source we already marked, and its source
301
+ // map describes that source. The other order was measured wrong: tsx
302
+ // emits each module on ONE line with an inline map, a prefix added after
303
+ // it shifts every generated column, and a stack frame on line 2 was
304
+ // reported on line 3 (Node 20.20 and 22.22). The format we see from the
305
+ // inner position is the same (`module` in a `type: module` scope,
306
+ // `commonjs` otherwise — also measured).
307
+ const probe = {
308
+ entryURL: scriptURL(cwd, file),
309
+ loadedFile,
310
+ seenFile,
311
+ };
312
+ const child = (0, node_child_process_1.spawn)("node", ["--import", hookImport(hook, probe), ...argv], {
305
313
  cwd,
306
314
  stdio: ["ignore", "pipe", "pipe"],
307
315
  env: {
@@ -321,7 +329,7 @@ async function runScripts(files, cwd, env = {}, opts = {}) {
321
329
  resolveRun({
322
330
  file,
323
331
  code,
324
- status: statusFor(code, report?.checks, output),
332
+ status: statusFor(code, report?.checks, readLoadEvidence(seenFile, loadedFile)),
325
333
  checks: report?.checks,
326
334
  ...(report ? { surfaces: report.surfaces } : {}),
327
335
  });
@@ -436,13 +444,30 @@ function formatScriptSummary(results) {
436
444
  return lines.join("\n");
437
445
  }
438
446
  /**
439
- * A `--import` argument that registers the resolver hook without a temp file:
447
+ * A `--import` argument that registers the harness hooks without a temp file:
440
448
  * a data: URL calling `module.register`. Inline because writing a shim into the
441
449
  * user's tree to run their tests would be a side effect the runner has no
442
- * business having.
450
+ * business having. The per-child probe rides in `data`, not the environment,
451
+ * so a process the script spawns does not inherit it.
443
452
  */
444
- function hookImport(hookHref) {
445
- const src = `import {register} from "node:module";register(${JSON.stringify(hookHref)});`;
453
+ function hookImport(hookHref, probe) {
454
+ const src = `import {register} from "node:module";register(${JSON.stringify(hookHref)},{data:${JSON.stringify(probe)}});`;
446
455
  return `data:text/javascript,${encodeURIComponent(src)}`;
447
456
  }
457
+ /**
458
+ * The URL Node will load the SCRIPT under — for `eval` too, where the process
459
+ * entry is `eval-entry.js` and the script is what it imports. ESM resolution
460
+ * realpaths file URLs, so a symlinked path must be realpathed here or the hook
461
+ * would never recognise it (which is safe — no marker, no did-not-load claim —
462
+ * but blind).
463
+ */
464
+ function scriptURL(cwd, file) {
465
+ const abs = (0, node_path_1.resolve)(cwd, file);
466
+ try {
467
+ return (0, node_url_1.pathToFileURL)((0, node_fs_1.realpathSync)(abs)).href;
468
+ }
469
+ catch {
470
+ return (0, node_url_1.pathToFileURL)(abs).href;
471
+ }
472
+ }
448
473
  //# sourceMappingURL=run-scripts.js.map
@@ -15,6 +15,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.formatSelectionReport = exports.assertNoCollision = exports.measureSelectionMatrix = exports.agent = exports.experimental_skill = exports.experimental_agent = exports.claudeAvailable = exports.parseClaudeRun = exports.buildClaudeArgs = exports.claudeCodeDriver = void 0;
18
+ // 🔴 PUBLIC ENTRY POINT `vigiles/claude-code` — every export here is a promise to users. The default for a
19
+ // symbol is INTERNAL. It is exported only if (a) a NAMED external consumer uses it, or (b) it is
20
+ // a deliberate extension point listed in STABILITY.md (the adapter kit is the example). "Might
21
+ // be useful" is neither. Review point: the diff of `api-surface/vigiles-claude-code.api.md`, which
22
+ // `npm run api:check` fails on.
18
23
  /**
19
24
  * `vigiles/claude-code` — the Claude Code-specific harness pieces a *different*
20
25
  * harness would swap out: the plugin/repo loader (reads real Claude Code plugin
package/dist/cli-main.js CHANGED
@@ -39,6 +39,7 @@ const scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
39
39
  const install_reader_js_1 = require("./core/install-reader.js");
40
40
  const plugin_declaration_js_1 = require("./plugin-declaration.js");
41
41
  const local_files_tracked_js_1 = require("./local-files-tracked.js");
42
+ const local_files_js_1 = require("./local-files.js");
42
43
  const scan_behavioral_js_1 = require("./scan-behavioral.js");
43
44
  const adapter_registry_js_1 = require("./adapter-registry.js");
44
45
  const skill_harness_js_1 = require("./skill-harness.js");
@@ -334,9 +335,9 @@ function compileGeneratorSkillToFile(specPath, source) {
334
335
  return false;
335
336
  }
336
337
  /** Compile a ClaudeSpec → its primary + any additional targets. */
337
- function compileClaudeToFile(spec, specPath, config, dialect) {
338
+ async function compileClaudeToFile(spec, specPath, config, dialect) {
338
339
  const basePath = process.cwd();
339
- const { markdown, errors, warnings, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
340
+ const { markdown, errors, warnings, linterResults, targets } = await (0, compile_js_1.compileClaude)(spec, {
340
341
  basePath,
341
342
  specFile: specPath,
342
343
  dialect,
@@ -470,9 +471,9 @@ function formatArtifactSize(markdown) {
470
471
  return `${size} · ~${String(kTokens)}k tokens est.`;
471
472
  }
472
473
  /** Compile a declarative SkillSpec → SKILL.md. */
473
- function compileSkillToFile(spec, specPath, dialect) {
474
+ async function compileSkillToFile(spec, specPath, dialect) {
474
475
  const outputPath = specPath.replace(/\.spec\.ts$/, "");
475
- const { artifact, errors, warnings } = (0, compile_js_1.compileSkill)(spec, {
476
+ const { artifact, errors, warnings } = await (0, compile_js_1.compileSkill)(spec, {
476
477
  basePath: process.cwd(),
477
478
  specFile: specPath,
478
479
  // The SKILL.md frontmatter profile comes from the resolved harness — a Codex
@@ -494,9 +495,9 @@ function compileSkillToFile(spec, specPath, dialect) {
494
495
  return false;
495
496
  }
496
497
  /** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
497
- function compileAgentToFile(spec, specPath, dialect) {
498
+ async function compileAgentToFile(spec, specPath, dialect) {
498
499
  const outputPath = specPath.replace(/\.spec\.ts$/, "");
499
- const { artifact, errors, warnings } = (0, compile_js_1.compileAgent)(spec, {
500
+ const { artifact, errors, warnings } = await (0, compile_js_1.compileAgent)(spec, {
500
501
  basePath: process.cwd(),
501
502
  specFile: specPath,
502
503
  dialect,
@@ -591,7 +592,7 @@ async function compile(specPaths, config, excludes, opts = {}) {
591
592
  const specDialect = opts.harnessFlag === undefined
592
593
  ? ((0, adapter_registry_js_1.adapterForInstructionFile)(targetFile)?.dialect ?? dialect)
593
594
  : dialect;
594
- if (compileClaudeToFile(spec, specPath, config, specDialect)) {
595
+ if (await compileClaudeToFile(spec, specPath, config, specDialect)) {
595
596
  writeInstructionMirrors(specPath.replace(/\.spec\.ts$/, ""), declaredHarnesses);
596
597
  }
597
598
  else {
@@ -607,11 +608,11 @@ async function compile(specPaths, config, excludes, opts = {}) {
607
608
  for (const w of (0, skill_harness_js_1.skillFrontmatterDropWarnings)(spec, forHarnesses)) {
608
609
  console.log(`⚠ ${w}`);
609
610
  }
610
- if (!compileSkillToFile(spec, specPath, dialect))
611
+ if (!(await compileSkillToFile(spec, specPath, dialect)))
611
612
  allValid = false;
612
613
  }
613
614
  else if (spec._specType === "agent") {
614
- if (!compileAgentToFile(spec, specPath, dialect))
615
+ if (!(await compileAgentToFile(spec, specPath, dialect)))
615
616
  allValid = false;
616
617
  }
617
618
  else if (spec._specType === "railway") {
@@ -820,7 +821,7 @@ async function findDuplicateRules(excludes, threshold = 0.3, silent = false, sco
820
821
  * Each named file is parsed on demand; there is no project-wide index. Returns
821
822
  * the count of broken references.
822
823
  */
823
- function verifyMarkdownSymbols(files, silent) {
824
+ async function verifyMarkdownSymbols(files, silent) {
824
825
  if (files.length === 0)
825
826
  return 0;
826
827
  const cwd = process.cwd();
@@ -834,7 +835,7 @@ function verifyMarkdownSymbols(files, silent) {
834
835
  catch {
835
836
  continue;
836
837
  }
837
- const broken = (0, refs_js_1.verifySymbolRefs)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, f)));
838
+ const broken = await (0, refs_js_1.verifySymbolRefs)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, f)));
838
839
  if (broken.length === 0)
839
840
  continue;
840
841
  if (!silent) {
@@ -1360,7 +1361,7 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
1360
1361
  // collected lazily so a repo with no railway spec never pays for the walk.
1361
1362
  if (spec._specType === "railway")
1362
1363
  knownAgents ??= await collectAgentNames(excludes);
1363
- const errors = specCompileErrors(spec, specPath, dialect, config, knownAgents ?? []);
1364
+ const errors = await specCompileErrors(spec, specPath, dialect, config, knownAgents ?? []);
1364
1365
  for (const e of errors)
1365
1366
  found.push(`${target}: ${e.message} (from ${specPath})`);
1366
1367
  }
@@ -1398,11 +1399,11 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
1398
1399
  * only the options differ. Exhaustive over `_specType`, so a FIFTH spec type is
1399
1400
  * a tsc error here instead of a silent skip, which is the failure this closes.
1400
1401
  */
1401
- function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
1402
+ async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
1402
1403
  const basePath = process.cwd();
1403
1404
  switch (spec._specType) {
1404
1405
  case "claude":
1405
- return (0, compile_js_1.compileClaude)(spec, {
1406
+ return (await (0, compile_js_1.compileClaude)(spec, {
1406
1407
  basePath,
1407
1408
  specFile: specPath,
1408
1409
  dialect,
@@ -1411,13 +1412,11 @@ function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
1411
1412
  maxSectionLines: config?.maxSectionLines,
1412
1413
  catalogOnly: config?.catalogOnly,
1413
1414
  linters: config?.linters,
1414
- }).errors;
1415
+ })).errors;
1415
1416
  case "skill":
1416
- return (0, compile_js_1.compileSkill)(spec, { basePath, specFile: specPath, dialect })
1417
- .errors;
1417
+ return (await (0, compile_js_1.compileSkill)(spec, { basePath, specFile: specPath, dialect })).errors;
1418
1418
  case "agent":
1419
- return (0, compile_js_1.compileAgent)(spec, { basePath, specFile: specPath, dialect })
1420
- .errors;
1419
+ return (await (0, compile_js_1.compileAgent)(spec, { basePath, specFile: specPath, dialect })).errors;
1421
1420
  case "railway":
1422
1421
  return (0, compile_js_1.compileRailway)(spec, { specFile: specPath, knownAgents }).errors;
1423
1422
  default:
@@ -1724,7 +1723,7 @@ async function runLint(restArgs, flags, excludes, config) {
1724
1723
  }
1725
1724
  }
1726
1725
  // 9. Verify code-shaped symbol references live (see src/refs.ts).
1727
- const symbolRefErrors = verifyMarkdownSymbols(files, silent);
1726
+ const symbolRefErrors = await verifyMarkdownSymbols(files, silent);
1728
1727
  // 10. Verify `vigiles:mcp server#tool` marks against live MCP servers
1729
1728
  // (only when a .mcp.json declares them). See src/mcp.ts.
1730
1729
  const mcpRefErrors = await verifyMarkdownMcpRefs(files, silent);
@@ -4915,6 +4914,21 @@ function gitHead(cwd) {
4915
4914
  }
4916
4915
  async function handleRunScripts(kind, args, restArgs, excludes) {
4917
4916
  const cwd = process.cwd();
4917
+ // The ignore file keeps NEW copies of `.vigiles/` local files out of git, but
4918
+ // cannot untrack one a repo already committed. Said here, on the CLI, because
4919
+ // it spawns `git` — never from a hook runtime — and said FIRST, before any
4920
+ // early return: a repo whose harness was removed still carries the stale
4921
+ // committed artifact, and "no files found" must not swallow the warning
4922
+ // (Codex review on #274).
4923
+ //
4924
+ // The ignore file is brought up to date FIRST, and only where `.vigiles/`
4925
+ // already exists: the writers below would otherwise make their edit after the
4926
+ // check, so a tracked ignore file dirtied by this very run went unreported on
4927
+ // the one run a user may ever do (Codex review on #275).
4928
+ const vigilesDir = (0, node_path_1.join)(cwd, local_files_js_1.VIGILES_DIR);
4929
+ if ((0, node_fs_1.existsSync)(vigilesDir))
4930
+ (0, local_files_js_1.ensureLocalFilesIgnored)(vigilesDir);
4931
+ (0, local_files_tracked_js_1.warnTrackedLocalFiles)(cwd);
4918
4932
  // Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
4919
4933
  const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
4920
4934
  // The eval LOCK flags (`--check`/`--update`) are resolved BEFORE file discovery
@@ -4930,9 +4944,13 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
4930
4944
  // A script under an excluded path is not DISCOVERED (a vendored corpus's own
4931
4945
  // harness must not run as ours), but a script you NAME still runs (#192).
4932
4946
  const files = (0, run_scripts_js_1.discoverScripts)(restArgs.map((p) => noteExplicitOverride(excludes, p, "running")), defaultGlob, cwd, excludes.ignore);
4933
- // `--min=N`: a CI gate asserts at least N scripts actually RAN — so a bad path,
4934
- // a renamed file, or a glob that matched nothing fails LOUD instead of passing
4935
- // green with zero evals executed. Default 0 (off) keeps local runs ergonomic.
4947
+ // `--min=N`: a CI gate asserts at least N scripts actually LOADED — so a bad
4948
+ // path, a renamed file, a glob that matched nothing, or a file that matched and
4949
+ // never linked fails LOUD instead of passing green with nothing executed.
4950
+ // Checked twice: here against files MATCHED (cheap, before anything runs), and
4951
+ // after the run against scripts that LOADED — a matched file that could not be
4952
+ // evaluated is not a script that ran (#243). A declared skip (exit 77) did
4953
+ // load, so it counts. Default 0 (off) keeps local runs ergonomic.
4936
4954
  const minFlag = args.find((a) => a.startsWith("--min="));
4937
4955
  const minRequired = minFlag
4938
4956
  ? Math.max(0, Number.parseInt(minFlag.split("=")[1] ?? "", 10) || 0)
@@ -5035,11 +5053,13 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
5035
5053
  // a flag: the run already happened, and this is the runner recording what it
5036
5054
  // saw — the same shape as the flight-recorder ledger it already appends to.
5037
5055
  recordRunCoverage(cwd, results, kind, harnessFlagFrom(args));
5038
- // The ignore file keeps NEW copies of `.vigiles/` local files out of git, but
5039
- // cannot untrack one a repo already committed. Said here, on the CLI, because
5040
- // it spawns `git` — never from a hook runtime.
5041
- (0, local_files_tracked_js_1.warnTrackedLocalFiles)(cwd);
5042
5056
  console.log("\n" + (0, run_scripts_js_1.formatScriptSummary)(results));
5057
+ const loadedCount = results.filter((r) => !(0, run_scripts_js_1.loadFailed)(r)).length;
5058
+ const belowFloor = loadedCount < minRequired;
5059
+ if (belowFloor) {
5060
+ console.error(`\n✗ vigiles ${kind}: --min=${String(minRequired)} but only ${String(loadedCount)} of ` +
5061
+ `${String(files.length)} matched ${kind} file(s) loaded.`);
5062
+ }
5043
5063
  if ((0, run_scripts_js_1.anyFailed)(results))
5044
5064
  process.exit(1);
5045
5065
  // 🔴 A SKIP THE AUTHOR NEVER DECLARED IS NOT A SKIP — and the discriminator was
@@ -5047,20 +5067,23 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
5047
5067
  // the runtime could not evaluate exits with whatever the loader gave it, 1 in
5048
5068
  // practice. Both are classified `"skip"` so that neither RETRACTS coverage —
5049
5069
  // which is right, a file that did not run proved nothing either way — but only
5050
- // the declared one is a reason to stay green.
5070
+ // the declared one is a reason to stay green. Which is which is decided by the
5071
+ // runner's load hook (a marker import that evaluates only once the module
5072
+ // graph linked), not by reading the child's output — see `statusFor`.
5051
5073
  //
5052
5074
  // Reported as #243: `vigiles test .` printed a resolver stack over a `⊘`, said
5053
5075
  // `0 passed, 1 skipped`, and exited 0. Downstream a consumer's README shipped
5054
5076
  // that exact command as its first setup step, so a new reader's suite silently
5055
5077
  // never ran. `--no-skip` would have caught it and is not the default; `--min=1`
5056
- // does not, because it counts files MATCHED, not scripts executed.
5078
+ // did not either, because it counted files MATCHED — it now also counts the
5079
+ // scripts that loaded (above).
5057
5080
  //
5058
5081
  // This is deliberately NOT a fifth `ScriptStatus`. Coverage retraction reads the
5059
5082
  // status as a bare STRING (`executedScripts`, `coverage-artifact.ts`, whose
5060
5083
  // parameter is typed `string`), so a new member would start retracting silently
5061
5084
  // with no type error — breaking the one property the classification exists to
5062
5085
  // protect.
5063
- const notEvaluated = results.filter((r) => r.status === "skip" && r.code !== run_scripts_js_1.SKIP_EXIT_CODE);
5086
+ const notEvaluated = results.filter(run_scripts_js_1.loadFailed);
5064
5087
  if (notEvaluated.length > 0) {
5065
5088
  console.error(`\n✗ vigiles ${kind}: ${String(notEvaluated.length)} script(s) never ran — the runtime could not load them:\n` +
5066
5089
  notEvaluated
@@ -5071,6 +5094,8 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
5071
5094
  `a declared skip stays green.`);
5072
5095
  process.exit(1);
5073
5096
  }
5097
+ if (belowFloor)
5098
+ process.exit(1);
5074
5099
  // `--no-skip`: in a context that ASSERTS the capability is present (a CI job),
5075
5100
  // a skipped tier is untested surface — fail loudly instead of passing green.
5076
5101
  if (args.includes("--no-skip") && results.some((r) => r.status === "skip")) {
@@ -5621,7 +5646,7 @@ async function handleHookRuntime(kind, restArgs) {
5621
5646
  actionHookCommand();
5622
5647
  return;
5623
5648
  case "refs":
5624
- refsHookCommand();
5649
+ await refsHookCommand();
5625
5650
  return;
5626
5651
  case "eval-lock-nudge":
5627
5652
  evalLockNudgeHookCommand();
@@ -5770,7 +5795,7 @@ function evalLockNudgeHookCommand() {
5770
5795
  * agent mark its references, at write time, with full context. `vigiles:ignore`
5771
5796
  * opts a prose span out.
5772
5797
  */
5773
- function refsHookCommand() {
5798
+ async function refsHookCommand() {
5774
5799
  let raw = "";
5775
5800
  try {
5776
5801
  raw = (0, node_fs_1.readFileSync)(0, "utf-8");
@@ -5803,7 +5828,7 @@ function refsHookCommand() {
5803
5828
  catch {
5804
5829
  return;
5805
5830
  }
5806
- const issues = (0, refs_js_1.collectRefIssues)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)));
5831
+ const issues = await (0, refs_js_1.collectRefIssues)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)));
5807
5832
  const action = (0, refs_js_1.refsHookAction)(issues.length, severity);
5808
5833
  if (action === "ok")
5809
5834
  return;
package/dist/codex.js CHANGED
@@ -14,6 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ // 🔴 PUBLIC ENTRY POINT `vigiles/codex` — every export here is a promise to users. The default for a
18
+ // symbol is INTERNAL. It is exported only if (a) a NAMED external consumer uses it, or (b) it is
19
+ // a deliberate extension point listed in STABILITY.md (the adapter kit is the example). "Might
20
+ // be useful" is neither. Review point: the diff of `api-surface/vigiles-codex.api.md`, which
21
+ // `npm run api:check` fails on.
17
22
  /**
18
23
  * `vigiles/codex` — the OpenAI Codex harness adapter. Sits beside
19
24
  * `vigiles/claude-code`: same harness-agnostic `vigiles` testing core, a
@@ -63,7 +63,7 @@ export interface CompileError {
63
63
  export declare function validateFileRef(filePath: string, basePath: string): CompileError | null;
64
64
  export declare function readPackageScripts(basePath: string): Record<string, string> | null;
65
65
  export declare function validateCommandRef(command: string, basePath: string): CompileError | null;
66
- export declare function validateSymbolRef(file: string, name: string, basePath: string): CompileError | null;
66
+ export declare function validateSymbolRef(file: string, name: string, basePath: string): Promise<CompileError | null>;
67
67
  export declare function validateDirRef(dirPath: string, basePath: string): CompileError | null;
68
68
  export declare function validateGlobRef(pattern: string, basePath: string): CompileError | null;
69
69
  export interface CompileClaudeResult {
@@ -142,7 +142,7 @@ export declare const DEFAULT_MAX_SECTION_CHARS = 15000;
142
142
  * Returns the compiled markdown, validation errors, and linter check results.
143
143
  * The markdown is generated even if there are errors (with warnings).
144
144
  */
145
- export declare function compileClaude(spec: ClaudeSpec, options?: CompileClaudeOptions): CompileClaudeResult;
145
+ export declare function compileClaude(spec: ClaudeSpec, options?: CompileClaudeOptions): Promise<CompileClaudeResult>;
146
146
  export interface CompileSkillResult {
147
147
  /**
148
148
  * The stamped artifact — present ONLY when `errors` is empty. `null` is what
@@ -163,7 +163,7 @@ export declare function compileSkill(spec: SkillSpec, options?: {
163
163
  /** The harness dialect — supplies `skillFrontmatterKeys`. Omitting it emits
164
164
  * every key the compiler can render, so existing callers are unchanged. */
165
165
  dialect?: HarnessDialect;
166
- }): CompileSkillResult;
166
+ }): Promise<CompileSkillResult>;
167
167
  export interface CompileAgentResult {
168
168
  /**
169
169
  * The stamped artifact — present ONLY when `errors` is empty. `null` is what
@@ -186,7 +186,7 @@ export declare function compileAgent(spec: AgentSpec, options: {
186
186
  /** The harness dialect to verify the tool contract against (required — the
187
187
  * core defines no default dialect; the adapter/composition root injects it). */
188
188
  dialect: HarnessDialect;
189
- }): CompileAgentResult;
189
+ }): Promise<CompileAgentResult>;
190
190
  export interface CompileRailwayOptions {
191
191
  /** Names of compiled agents, to resolve `delegate` targets. Skipped if omitted. */
192
192
  knownAgents?: readonly string[];
@@ -230,5 +230,5 @@ export interface AdoptResult {
230
230
  * Compare a generated file against what the spec would produce.
231
231
  * Returns the diff so users can see what was manually changed.
232
232
  */
233
- export declare function adoptDiff(filePath: string, spec: ClaudeSpec | SkillSpec | AgentSpec, basePath: string, dialect: HarnessDialect): AdoptResult;
233
+ export declare function adoptDiff(filePath: string, spec: ClaudeSpec | SkillSpec | AgentSpec, basePath: string, dialect: HarnessDialect): Promise<AdoptResult>;
234
234
  //# sourceMappingURL=compile.d.ts.map