vigiles 30.0.1 → 30.0.2
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.
- package/dist/adapters/claude-code/run-scripts.d.ts +42 -2
- package/dist/adapters/claude-code/run-scripts.js +92 -67
- package/dist/cli-main.js +37 -10
- package/dist/harness-resolve-hooks.d.mts +21 -0
- package/dist/harness-resolve-hooks.mjs +76 -8
- package/dist/local-files-tracked.d.ts +18 -1
- package/dist/local-files-tracked.js +55 -13
- package/dist/local-files.d.ts +11 -2
- package/dist/local-files.js +31 -10
- package/package.json +1 -1
|
@@ -37,7 +37,19 @@ export interface ScriptRunResult {
|
|
|
37
37
|
*/
|
|
38
38
|
export declare const SKIP_EXIT_CODE = 77;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
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
|
|
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
|
|
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,
|
|
84
|
+
function statusFor(code, checks, load) {
|
|
64
85
|
if (code === exports.SKIP_EXIT_CODE)
|
|
65
86
|
return "skip";
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
|
115
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
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");
|
|
@@ -4915,6 +4916,21 @@ function gitHead(cwd) {
|
|
|
4915
4916
|
}
|
|
4916
4917
|
async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
4917
4918
|
const cwd = process.cwd();
|
|
4919
|
+
// The ignore file keeps NEW copies of `.vigiles/` local files out of git, but
|
|
4920
|
+
// cannot untrack one a repo already committed. Said here, on the CLI, because
|
|
4921
|
+
// it spawns `git` — never from a hook runtime — and said FIRST, before any
|
|
4922
|
+
// early return: a repo whose harness was removed still carries the stale
|
|
4923
|
+
// committed artifact, and "no files found" must not swallow the warning
|
|
4924
|
+
// (Codex review on #274).
|
|
4925
|
+
//
|
|
4926
|
+
// The ignore file is brought up to date FIRST, and only where `.vigiles/`
|
|
4927
|
+
// already exists: the writers below would otherwise make their edit after the
|
|
4928
|
+
// check, so a tracked ignore file dirtied by this very run went unreported on
|
|
4929
|
+
// the one run a user may ever do (Codex review on #275).
|
|
4930
|
+
const vigilesDir = (0, node_path_1.join)(cwd, local_files_js_1.VIGILES_DIR);
|
|
4931
|
+
if ((0, node_fs_1.existsSync)(vigilesDir))
|
|
4932
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)(vigilesDir);
|
|
4933
|
+
(0, local_files_tracked_js_1.warnTrackedLocalFiles)(cwd);
|
|
4918
4934
|
// Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
|
|
4919
4935
|
const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
|
|
4920
4936
|
// The eval LOCK flags (`--check`/`--update`) are resolved BEFORE file discovery
|
|
@@ -4930,9 +4946,13 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
4930
4946
|
// A script under an excluded path is not DISCOVERED (a vendored corpus's own
|
|
4931
4947
|
// harness must not run as ours), but a script you NAME still runs (#192).
|
|
4932
4948
|
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
|
|
4934
|
-
// a renamed file,
|
|
4935
|
-
//
|
|
4949
|
+
// `--min=N`: a CI gate asserts at least N scripts actually LOADED — so a bad
|
|
4950
|
+
// path, a renamed file, a glob that matched nothing, or a file that matched and
|
|
4951
|
+
// never linked fails LOUD instead of passing green with nothing executed.
|
|
4952
|
+
// Checked twice: here against files MATCHED (cheap, before anything runs), and
|
|
4953
|
+
// after the run against scripts that LOADED — a matched file that could not be
|
|
4954
|
+
// evaluated is not a script that ran (#243). A declared skip (exit 77) did
|
|
4955
|
+
// load, so it counts. Default 0 (off) keeps local runs ergonomic.
|
|
4936
4956
|
const minFlag = args.find((a) => a.startsWith("--min="));
|
|
4937
4957
|
const minRequired = minFlag
|
|
4938
4958
|
? Math.max(0, Number.parseInt(minFlag.split("=")[1] ?? "", 10) || 0)
|
|
@@ -5035,11 +5055,13 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
5035
5055
|
// a flag: the run already happened, and this is the runner recording what it
|
|
5036
5056
|
// saw — the same shape as the flight-recorder ledger it already appends to.
|
|
5037
5057
|
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
5058
|
console.log("\n" + (0, run_scripts_js_1.formatScriptSummary)(results));
|
|
5059
|
+
const loadedCount = results.filter((r) => !(0, run_scripts_js_1.loadFailed)(r)).length;
|
|
5060
|
+
const belowFloor = loadedCount < minRequired;
|
|
5061
|
+
if (belowFloor) {
|
|
5062
|
+
console.error(`\n✗ vigiles ${kind}: --min=${String(minRequired)} but only ${String(loadedCount)} of ` +
|
|
5063
|
+
`${String(files.length)} matched ${kind} file(s) loaded.`);
|
|
5064
|
+
}
|
|
5043
5065
|
if ((0, run_scripts_js_1.anyFailed)(results))
|
|
5044
5066
|
process.exit(1);
|
|
5045
5067
|
// 🔴 A SKIP THE AUTHOR NEVER DECLARED IS NOT A SKIP — and the discriminator was
|
|
@@ -5047,20 +5069,23 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
5047
5069
|
// the runtime could not evaluate exits with whatever the loader gave it, 1 in
|
|
5048
5070
|
// practice. Both are classified `"skip"` so that neither RETRACTS coverage —
|
|
5049
5071
|
// 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.
|
|
5072
|
+
// the declared one is a reason to stay green. Which is which is decided by the
|
|
5073
|
+
// runner's load hook (a marker import that evaluates only once the module
|
|
5074
|
+
// graph linked), not by reading the child's output — see `statusFor`.
|
|
5051
5075
|
//
|
|
5052
5076
|
// Reported as #243: `vigiles test .` printed a resolver stack over a `⊘`, said
|
|
5053
5077
|
// `0 passed, 1 skipped`, and exited 0. Downstream a consumer's README shipped
|
|
5054
5078
|
// that exact command as its first setup step, so a new reader's suite silently
|
|
5055
5079
|
// never ran. `--no-skip` would have caught it and is not the default; `--min=1`
|
|
5056
|
-
//
|
|
5080
|
+
// did not either, because it counted files MATCHED — it now also counts the
|
|
5081
|
+
// scripts that loaded (above).
|
|
5057
5082
|
//
|
|
5058
5083
|
// This is deliberately NOT a fifth `ScriptStatus`. Coverage retraction reads the
|
|
5059
5084
|
// status as a bare STRING (`executedScripts`, `coverage-artifact.ts`, whose
|
|
5060
5085
|
// parameter is typed `string`), so a new member would start retracting silently
|
|
5061
5086
|
// with no type error — breaking the one property the classification exists to
|
|
5062
5087
|
// protect.
|
|
5063
|
-
const notEvaluated = results.filter(
|
|
5088
|
+
const notEvaluated = results.filter(run_scripts_js_1.loadFailed);
|
|
5064
5089
|
if (notEvaluated.length > 0) {
|
|
5065
5090
|
console.error(`\n✗ vigiles ${kind}: ${String(notEvaluated.length)} script(s) never ran — the runtime could not load them:\n` +
|
|
5066
5091
|
notEvaluated
|
|
@@ -5071,6 +5096,8 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
5071
5096
|
`a declared skip stays green.`);
|
|
5072
5097
|
process.exit(1);
|
|
5073
5098
|
}
|
|
5099
|
+
if (belowFloor)
|
|
5100
|
+
process.exit(1);
|
|
5074
5101
|
// `--no-skip`: in a context that ASSERTS the capability is present (a CI job),
|
|
5075
5102
|
// a skipped tier is untested surface — fail loudly instead of passing green.
|
|
5076
5103
|
if (args.includes("--no-skip") && results.some((r) => r.status === "skip")) {
|
|
@@ -8,6 +8,27 @@ type Resolved = {
|
|
|
8
8
|
shortCircuit?: boolean;
|
|
9
9
|
};
|
|
10
10
|
type NextResolve = (specifier: string, context: ResolveContext) => Resolved | Promise<Resolved>;
|
|
11
|
+
type LoadContext = {
|
|
12
|
+
format?: string | null;
|
|
13
|
+
conditions: string[];
|
|
14
|
+
};
|
|
15
|
+
type Loaded = {
|
|
16
|
+
format?: string | null;
|
|
17
|
+
source?: string | ArrayBuffer | ArrayBufferView | null;
|
|
18
|
+
shortCircuit?: boolean;
|
|
19
|
+
};
|
|
20
|
+
type NextLoad = (url: string, context: LoadContext) => Loaded | Promise<Loaded>;
|
|
21
|
+
/** What the runner hands this hook for ONE child. See `run-scripts.ts`. */
|
|
22
|
+
export interface LoadProbe {
|
|
23
|
+
/** The script's own module URL (realpath), whatever process entry runs it. */
|
|
24
|
+
readonly entryURL: string;
|
|
25
|
+
/** Written (empty) by the marker module when it evaluates: the graph linked. */
|
|
26
|
+
readonly loadedFile: string;
|
|
27
|
+
/** Written with `{ format, marked }` when this hook sees the script's module. */
|
|
28
|
+
readonly seenFile: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function initialize(data: LoadProbe | undefined): void;
|
|
11
31
|
export declare function resolve(specifier: string, context: ResolveContext, nextResolve: NextResolve): Promise<Resolved>;
|
|
32
|
+
export declare function load(url: string, context: LoadContext, nextLoad: NextLoad): Promise<Loaded>;
|
|
12
33
|
export {};
|
|
13
34
|
//# sourceMappingURL=harness-resolve-hooks.d.mts.map
|
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module-
|
|
3
|
-
*
|
|
2
|
+
* Module-customization hooks for HARNESS scripts (`vigiles test` / `vigiles eval`).
|
|
3
|
+
* Two jobs, one registration:
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `./
|
|
8
|
-
* `
|
|
5
|
+
* 1. RESOLVE — make a bare `vigiles` import resolve to the CLI's OWN
|
|
6
|
+
* installation. The rescue itself — why it exists, what it refuses to touch —
|
|
7
|
+
* lives in `./self-resolve.mjs`, because the spec host registers the same
|
|
8
|
+
* branch from `./spec-hooks.mjs` and two copies of it is exactly the
|
|
9
|
+
* divergence that put `test` and `compile` on different answers to the same
|
|
10
|
+
* question. What stays here is the protocol: try normal resolution first, and
|
|
11
|
+
* only consider the rescue on the way out of the failure.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* 2. LOAD — tell the runner whether the script LOADED (#243). ESM links the
|
|
14
|
+
* whole import graph before evaluating any of it, and evaluates a module's
|
|
15
|
+
* imports in source order. So a marker import placed FIRST in the script's
|
|
16
|
+
* module evaluates if and only if every module in the graph was found, parsed
|
|
17
|
+
* and linked — and before any dependency or the script body runs. The marker
|
|
18
|
+
* writes a file; the runner reads it. No output text is read, so a harness
|
|
19
|
+
* that prints a loader error as EVIDENCE about the thing it tests (a hook
|
|
20
|
+
* transcript, say) can no longer be mistaken for one that never ran.
|
|
21
|
+
*
|
|
22
|
+
* The probe paths arrive through `register(…, { data })` → `initialize`, NOT the
|
|
23
|
+
* environment, so a process the harness spawns cannot inherit them and write
|
|
24
|
+
* over its parent's answer.
|
|
12
25
|
*/
|
|
26
|
+
import { writeFileSync } from "node:fs";
|
|
13
27
|
import { resolveSelfSpecifier } from "./self-resolve.mjs";
|
|
28
|
+
const MARKER_URL = "vigiles-internal:loaded";
|
|
29
|
+
/**
|
|
30
|
+
* Formats the marker can be planted in. 🔴 CommonJS is deliberately absent: it
|
|
31
|
+
* has no link phase to mark (a `require` resolves while the body runs), so a CJS
|
|
32
|
+
* script carries no marker and a non-zero exit from it is conservatively a
|
|
33
|
+
* `fail` — which retracts coverage rather than hiding a real failure.
|
|
34
|
+
* `module-typescript` is Node's own type stripping (22.6+), which strips in
|
|
35
|
+
* place and so keeps positions.
|
|
36
|
+
*/
|
|
37
|
+
const MARKABLE = new Set(["module", "module-typescript"]);
|
|
38
|
+
let probe;
|
|
39
|
+
export function initialize(data) {
|
|
40
|
+
probe = data;
|
|
41
|
+
}
|
|
14
42
|
export async function resolve(specifier, context, nextResolve) {
|
|
43
|
+
if (probe !== undefined && specifier === MARKER_URL)
|
|
44
|
+
return { url: MARKER_URL, shortCircuit: true };
|
|
15
45
|
try {
|
|
16
46
|
return await nextResolve(specifier, context);
|
|
17
47
|
}
|
|
@@ -24,4 +54,42 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
24
54
|
return rescued;
|
|
25
55
|
}
|
|
26
56
|
}
|
|
57
|
+
export async function load(url, context, nextLoad) {
|
|
58
|
+
if (probe !== undefined && url === MARKER_URL)
|
|
59
|
+
return {
|
|
60
|
+
format: "module",
|
|
61
|
+
shortCircuit: true,
|
|
62
|
+
source: `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(probe.loadedFile)}, "");`,
|
|
63
|
+
};
|
|
64
|
+
const loaded = await nextLoad(url, context);
|
|
65
|
+
if (probe === undefined || url !== probe.entryURL)
|
|
66
|
+
return loaded;
|
|
67
|
+
const marked = MARKABLE.has(String(loaded.format));
|
|
68
|
+
// Written BEFORE the graph links, so its presence proves the hook saw the
|
|
69
|
+
// script. Without it a missing `loadedFile` would mean nothing: a script this
|
|
70
|
+
// hook never saw (a loader that rewrote its URL, say) is never did-not-load.
|
|
71
|
+
writeFileSync(probe.seenFile, JSON.stringify({ format: loaded.format, marked }));
|
|
72
|
+
if (!marked)
|
|
73
|
+
return loaded;
|
|
74
|
+
return { ...loaded, source: withMarker(sourceText(loaded.source)) };
|
|
75
|
+
}
|
|
76
|
+
function sourceText(source) {
|
|
77
|
+
if (typeof source === "string")
|
|
78
|
+
return source;
|
|
79
|
+
// `String(uint8array)` gives "1,2,3", so decode explicitly.
|
|
80
|
+
return new TextDecoder().decode(source ?? new Uint8Array());
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Put the marker import on the file's FIRST line — after a shebang, which must
|
|
84
|
+
* stay first — so no line number moves; only columns on that one line do.
|
|
85
|
+
*/
|
|
86
|
+
function withMarker(src) {
|
|
87
|
+
const mark = `import ${JSON.stringify(MARKER_URL)};`;
|
|
88
|
+
if (!src.startsWith("#!"))
|
|
89
|
+
return mark + src;
|
|
90
|
+
const nl = src.indexOf("\n");
|
|
91
|
+
if (nl === -1)
|
|
92
|
+
return `${src}\n${mark}`;
|
|
93
|
+
return src.slice(0, nl + 1) + mark + src.slice(nl + 1);
|
|
94
|
+
}
|
|
27
95
|
//# sourceMappingURL=harness-resolve-hooks.mjs.map
|
|
@@ -5,13 +5,30 @@
|
|
|
5
5
|
* every "cannot tell".
|
|
6
6
|
*/
|
|
7
7
|
export declare function trackedLocalFiles(root: string): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Does the COMMITTED `.vigiles/.gitignore` (the one at `HEAD`) lack entries
|
|
10
|
+
* vigiles needs — i.e. will vigiles' additions show up as a change to a tracked
|
|
11
|
+
* file until someone commits them.
|
|
12
|
+
*
|
|
13
|
+
* The question is asked of `HEAD`, not of the worktree's dirtiness, and that
|
|
14
|
+
* is the point (Codex review on #275, third pass). "The file is modified" was
|
|
15
|
+
* the earlier predicate; it could not tell vigiles' additions from the owner's
|
|
16
|
+
* own unrelated edit, and then advised committing work in progress. Reading the
|
|
17
|
+
* committed copy answers only for vigiles' entries, staged or not, and says
|
|
18
|
+
* nothing once they are committed, whatever else the owner is editing.
|
|
19
|
+
* `HEAD:./path`, not `HEAD:path`: without `./` git resolves the path from the
|
|
20
|
+
* REPOSITORY root, so a package nested in a larger worktree read the wrong file
|
|
21
|
+
* — or none — and stayed silent (Codex review on #275, gitrevisions(7)).
|
|
22
|
+
* Silent on every "cannot tell": not in a repo, no `HEAD`, file not committed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function committedIgnoreFileLacksEntries(root: string): boolean;
|
|
8
25
|
/**
|
|
9
26
|
* The one-line warning for {@link trackedLocalFiles}, or `null` when there is
|
|
10
27
|
* nothing to say. Files inside a tracked local DIRECTORY (`state/`,
|
|
11
28
|
* `eval-cache/`) are named by that directory, so one line stays one line and the
|
|
12
29
|
* command it prints untracks all of them.
|
|
13
30
|
*/
|
|
14
|
-
export declare function formatTrackedLocalFiles(tracked: readonly string[]): string | null;
|
|
31
|
+
export declare function formatTrackedLocalFiles(tracked: readonly string[], committedLacksEntries?: boolean): string | null;
|
|
15
32
|
/** Print the warning for `root` to stderr when there is one. CLI-only. */
|
|
16
33
|
export declare function warnTrackedLocalFiles(root: string): void;
|
|
17
34
|
//# sourceMappingURL=local-files-tracked.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.trackedLocalFiles = trackedLocalFiles;
|
|
4
|
+
exports.committedIgnoreFileLacksEntries = committedIgnoreFileLacksEntries;
|
|
4
5
|
exports.formatTrackedLocalFiles = formatTrackedLocalFiles;
|
|
5
6
|
exports.warnTrackedLocalFiles = warnTrackedLocalFiles;
|
|
6
7
|
/**
|
|
@@ -20,6 +21,8 @@ exports.warnTrackedLocalFiles = warnTrackedLocalFiles;
|
|
|
20
21
|
const node_child_process_1 = require("node:child_process");
|
|
21
22
|
const node_path_1 = require("node:path");
|
|
22
23
|
const local_files_js_1 = require("./local-files.js");
|
|
24
|
+
/** `.vigiles/.gitignore`, repo-relative. */
|
|
25
|
+
const IGNORE_FILE = node_path_1.posix.join(local_files_js_1.VIGILES_DIR, local_files_js_1.LOCAL_GITIGNORE_FILE);
|
|
23
26
|
/**
|
|
24
27
|
* The per-checkout files under `<root>/.vigiles/` that git tracks, as
|
|
25
28
|
* repo-relative paths from `root`. Empty when nothing is tracked, when `root` is
|
|
@@ -42,28 +45,67 @@ function trackedLocalFiles(root) {
|
|
|
42
45
|
return [];
|
|
43
46
|
}
|
|
44
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Does the COMMITTED `.vigiles/.gitignore` (the one at `HEAD`) lack entries
|
|
50
|
+
* vigiles needs — i.e. will vigiles' additions show up as a change to a tracked
|
|
51
|
+
* file until someone commits them.
|
|
52
|
+
*
|
|
53
|
+
* The question is asked of `HEAD`, not of the worktree's dirtiness, and that
|
|
54
|
+
* is the point (Codex review on #275, third pass). "The file is modified" was
|
|
55
|
+
* the earlier predicate; it could not tell vigiles' additions from the owner's
|
|
56
|
+
* own unrelated edit, and then advised committing work in progress. Reading the
|
|
57
|
+
* committed copy answers only for vigiles' entries, staged or not, and says
|
|
58
|
+
* nothing once they are committed, whatever else the owner is editing.
|
|
59
|
+
* `HEAD:./path`, not `HEAD:path`: without `./` git resolves the path from the
|
|
60
|
+
* REPOSITORY root, so a package nested in a larger worktree read the wrong file
|
|
61
|
+
* — or none — and stayed silent (Codex review on #275, gitrevisions(7)).
|
|
62
|
+
* Silent on every "cannot tell": not in a repo, no `HEAD`, file not committed.
|
|
63
|
+
*/
|
|
64
|
+
function committedIgnoreFileLacksEntries(root) {
|
|
65
|
+
try {
|
|
66
|
+
const r = (0, node_child_process_1.spawnSync)("git", ["show", `HEAD:./${IGNORE_FILE}`], {
|
|
67
|
+
cwd: root,
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
70
|
+
});
|
|
71
|
+
if (r.status !== 0 || typeof r.stdout !== "string")
|
|
72
|
+
return false;
|
|
73
|
+
return (0, local_files_js_1.entriesNotInEffect)(r.stdout).length > 0;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
45
79
|
/**
|
|
46
80
|
* The one-line warning for {@link trackedLocalFiles}, or `null` when there is
|
|
47
81
|
* nothing to say. Files inside a tracked local DIRECTORY (`state/`,
|
|
48
82
|
* `eval-cache/`) are named by that directory, so one line stays one line and the
|
|
49
83
|
* command it prints untracks all of them.
|
|
50
84
|
*/
|
|
51
|
-
function formatTrackedLocalFiles(tracked) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
function formatTrackedLocalFiles(tracked, committedLacksEntries = false) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
if (tracked.length > 0) {
|
|
88
|
+
// `.vigiles/state/.claude/hooks/x.json` → `.vigiles/state`: the list entry.
|
|
89
|
+
const entries = [
|
|
90
|
+
...new Set(tracked.map((p) => p.split("/").slice(0, 2).join("/"))),
|
|
91
|
+
];
|
|
92
|
+
const one = entries.length === 1;
|
|
93
|
+
lines.push(`⚠ ${entries.join(", ")} ${one ? "is" : "are"} tracked by git, but ` +
|
|
94
|
+
`${local_files_js_1.VIGILES_DIR}/ local files describe one checkout and would credit a ` +
|
|
95
|
+
`machine where nothing ran. .gitignore does not untrack a tracked file; ` +
|
|
96
|
+
`untrack ${one ? "it" : "them"} once: git rm -r --cached ${entries.join(" ")}`);
|
|
97
|
+
}
|
|
98
|
+
// Not "untrack it": the repo may keep its own rules there, and untracking
|
|
99
|
+
// would take them away from everyone else.
|
|
100
|
+
if (committedLacksEntries)
|
|
101
|
+
lines.push(`⚠ ${IGNORE_FILE} is tracked by git and its committed version lacks ` +
|
|
102
|
+
`vigiles' local-file entries, which vigiles adds to your copy. Commit ` +
|
|
103
|
+
`those lines once; entries are appended only when missing.`);
|
|
104
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
63
105
|
}
|
|
64
106
|
/** Print the warning for `root` to stderr when there is one. CLI-only. */
|
|
65
107
|
function warnTrackedLocalFiles(root) {
|
|
66
|
-
const line = formatTrackedLocalFiles(trackedLocalFiles(root));
|
|
108
|
+
const line = formatTrackedLocalFiles(trackedLocalFiles(root), committedIgnoreFileLacksEntries(root));
|
|
67
109
|
if (line)
|
|
68
110
|
process.stderr.write(line + "\n");
|
|
69
111
|
}
|
package/dist/local-files.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export declare const LOCAL_FILES: readonly LocalFile[];
|
|
|
35
35
|
*/
|
|
36
36
|
export declare const COMMITTED_PATHS: readonly string[];
|
|
37
37
|
/** The header comment on a `.vigiles/.gitignore` vigiles creates. */
|
|
38
|
+
/** The ignore file vigiles keeps inside `.vigiles/`. One spelling for every reader. */
|
|
39
|
+
export declare const LOCAL_GITIGNORE_FILE = ".gitignore";
|
|
38
40
|
export declare const LOCAL_GITIGNORE_HEADER: readonly string[];
|
|
39
41
|
/** The ignore lines, in order: every local path, then the file itself. */
|
|
40
42
|
export declare function localIgnoreEntries(): string[];
|
|
@@ -49,8 +51,9 @@ export declare function localIgnoreEntries(): string[];
|
|
|
49
51
|
* or reordered. Appending is enough because git's last matching rule wins.
|
|
50
52
|
*
|
|
51
53
|
* "In effect" is judged the way git reads the file, not by text membership:
|
|
52
|
-
* lines in order, a later
|
|
53
|
-
* whitespace is part of the pattern (only
|
|
54
|
+
* lines in order, a later negation cancels an earlier `/entry` (see
|
|
55
|
+
* {@link inEffect}), and leading whitespace is part of the pattern (only
|
|
56
|
+
* trailing whitespace is dropped).
|
|
54
57
|
* Both cases were measured with `git check-ignore`: `/coverage.json` followed
|
|
55
58
|
* by `!/coverage.json`, and ` /coverage.json`, each leave the file NOT
|
|
56
59
|
* ignored while a trimmed set would call the entry present.
|
|
@@ -58,5 +61,11 @@ export declare function localIgnoreEntries(): string[];
|
|
|
58
61
|
* Costs one read on the hot path. Best-effort: any fs error is swallowed —
|
|
59
62
|
* keeping git tidy must never break a hook, a test run or an audit.
|
|
60
63
|
*/
|
|
64
|
+
/**
|
|
65
|
+
* The entries a `.vigiles/.gitignore` with this content does NOT put in effect.
|
|
66
|
+
* One reader for both questions — "what to append" ({@link ensureLocalFilesIgnored})
|
|
67
|
+
* and "does the committed copy already carry them" (the CLI's tracked-file check).
|
|
68
|
+
*/
|
|
69
|
+
export declare function entriesNotInEffect(content: string): string[];
|
|
61
70
|
export declare function ensureLocalFilesIgnored(vigilesDir: string): void;
|
|
62
71
|
//# sourceMappingURL=local-files.d.ts.map
|
package/dist/local-files.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LOCAL_GITIGNORE_HEADER = exports.COMMITTED_PATHS = exports.LOCAL_FILES = exports.EVAL_CACHE_DIR = exports.HOOK_OBSERVATIONS_FILE = exports.GUARD_LEDGER_FILE = exports.EFFECT_ACTIVE_FILE = exports.ACTIVE_AGENT_FILE = exports.ACTIVE_SKILL_FILE = exports.HOOK_STATE_DIR = exports.LEDGER_FILE = exports.COVERAGE_ARTIFACT_FILE = exports.VIGILES_DIR = void 0;
|
|
3
|
+
exports.LOCAL_GITIGNORE_HEADER = exports.LOCAL_GITIGNORE_FILE = exports.COMMITTED_PATHS = exports.LOCAL_FILES = exports.EVAL_CACHE_DIR = exports.HOOK_OBSERVATIONS_FILE = exports.GUARD_LEDGER_FILE = exports.EFFECT_ACTIVE_FILE = exports.ACTIVE_AGENT_FILE = exports.ACTIVE_SKILL_FILE = exports.HOOK_STATE_DIR = exports.LEDGER_FILE = exports.COVERAGE_ARTIFACT_FILE = exports.VIGILES_DIR = void 0;
|
|
4
4
|
exports.localIgnoreEntries = localIgnoreEntries;
|
|
5
|
+
exports.entriesNotInEffect = entriesNotInEffect;
|
|
5
6
|
exports.ensureLocalFilesIgnored = ensureLocalFilesIgnored;
|
|
6
7
|
/**
|
|
7
8
|
* The files vigiles writes into a project's `.vigiles/` that describe ONE
|
|
@@ -108,6 +109,8 @@ exports.COMMITTED_PATHS = [
|
|
|
108
109
|
"action-gates.json", // declared action gates (`action-gate.ts`)
|
|
109
110
|
];
|
|
110
111
|
/** The header comment on a `.vigiles/.gitignore` vigiles creates. */
|
|
112
|
+
/** The ignore file vigiles keeps inside `.vigiles/`. One spelling for every reader. */
|
|
113
|
+
exports.LOCAL_GITIGNORE_FILE = ".gitignore";
|
|
111
114
|
exports.LOCAL_GITIGNORE_HEADER = [
|
|
112
115
|
"# Written by vigiles. These files describe one checkout on one machine and are",
|
|
113
116
|
"# never committed. Everything else in .vigiles/ is the project's: commit it.",
|
|
@@ -116,16 +119,25 @@ exports.LOCAL_GITIGNORE_HEADER = [
|
|
|
116
119
|
function localIgnoreEntries() {
|
|
117
120
|
return [
|
|
118
121
|
...exports.LOCAL_FILES.map((f) => `/${f.name}${f.dir ? "/" : ""}`),
|
|
119
|
-
|
|
122
|
+
`/${exports.LOCAL_GITIGNORE_FILE}`,
|
|
120
123
|
];
|
|
121
124
|
}
|
|
122
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Is `entry` ignoring its path after git reads `lines` top to bottom.
|
|
127
|
+
*
|
|
128
|
+
* Conservative on purpose: ANY negation after the entry's last occurrence counts
|
|
129
|
+
* as cancelling it. Git re-includes on `!/coverage.json`, but equally on
|
|
130
|
+
* `!coverage.json` or `!*.json` (last matching pattern wins), and matching
|
|
131
|
+
* gitignore globs here would be a second, partial implementation of git. The
|
|
132
|
+
* cost of being conservative is one extra append after an unrelated negation;
|
|
133
|
+
* the next call finds the entries last and leaves the file alone.
|
|
134
|
+
*/
|
|
123
135
|
function inEffect(lines, entry) {
|
|
124
136
|
let on = false;
|
|
125
137
|
for (const line of lines) {
|
|
126
138
|
if (line === entry)
|
|
127
139
|
on = true;
|
|
128
|
-
else if (line
|
|
140
|
+
else if (line.startsWith("!"))
|
|
129
141
|
on = false;
|
|
130
142
|
}
|
|
131
143
|
return on;
|
|
@@ -141,8 +153,9 @@ function inEffect(lines, entry) {
|
|
|
141
153
|
* or reordered. Appending is enough because git's last matching rule wins.
|
|
142
154
|
*
|
|
143
155
|
* "In effect" is judged the way git reads the file, not by text membership:
|
|
144
|
-
* lines in order, a later
|
|
145
|
-
* whitespace is part of the pattern (only
|
|
156
|
+
* lines in order, a later negation cancels an earlier `/entry` (see
|
|
157
|
+
* {@link inEffect}), and leading whitespace is part of the pattern (only
|
|
158
|
+
* trailing whitespace is dropped).
|
|
146
159
|
* Both cases were measured with `git check-ignore`: `/coverage.json` followed
|
|
147
160
|
* by `!/coverage.json`, and ` /coverage.json`, each leave the file NOT
|
|
148
161
|
* ignored while a trimmed set would call the entry present.
|
|
@@ -150,9 +163,18 @@ function inEffect(lines, entry) {
|
|
|
150
163
|
* Costs one read on the hot path. Best-effort: any fs error is swallowed —
|
|
151
164
|
* keeping git tidy must never break a hook, a test run or an audit.
|
|
152
165
|
*/
|
|
166
|
+
/**
|
|
167
|
+
* The entries a `.vigiles/.gitignore` with this content does NOT put in effect.
|
|
168
|
+
* One reader for both questions — "what to append" ({@link ensureLocalFilesIgnored})
|
|
169
|
+
* and "does the committed copy already carry them" (the CLI's tracked-file check).
|
|
170
|
+
*/
|
|
171
|
+
function entriesNotInEffect(content) {
|
|
172
|
+
const lines = content.split(/\r?\n/).map((l) => l.replace(/\s+$/, ""));
|
|
173
|
+
return localIgnoreEntries().filter((e) => !inEffect(lines, e));
|
|
174
|
+
}
|
|
153
175
|
function ensureLocalFilesIgnored(vigilesDir) {
|
|
154
176
|
try {
|
|
155
|
-
const file = (0, node_path_1.resolve)(vigilesDir,
|
|
177
|
+
const file = (0, node_path_1.resolve)(vigilesDir, exports.LOCAL_GITIGNORE_FILE);
|
|
156
178
|
const entries = localIgnoreEntries();
|
|
157
179
|
let current;
|
|
158
180
|
try {
|
|
@@ -166,11 +188,10 @@ function ensureLocalFilesIgnored(vigilesDir) {
|
|
|
166
188
|
(0, node_fs_1.writeFileSync)(file, [...exports.LOCAL_GITIGNORE_HEADER, ...entries, ""].join("\n"));
|
|
167
189
|
return;
|
|
168
190
|
}
|
|
169
|
-
const
|
|
170
|
-
const missing = entries.filter((e) => !inEffect(lines, e));
|
|
191
|
+
const missing = entriesNotInEffect(current);
|
|
171
192
|
if (missing.length === 0)
|
|
172
193
|
return;
|
|
173
|
-
const header =
|
|
194
|
+
const header = current.split(/\r?\n/).includes(exports.LOCAL_GITIGNORE_HEADER[0])
|
|
174
195
|
? []
|
|
175
196
|
: exports.LOCAL_GITIGNORE_HEADER;
|
|
176
197
|
const sep = current === "" || current.endsWith("\n") ? "" : "\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "30.0.
|
|
3
|
+
"version": "30.0.2",
|
|
4
4
|
"description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|