vigiles 30.0.0 → 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/agent-runtime.js +3 -1
- package/dist/adapters/claude-code/effect-region.js +3 -1
- package/dist/adapters/claude-code/run-scripts.d.ts +42 -2
- package/dist/adapters/claude-code/run-scripts.js +92 -67
- package/dist/adapters/claude-code/skill-runtime.js +3 -1
- package/dist/cli-main.js +38 -6
- package/dist/core/guards.js +3 -1
- package/dist/coverage-artifact.d.ts +3 -2
- package/dist/coverage-artifact.js +6 -5
- package/dist/eval-cache.d.ts +6 -1
- package/dist/eval-cache.js +11 -1
- package/dist/eval.js +2 -1
- package/dist/harness-resolve-hooks.d.mts +21 -0
- package/dist/harness-resolve-hooks.mjs +76 -8
- package/dist/hook-runtime.js +4 -2
- package/dist/hook-state-store.js +3 -1
- package/dist/local-files-tracked.d.ts +34 -0
- package/dist/local-files-tracked.js +112 -0
- package/dist/local-files.d.ts +71 -0
- package/dist/local-files.js +204 -0
- package/dist/observe.d.ts +3 -2
- package/dist/observe.js +7 -6
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ const node_fs_1 = require("node:fs");
|
|
|
40
40
|
const node_path_1 = require("node:path");
|
|
41
41
|
const effects_js_1 = require("../../core/effects.js");
|
|
42
42
|
const dialect_js_1 = require("./dialect.js");
|
|
43
|
+
const local_files_js_1 = require("../../local-files.js");
|
|
43
44
|
const effect_region_js_1 = require("./effect-region.js");
|
|
44
45
|
const agent_tools_js_1 = require("./agent-tools.js");
|
|
45
46
|
Object.defineProperty(exports, "parseAgentTools", { enumerable: true, get: function () { return agent_tools_js_1.parseAgentTools; } });
|
|
@@ -90,7 +91,7 @@ function decidePreToolUse(allowed, tool) {
|
|
|
90
91
|
// STACK: push on dispatch, pop on SubagentStop (back to the parent), gate on the
|
|
91
92
|
// stack TOP. Counterexample the flat model fails and the stack model passes:
|
|
92
93
|
// Open(writer); Open(writer); Stop; Call(Bash).
|
|
93
|
-
const ACTIVE_PATH =
|
|
94
|
+
const ACTIVE_PATH = `${local_files_js_1.VIGILES_DIR}/${local_files_js_1.ACTIVE_AGENT_FILE}`;
|
|
94
95
|
/**
|
|
95
96
|
* Read the active-agent stack (oldest → newest; the dispatched subagent chain).
|
|
96
97
|
* Back-compat: a legacy single-slot `{ agent: string }` marker reads as a one-frame
|
|
@@ -122,6 +123,7 @@ function writeActiveStack(cwd, stack) {
|
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
|
|
126
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)((0, node_path_1.dirname)(p));
|
|
125
127
|
(0, node_fs_1.writeFileSync)(p, JSON.stringify({ stack }) + "\n");
|
|
126
128
|
}
|
|
127
129
|
/**
|
|
@@ -12,11 +12,13 @@ exports.hasEffectBoundary = hasEffectBoundary;
|
|
|
12
12
|
*/
|
|
13
13
|
const node_fs_1 = require("node:fs");
|
|
14
14
|
const node_path_1 = require("node:path");
|
|
15
|
-
const
|
|
15
|
+
const local_files_js_1 = require("../../local-files.js");
|
|
16
|
+
const EFFECT_ACTIVE_PATH = `${local_files_js_1.VIGILES_DIR}/${local_files_js_1.EFFECT_ACTIVE_FILE}`;
|
|
16
17
|
/** Record that the agent has entered an effect boundary. */
|
|
17
18
|
function setEffectActive(cwd) {
|
|
18
19
|
const p = (0, node_path_1.resolve)(cwd, EFFECT_ACTIVE_PATH);
|
|
19
20
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
|
|
21
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)((0, node_path_1.dirname)(p));
|
|
20
22
|
(0, node_fs_1.writeFileSync)(p, JSON.stringify({ active: true }) + "\n");
|
|
21
23
|
}
|
|
22
24
|
/** Clear the effect-active marker (the agent exited the effect boundary). */
|
|
@@ -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
|
|
@@ -38,6 +38,7 @@ const node_fs_1 = require("node:fs");
|
|
|
38
38
|
const node_path_1 = require("node:path");
|
|
39
39
|
const effects_js_1 = require("../../core/effects.js");
|
|
40
40
|
const dialect_js_1 = require("./dialect.js");
|
|
41
|
+
const local_files_js_1 = require("../../local-files.js");
|
|
41
42
|
const STEP_RE = /^###\s+Step\s+(\d+)/;
|
|
42
43
|
const GATE_CMD_RE = /<!--\s*vigiles:gate\s+"([^"]*)"(?:\s+retry:(\d+))?\s*-->/;
|
|
43
44
|
const GATE_FILE_RE = /<!--\s*vigiles:gate\s+file:(\S+)\s*-->/;
|
|
@@ -235,11 +236,12 @@ function runSkillGates(gates, cwd) {
|
|
|
235
236
|
// (Claude Code hooks don't surface the active skill, so we record it). Wiring
|
|
236
237
|
// `skill-start` to fire automatically is the integration step; the decision
|
|
237
238
|
// logic below is harness-agnostic and fully testable.
|
|
238
|
-
const ACTIVE_PATH =
|
|
239
|
+
const ACTIVE_PATH = `${local_files_js_1.VIGILES_DIR}/${local_files_js_1.ACTIVE_SKILL_FILE}`;
|
|
239
240
|
/** Record the skill the agent is currently executing. */
|
|
240
241
|
function setActiveSkill(cwd, skillPath) {
|
|
241
242
|
const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
|
|
242
243
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
|
|
244
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)((0, node_path_1.dirname)(p));
|
|
243
245
|
(0, node_fs_1.writeFileSync)(p, JSON.stringify({ skill: skillPath }) + "\n");
|
|
244
246
|
}
|
|
245
247
|
/** Clear the active-skill marker (the skill finished). */
|
package/dist/cli-main.js
CHANGED
|
@@ -38,6 +38,8 @@ const surface_discovery_js_1 = require("./core/surface-discovery.js");
|
|
|
38
38
|
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
|
+
const local_files_tracked_js_1 = require("./local-files-tracked.js");
|
|
42
|
+
const local_files_js_1 = require("./local-files.js");
|
|
41
43
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
42
44
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
43
45
|
const skill_harness_js_1 = require("./skill-harness.js");
|
|
@@ -4914,6 +4916,21 @@ function gitHead(cwd) {
|
|
|
4914
4916
|
}
|
|
4915
4917
|
async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
4916
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);
|
|
4917
4934
|
// Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
|
|
4918
4935
|
const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
|
|
4919
4936
|
// The eval LOCK flags (`--check`/`--update`) are resolved BEFORE file discovery
|
|
@@ -4929,9 +4946,13 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
4929
4946
|
// A script under an excluded path is not DISCOVERED (a vendored corpus's own
|
|
4930
4947
|
// harness must not run as ours), but a script you NAME still runs (#192).
|
|
4931
4948
|
const files = (0, run_scripts_js_1.discoverScripts)(restArgs.map((p) => noteExplicitOverride(excludes, p, "running")), defaultGlob, cwd, excludes.ignore);
|
|
4932
|
-
// `--min=N`: a CI gate asserts at least N scripts actually
|
|
4933
|
-
// a renamed file,
|
|
4934
|
-
//
|
|
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.
|
|
4935
4956
|
const minFlag = args.find((a) => a.startsWith("--min="));
|
|
4936
4957
|
const minRequired = minFlag
|
|
4937
4958
|
? Math.max(0, Number.parseInt(minFlag.split("=")[1] ?? "", 10) || 0)
|
|
@@ -5035,6 +5056,12 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
5035
5056
|
// saw — the same shape as the flight-recorder ledger it already appends to.
|
|
5036
5057
|
recordRunCoverage(cwd, results, kind, harnessFlagFrom(args));
|
|
5037
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
|
+
}
|
|
5038
5065
|
if ((0, run_scripts_js_1.anyFailed)(results))
|
|
5039
5066
|
process.exit(1);
|
|
5040
5067
|
// 🔴 A SKIP THE AUTHOR NEVER DECLARED IS NOT A SKIP — and the discriminator was
|
|
@@ -5042,20 +5069,23 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
5042
5069
|
// the runtime could not evaluate exits with whatever the loader gave it, 1 in
|
|
5043
5070
|
// practice. Both are classified `"skip"` so that neither RETRACTS coverage —
|
|
5044
5071
|
// which is right, a file that did not run proved nothing either way — but only
|
|
5045
|
-
// 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`.
|
|
5046
5075
|
//
|
|
5047
5076
|
// Reported as #243: `vigiles test .` printed a resolver stack over a `⊘`, said
|
|
5048
5077
|
// `0 passed, 1 skipped`, and exited 0. Downstream a consumer's README shipped
|
|
5049
5078
|
// that exact command as its first setup step, so a new reader's suite silently
|
|
5050
5079
|
// never ran. `--no-skip` would have caught it and is not the default; `--min=1`
|
|
5051
|
-
//
|
|
5080
|
+
// did not either, because it counted files MATCHED — it now also counts the
|
|
5081
|
+
// scripts that loaded (above).
|
|
5052
5082
|
//
|
|
5053
5083
|
// This is deliberately NOT a fifth `ScriptStatus`. Coverage retraction reads the
|
|
5054
5084
|
// status as a bare STRING (`executedScripts`, `coverage-artifact.ts`, whose
|
|
5055
5085
|
// parameter is typed `string`), so a new member would start retracting silently
|
|
5056
5086
|
// with no type error — breaking the one property the classification exists to
|
|
5057
5087
|
// protect.
|
|
5058
|
-
const notEvaluated = results.filter(
|
|
5088
|
+
const notEvaluated = results.filter(run_scripts_js_1.loadFailed);
|
|
5059
5089
|
if (notEvaluated.length > 0) {
|
|
5060
5090
|
console.error(`\n✗ vigiles ${kind}: ${String(notEvaluated.length)} script(s) never ran — the runtime could not load them:\n` +
|
|
5061
5091
|
notEvaluated
|
|
@@ -5066,6 +5096,8 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
5066
5096
|
`a declared skip stays green.`);
|
|
5067
5097
|
process.exit(1);
|
|
5068
5098
|
}
|
|
5099
|
+
if (belowFloor)
|
|
5100
|
+
process.exit(1);
|
|
5069
5101
|
// `--no-skip`: in a context that ASSERTS the capability is present (a CI job),
|
|
5070
5102
|
// a skipped tier is untested surface — fail loudly instead of passing green.
|
|
5071
5103
|
if (args.includes("--no-skip") && results.some((r) => r.status === "skip")) {
|
package/dist/core/guards.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.runGuardHook = runGuardHook;
|
|
|
41
41
|
const node_fs_1 = require("node:fs");
|
|
42
42
|
const node_path_1 = require("node:path");
|
|
43
43
|
const arg_match_js_1 = require("../arg-match.js");
|
|
44
|
+
const local_files_js_1 = require("../local-files.js");
|
|
44
45
|
/** Ergonomic builders for the closed vocabulary. */
|
|
45
46
|
exports.guard = {
|
|
46
47
|
block: (target, reason) => ({
|
|
@@ -237,7 +238,7 @@ function parseGuards(json) {
|
|
|
237
238
|
// Runtime IO — load the guard set, read/append the session ledger
|
|
238
239
|
// ---------------------------------------------------------------------------
|
|
239
240
|
const GUARDS_FILE = ".vigiles/guards.json";
|
|
240
|
-
const LEDGER_FILE =
|
|
241
|
+
const LEDGER_FILE = `${local_files_js_1.VIGILES_DIR}/${local_files_js_1.GUARD_LEDGER_FILE}`;
|
|
241
242
|
/** Load the declared guard set from `.vigiles/guards.json` (absent → none). */
|
|
242
243
|
function loadGuards(cwd) {
|
|
243
244
|
const p = (0, node_path_1.resolve)(cwd, GUARDS_FILE);
|
|
@@ -273,6 +274,7 @@ function recordGuardCall(cwd, event) {
|
|
|
273
274
|
const calls = readGuardLedger(cwd);
|
|
274
275
|
calls.push({ tool: event.tool, input: event.input });
|
|
275
276
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
|
|
277
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)((0, node_path_1.dirname)(p));
|
|
276
278
|
(0, node_fs_1.writeFileSync)(p, JSON.stringify({ calls }, null, 2));
|
|
277
279
|
}
|
|
278
280
|
/** Parse a PreToolUse event JSON (the hook's stdin) into a {@link ToolEvent}. */
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ProbeOrigin, SurfaceProbe } from "./check-count.js";
|
|
2
|
+
import { COVERAGE_ARTIFACT_FILE } from "./local-files.js";
|
|
2
3
|
import type { Surface, SurfaceKind } from "./test-coverage.js";
|
|
3
4
|
/** Bumped when the record shape changes in a non-additive way. */
|
|
4
5
|
export declare const COVERAGE_ARTIFACT_VERSION = 1;
|
|
5
|
-
/** The artifact filename under `.vigiles
|
|
6
|
-
export
|
|
6
|
+
/** The artifact filename under `.vigiles/` — defined on the one local-files list. */
|
|
7
|
+
export { COVERAGE_ARTIFACT_FILE };
|
|
7
8
|
/**
|
|
8
9
|
* Which runner produced a record. Mirrors the discovery split in
|
|
9
10
|
* `test-coverage.ts`: `vigiles test` runs `*.harness.*` (free, every push),
|
|
@@ -58,10 +58,10 @@ const node_crypto_1 = require("node:crypto");
|
|
|
58
58
|
const node_fs_1 = require("node:fs");
|
|
59
59
|
const node_path_1 = require("node:path");
|
|
60
60
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
61
|
+
const local_files_js_1 = require("./local-files.js");
|
|
62
|
+
Object.defineProperty(exports, "COVERAGE_ARTIFACT_FILE", { enumerable: true, get: function () { return local_files_js_1.COVERAGE_ARTIFACT_FILE; } });
|
|
61
63
|
/** Bumped when the record shape changes in a non-additive way. */
|
|
62
64
|
exports.COVERAGE_ARTIFACT_VERSION = 1;
|
|
63
|
-
/** The artifact filename under `.vigiles/`. */
|
|
64
|
-
exports.COVERAGE_ARTIFACT_FILE = "coverage.json";
|
|
65
65
|
/**
|
|
66
66
|
* Every {@link RunAttribution}, as data — the artifact's validator reads it, so
|
|
67
67
|
* the accepted set and the type cannot drift apart into a record shape the
|
|
@@ -576,7 +576,7 @@ function mergeRuns(previous, next, executed) {
|
|
|
576
576
|
}
|
|
577
577
|
/** Read the artifact, or `undefined` when there is none / it is not one. */
|
|
578
578
|
function readCoverageArtifact(root) {
|
|
579
|
-
const file = (0, node_path_1.resolve)(root,
|
|
579
|
+
const file = (0, node_path_1.resolve)(root, local_files_js_1.VIGILES_DIR, local_files_js_1.COVERAGE_ARTIFACT_FILE);
|
|
580
580
|
if (!(0, node_fs_1.existsSync)(file))
|
|
581
581
|
return undefined;
|
|
582
582
|
let value;
|
|
@@ -618,9 +618,10 @@ function isCoverageRun(value) {
|
|
|
618
618
|
/** Write the artifact. Best-effort: recording must never fail a green run. */
|
|
619
619
|
function writeCoverageArtifact(root, artifact) {
|
|
620
620
|
try {
|
|
621
|
-
const dir = (0, node_path_1.resolve)(root,
|
|
621
|
+
const dir = (0, node_path_1.resolve)(root, local_files_js_1.VIGILES_DIR);
|
|
622
622
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
623
|
-
(0,
|
|
623
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)(dir);
|
|
624
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(dir, local_files_js_1.COVERAGE_ARTIFACT_FILE), JSON.stringify(artifact, null, 2) + "\n");
|
|
624
625
|
}
|
|
625
626
|
catch {
|
|
626
627
|
/* best-effort — a scratch-dir failure is not a test failure */
|
package/dist/eval-cache.d.ts
CHANGED
|
@@ -74,7 +74,12 @@ export declare function cacheKey(input: CacheKeyInput): SHA256Hash;
|
|
|
74
74
|
* gate must surface, not mask. The message tells you how to recover.
|
|
75
75
|
*/
|
|
76
76
|
export declare function readCache(dir: string, key: SHA256Hash): CacheRecord | null;
|
|
77
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Write a cached record by key (creating the cache dir as needed). When `dir` is
|
|
79
|
+
* the default `.vigiles/eval-cache`, the recorded runs are per-checkout, so the
|
|
80
|
+
* `.vigiles/.gitignore` is kept current; a `cacheDir` the spec chose elsewhere is
|
|
81
|
+
* the author's to place and is left alone.
|
|
82
|
+
*/
|
|
78
83
|
export declare function writeCache(dir: string, key: SHA256Hash, record: CacheRecord): void;
|
|
79
84
|
/** Snapshot the text files under `cwd` as `relativePath → contents` (bounded). */
|
|
80
85
|
export declare function snapshotDir(cwd: string): Record<string, string>;
|
package/dist/eval-cache.js
CHANGED
|
@@ -28,6 +28,7 @@ exports.hashDir = hashDir;
|
|
|
28
28
|
const node_fs_1 = require("node:fs");
|
|
29
29
|
const node_path_1 = require("node:path");
|
|
30
30
|
const hash_js_1 = require("./core/hash.js");
|
|
31
|
+
const local_files_js_1 = require("./local-files.js");
|
|
31
32
|
const MAX_SNAPSHOT_FILE_BYTES = 1024 * 1024;
|
|
32
33
|
const SKIP_DIRS = new Set(["node_modules", ".git"]);
|
|
33
34
|
/**
|
|
@@ -113,9 +114,18 @@ function readCache(dir, key) {
|
|
|
113
114
|
throw new Error(`eval cache: corrupt record ${path} (invalid JSON) — delete it or clear the cache dir`);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Write a cached record by key (creating the cache dir as needed). When `dir` is
|
|
119
|
+
* the default `.vigiles/eval-cache`, the recorded runs are per-checkout, so the
|
|
120
|
+
* `.vigiles/.gitignore` is kept current; a `cacheDir` the spec chose elsewhere is
|
|
121
|
+
* the author's to place and is left alone.
|
|
122
|
+
*/
|
|
117
123
|
function writeCache(dir, key, record) {
|
|
118
124
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
125
|
+
if ((0, node_path_1.basename)(dir) === local_files_js_1.EVAL_CACHE_DIR &&
|
|
126
|
+
(0, node_path_1.basename)((0, node_path_1.dirname)(dir)) === local_files_js_1.VIGILES_DIR) {
|
|
127
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)((0, node_path_1.dirname)(dir));
|
|
128
|
+
}
|
|
119
129
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, `${key}.json`), JSON.stringify(record));
|
|
120
130
|
}
|
|
121
131
|
/** Snapshot the text files under `cwd` as `relativePath → contents` (bounded). */
|
package/dist/eval.js
CHANGED
|
@@ -79,6 +79,7 @@ const proofs_js_1 = require("./core/proofs.js");
|
|
|
79
79
|
const model_access_js_1 = require("./adapters/claude-code/model-access.js");
|
|
80
80
|
const harness_test_js_1 = require("./harness-test.js");
|
|
81
81
|
const eval_cache_js_1 = require("./eval-cache.js");
|
|
82
|
+
const local_files_js_1 = require("./local-files.js");
|
|
82
83
|
const eval_lock_js_1 = require("./eval-lock.js");
|
|
83
84
|
const stats_js_1 = require("./stats.js");
|
|
84
85
|
const check_count_js_1 = require("./check-count.js");
|
|
@@ -1331,7 +1332,7 @@ async function runEvalWith(spec, runner) {
|
|
|
1331
1332
|
tools: spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"],
|
|
1332
1333
|
timeoutMs: spec.timeoutMs ?? 240000,
|
|
1333
1334
|
cache: spec.cache ?? "off",
|
|
1334
|
-
cacheDir: spec.cacheDir ?? (0, node_path_1.resolve)(process.cwd(),
|
|
1335
|
+
cacheDir: spec.cacheDir ?? (0, node_path_1.resolve)(process.cwd(), local_files_js_1.VIGILES_DIR, local_files_js_1.EVAL_CACHE_DIR),
|
|
1335
1336
|
};
|
|
1336
1337
|
if (cfg.cache !== "off" && !isDatedModel(cfg.model)) {
|
|
1337
1338
|
warnFloatingModel(cfg.model);
|
|
@@ -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
|
package/dist/hook-runtime.js
CHANGED
|
@@ -50,6 +50,7 @@ const hook_install_js_1 = require("./hook-install.js");
|
|
|
50
50
|
const hook_providers_js_1 = require("./core/hook-providers.js");
|
|
51
51
|
const hook_state_store_js_1 = require("./hook-state-store.js");
|
|
52
52
|
const observe_js_1 = require("./observe.js");
|
|
53
|
+
const local_files_js_1 = require("./local-files.js");
|
|
53
54
|
const load_hook_js_1 = require("./load-hook.js");
|
|
54
55
|
const event_capability_js_1 = require("./core/event-capability.js");
|
|
55
56
|
/**
|
|
@@ -168,8 +169,9 @@ async function loadProviderRegistry(root) {
|
|
|
168
169
|
/** Append an observe-mode record to `.vigiles/hook-observations.jsonl` (best-effort). */
|
|
169
170
|
function recordObservation(file, on, would, reason, root) {
|
|
170
171
|
try {
|
|
171
|
-
const dir = (0, node_path_1.resolve)(root,
|
|
172
|
+
const dir = (0, node_path_1.resolve)(root, local_files_js_1.VIGILES_DIR);
|
|
172
173
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
174
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)(dir);
|
|
173
175
|
const line = JSON.stringify({
|
|
174
176
|
ts: new Date().toISOString(),
|
|
175
177
|
hook: file,
|
|
@@ -177,7 +179,7 @@ function recordObservation(file, on, would, reason, root) {
|
|
|
177
179
|
would,
|
|
178
180
|
reason,
|
|
179
181
|
}) + "\n";
|
|
180
|
-
(0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(dir,
|
|
182
|
+
(0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(dir, local_files_js_1.HOOK_OBSERVATIONS_FILE), line);
|
|
181
183
|
}
|
|
182
184
|
catch {
|
|
183
185
|
/* recording is best-effort — never let it break a live session */
|
package/dist/hook-state-store.js
CHANGED
|
@@ -57,6 +57,7 @@ const node_path_1 = require("node:path");
|
|
|
57
57
|
const hash_js_1 = require("./core/hash.js");
|
|
58
58
|
const hook_state_js_1 = require("./core/hook-state.js");
|
|
59
59
|
const hook_install_js_1 = require("./hook-install.js");
|
|
60
|
+
const local_files_js_1 = require("./local-files.js");
|
|
60
61
|
/**
|
|
61
62
|
* The directory a hook's recorded facts live in — the SCOPE of `state()`/`record()`.
|
|
62
63
|
*
|
|
@@ -78,7 +79,7 @@ function hookStateDir(file, cwd = process.cwd()) {
|
|
|
78
79
|
const dir = (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file));
|
|
79
80
|
const rel = (0, node_path_1.relative)(cwd, dir);
|
|
80
81
|
const inside = rel !== "" && !rel.startsWith("..") && !(0, node_path_1.isAbsolute)(rel);
|
|
81
|
-
return (0, node_path_1.resolve)(cwd,
|
|
82
|
+
return (0, node_path_1.resolve)(cwd, local_files_js_1.VIGILES_DIR, local_files_js_1.HOOK_STATE_DIR, inside ? rel : `external-${(0, hash_js_1.sha256short)(dir)}`);
|
|
82
83
|
}
|
|
83
84
|
/** Read one recorded fact for a hook, or `null` if it was never recorded. */
|
|
84
85
|
function readHookState(file, key, cwd = process.cwd()) {
|
|
@@ -112,6 +113,7 @@ function writeHookState(file, w, opts = {}) {
|
|
|
112
113
|
by: (0, hook_install_js_1.normalizeHookRef)(file, cwd),
|
|
113
114
|
};
|
|
114
115
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
116
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)((0, node_path_1.resolve)(cwd, local_files_js_1.VIGILES_DIR));
|
|
115
117
|
const tmp = `${target}.${String(process.pid)}.tmp`;
|
|
116
118
|
(0, node_fs_1.writeFileSync)(tmp, JSON.stringify(entry, null, 2) + "\n");
|
|
117
119
|
(0, node_fs_1.renameSync)(tmp, target);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-checkout files under `<root>/.vigiles/` that git tracks, as
|
|
3
|
+
* repo-relative paths from `root`. Empty when nothing is tracked, when `root` is
|
|
4
|
+
* not in a git work tree, or when `git` is missing — silence is the answer to
|
|
5
|
+
* every "cannot tell".
|
|
6
|
+
*/
|
|
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;
|
|
25
|
+
/**
|
|
26
|
+
* The one-line warning for {@link trackedLocalFiles}, or `null` when there is
|
|
27
|
+
* nothing to say. Files inside a tracked local DIRECTORY (`state/`,
|
|
28
|
+
* `eval-cache/`) are named by that directory, so one line stays one line and the
|
|
29
|
+
* command it prints untracks all of them.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatTrackedLocalFiles(tracked: readonly string[], committedLacksEntries?: boolean): string | null;
|
|
32
|
+
/** Print the warning for `root` to stderr when there is one. CLI-only. */
|
|
33
|
+
export declare function warnTrackedLocalFiles(root: string): void;
|
|
34
|
+
//# sourceMappingURL=local-files-tracked.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trackedLocalFiles = trackedLocalFiles;
|
|
4
|
+
exports.committedIgnoreFileLacksEntries = committedIgnoreFileLacksEntries;
|
|
5
|
+
exports.formatTrackedLocalFiles = formatTrackedLocalFiles;
|
|
6
|
+
exports.warnTrackedLocalFiles = warnTrackedLocalFiles;
|
|
7
|
+
/**
|
|
8
|
+
* The half `.gitignore` cannot do: tell the owner when a per-checkout file under
|
|
9
|
+
* `.vigiles/` is ALREADY tracked.
|
|
10
|
+
*
|
|
11
|
+
* `ensureLocalFilesIgnored` (`local-files.ts`) keeps new copies out of git, but
|
|
12
|
+
* an ignore rule does not untrack a file git already follows — a repo that
|
|
13
|
+
* committed `.vigiles/coverage.json` before this existed keeps committing every
|
|
14
|
+
* rewrite of it. Only the owner can fix that (`git rm --cached`), and changing
|
|
15
|
+
* the index is theirs to decide, so this reports and never acts.
|
|
16
|
+
*
|
|
17
|
+
* CLI-only, by construction: it spawns `git`, and a hook decision must never pay
|
|
18
|
+
* for a child process. That is why it is a separate module from the ignore half,
|
|
19
|
+
* which hook runtimes import.
|
|
20
|
+
*/
|
|
21
|
+
const node_child_process_1 = require("node:child_process");
|
|
22
|
+
const node_path_1 = require("node:path");
|
|
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);
|
|
26
|
+
/**
|
|
27
|
+
* The per-checkout files under `<root>/.vigiles/` that git tracks, as
|
|
28
|
+
* repo-relative paths from `root`. Empty when nothing is tracked, when `root` is
|
|
29
|
+
* not in a git work tree, or when `git` is missing — silence is the answer to
|
|
30
|
+
* every "cannot tell".
|
|
31
|
+
*/
|
|
32
|
+
function trackedLocalFiles(root) {
|
|
33
|
+
const specs = local_files_js_1.LOCAL_FILES.map((f) => node_path_1.posix.join(local_files_js_1.VIGILES_DIR, f.name));
|
|
34
|
+
try {
|
|
35
|
+
const r = (0, node_child_process_1.spawnSync)("git", ["ls-files", "-z", "--", ...specs], {
|
|
36
|
+
cwd: root,
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
39
|
+
});
|
|
40
|
+
if (r.status !== 0 || typeof r.stdout !== "string")
|
|
41
|
+
return [];
|
|
42
|
+
return r.stdout.split("\0").filter((p) => p !== "");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
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
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The one-line warning for {@link trackedLocalFiles}, or `null` when there is
|
|
81
|
+
* nothing to say. Files inside a tracked local DIRECTORY (`state/`,
|
|
82
|
+
* `eval-cache/`) are named by that directory, so one line stays one line and the
|
|
83
|
+
* command it prints untracks all of them.
|
|
84
|
+
*/
|
|
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;
|
|
105
|
+
}
|
|
106
|
+
/** Print the warning for `root` to stderr when there is one. CLI-only. */
|
|
107
|
+
function warnTrackedLocalFiles(root) {
|
|
108
|
+
const line = formatTrackedLocalFiles(trackedLocalFiles(root), committedIgnoreFileLacksEntries(root));
|
|
109
|
+
if (line)
|
|
110
|
+
process.stderr.write(line + "\n");
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=local-files-tracked.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** vigiles's own directory in a project. */
|
|
2
|
+
export declare const VIGILES_DIR = ".vigiles";
|
|
3
|
+
/** The execution tier of coverage (`coverage-artifact.ts`). */
|
|
4
|
+
export declare const COVERAGE_ARTIFACT_FILE = "coverage.json";
|
|
5
|
+
/** The flight-recorder ledger (`observe.ts`). */
|
|
6
|
+
export declare const LEDGER_FILE = "runs.jsonl";
|
|
7
|
+
/** Compiled hooks' named state, `record()`/`state()` (`hook-state-store.ts`). */
|
|
8
|
+
export declare const HOOK_STATE_DIR = "state";
|
|
9
|
+
/** The skill in progress (`adapters/claude-code/skill-runtime.ts`). */
|
|
10
|
+
export declare const ACTIVE_SKILL_FILE = "active-skill.json";
|
|
11
|
+
/** The subagent stack in progress (`adapters/claude-code/agent-runtime.ts`). */
|
|
12
|
+
export declare const ACTIVE_AGENT_FILE = "active-agent.json";
|
|
13
|
+
/** Inside-an-effect-boundary marker (`adapters/claude-code/effect-region.ts`). */
|
|
14
|
+
export declare const EFFECT_ACTIVE_FILE = "effect-active.json";
|
|
15
|
+
/** The calls a guard allowed this session (`core/guards.ts`). */
|
|
16
|
+
export declare const GUARD_LEDGER_FILE = "guard-ledger.json";
|
|
17
|
+
/** What an `observe`-mode hook would have blocked (`hook-runtime.ts`). */
|
|
18
|
+
export declare const HOOK_OBSERVATIONS_FILE = "hook-observations.jsonl";
|
|
19
|
+
/** Recorded model runs for eval replay, the default `cacheDir` (`eval.ts`). */
|
|
20
|
+
export declare const EVAL_CACHE_DIR = "eval-cache";
|
|
21
|
+
/** One per-checkout path under `.vigiles/`. */
|
|
22
|
+
export interface LocalFile {
|
|
23
|
+
/** The name directly under `.vigiles/`. */
|
|
24
|
+
readonly name: string;
|
|
25
|
+
/** A directory (its whole subtree is local) rather than a single file. */
|
|
26
|
+
readonly dir: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Every per-checkout path vigiles writes under `.vigiles/`. The one list. */
|
|
29
|
+
export declare const LOCAL_FILES: readonly LocalFile[];
|
|
30
|
+
/**
|
|
31
|
+
* The names under `.vigiles/` that ARE the project's and must stay committable.
|
|
32
|
+
* Not used at runtime — it is the other half of the classification the guard in
|
|
33
|
+
* `local-files.test.ts` holds the source to, and the list that test checks is
|
|
34
|
+
* NOT ignored.
|
|
35
|
+
*/
|
|
36
|
+
export declare const COMMITTED_PATHS: readonly string[];
|
|
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";
|
|
40
|
+
export declare const LOCAL_GITIGNORE_HEADER: readonly string[];
|
|
41
|
+
/** The ignore lines, in order: every local path, then the file itself. */
|
|
42
|
+
export declare function localIgnoreEntries(): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Keep `<vigilesDir>/.gitignore` listing every local path. Call it where a
|
|
45
|
+
* local file is written — not from `vigiles init`, which a project may never
|
|
46
|
+
* run, while the write always happens.
|
|
47
|
+
*
|
|
48
|
+
* - Absent → created with {@link LOCAL_GITIGNORE_HEADER} and every entry.
|
|
49
|
+
* - Every entry IN EFFECT → not touched (no rewrite, no mtime change).
|
|
50
|
+
* - Otherwise the entries not in effect are appended and nothing is removed
|
|
51
|
+
* or reordered. Appending is enough because git's last matching rule wins.
|
|
52
|
+
*
|
|
53
|
+
* "In effect" is judged the way git reads the file, not by text membership:
|
|
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).
|
|
57
|
+
* Both cases were measured with `git check-ignore`: `/coverage.json` followed
|
|
58
|
+
* by `!/coverage.json`, and ` /coverage.json`, each leave the file NOT
|
|
59
|
+
* ignored while a trimmed set would call the entry present.
|
|
60
|
+
*
|
|
61
|
+
* Costs one read on the hot path. Best-effort: any fs error is swallowed —
|
|
62
|
+
* keeping git tidy must never break a hook, a test run or an audit.
|
|
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[];
|
|
70
|
+
export declare function ensureLocalFilesIgnored(vigilesDir: string): void;
|
|
71
|
+
//# sourceMappingURL=local-files.d.ts.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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
|
+
exports.localIgnoreEntries = localIgnoreEntries;
|
|
5
|
+
exports.entriesNotInEffect = entriesNotInEffect;
|
|
6
|
+
exports.ensureLocalFilesIgnored = ensureLocalFilesIgnored;
|
|
7
|
+
/**
|
|
8
|
+
* The files vigiles writes into a project's `.vigiles/` that describe ONE
|
|
9
|
+
* checkout on ONE machine — and the `.gitignore` that keeps them out of git.
|
|
10
|
+
*
|
|
11
|
+
* `.vigiles/` holds two kinds of file. Some are the project's own and are
|
|
12
|
+
* committed: hook sources and their stamps (`hooks/`), eval locks
|
|
13
|
+
* (`eval-locks/`), providers, the eval baseline, spec sidecars. The rest are a
|
|
14
|
+
* record of what happened HERE — which run exercised which surface, which skill
|
|
15
|
+
* is active right now, when a throttled hook last spoke — and committing one
|
|
16
|
+
* transplants that record onto a machine where none of it happened. A committed
|
|
17
|
+
* `coverage.json` credits coverage nobody ran; a committed `state/` silences a
|
|
18
|
+
* teammate's hook for a window they never saw; a committed `guard-ledger.json`
|
|
19
|
+
* satisfies a `requireBefore` guard with a prerequisite someone else ran.
|
|
20
|
+
*
|
|
21
|
+
* ## Why this file exists
|
|
22
|
+
*
|
|
23
|
+
* The rule "do not commit these" lived as one sentence in
|
|
24
|
+
* `docs/rules/untested-skill.md`, and vigiles honoured it only in its OWN repo's
|
|
25
|
+
* root `.gitignore`. Consumers committed them anyway: two repos carried
|
|
26
|
+
* `.vigiles/coverage.json` for weeks, one built a `merge=ours` git driver to
|
|
27
|
+
* paper over the conflicts it caused, and the other shipped `coverage.json` and
|
|
28
|
+
* `runs.jsonl` inside its npm tarball. A rule that exists only as prose enforces
|
|
29
|
+
* nothing, so the writer now enforces it where the write happens.
|
|
30
|
+
*
|
|
31
|
+
* ## The mechanism: a `.gitignore` INSIDE `.vigiles/`
|
|
32
|
+
*
|
|
33
|
+
* Tools that keep local state in a project leave the project's root
|
|
34
|
+
* `.gitignore` alone and drop one inside their own directory — `ruff` writes
|
|
35
|
+
* `.ruff_cache/.gitignore` containing `*`. `*` is wrong here, because the same
|
|
36
|
+
* directory holds committed files, so the file lists the local paths instead,
|
|
37
|
+
* each anchored with a leading `/` so it cannot match a same-named file deeper
|
|
38
|
+
* down. It also lists ITSELF, like ruff's `*` covers its own file: it then
|
|
39
|
+
* leaves no footprint in the project's history and never churns on upgrade.
|
|
40
|
+
*
|
|
41
|
+
* `.gitignore` does not untrack a file that is already tracked. That half is
|
|
42
|
+
* `local-files-tracked.ts`, which runs from the CLI only — it spawns `git`, and
|
|
43
|
+
* nothing here may, because {@link ensureLocalFilesIgnored} is called from hook
|
|
44
|
+
* runtimes on the decision path.
|
|
45
|
+
*
|
|
46
|
+
* ## One list, and the constants live IN it
|
|
47
|
+
*
|
|
48
|
+
* Every writer imports its file name from here rather than spelling it, so a
|
|
49
|
+
* name cannot drift from the ignore entry. The constants were defined in their
|
|
50
|
+
* writer modules before; the direction is inverted on purpose. With the list
|
|
51
|
+
* importing from the writers, each writer would import this module back (for
|
|
52
|
+
* the ensure call), a cycle in which a top-level list reads a `const` still in
|
|
53
|
+
* its temporal dead zone — and a hook deciding a `Bash` call would load
|
|
54
|
+
* `eval.ts` and `coverage-artifact.ts` just to learn two file names. This module
|
|
55
|
+
* imports nothing from the package, so every writer can depend on it for free.
|
|
56
|
+
* `local-files.test.ts` guards the other direction: every `.vigiles/` path the
|
|
57
|
+
* source spells must be either on this list or on {@link COMMITTED_PATHS}.
|
|
58
|
+
*
|
|
59
|
+
* Node-only (it reads and writes a file).
|
|
60
|
+
*/
|
|
61
|
+
const node_fs_1 = require("node:fs");
|
|
62
|
+
const node_path_1 = require("node:path");
|
|
63
|
+
/** vigiles's own directory in a project. */
|
|
64
|
+
exports.VIGILES_DIR = ".vigiles";
|
|
65
|
+
/** The execution tier of coverage (`coverage-artifact.ts`). */
|
|
66
|
+
exports.COVERAGE_ARTIFACT_FILE = "coverage.json";
|
|
67
|
+
/** The flight-recorder ledger (`observe.ts`). */
|
|
68
|
+
exports.LEDGER_FILE = "runs.jsonl";
|
|
69
|
+
/** Compiled hooks' named state, `record()`/`state()` (`hook-state-store.ts`). */
|
|
70
|
+
exports.HOOK_STATE_DIR = "state";
|
|
71
|
+
/** The skill in progress (`adapters/claude-code/skill-runtime.ts`). */
|
|
72
|
+
exports.ACTIVE_SKILL_FILE = "active-skill.json";
|
|
73
|
+
/** The subagent stack in progress (`adapters/claude-code/agent-runtime.ts`). */
|
|
74
|
+
exports.ACTIVE_AGENT_FILE = "active-agent.json";
|
|
75
|
+
/** Inside-an-effect-boundary marker (`adapters/claude-code/effect-region.ts`). */
|
|
76
|
+
exports.EFFECT_ACTIVE_FILE = "effect-active.json";
|
|
77
|
+
/** The calls a guard allowed this session (`core/guards.ts`). */
|
|
78
|
+
exports.GUARD_LEDGER_FILE = "guard-ledger.json";
|
|
79
|
+
/** What an `observe`-mode hook would have blocked (`hook-runtime.ts`). */
|
|
80
|
+
exports.HOOK_OBSERVATIONS_FILE = "hook-observations.jsonl";
|
|
81
|
+
/** Recorded model runs for eval replay, the default `cacheDir` (`eval.ts`). */
|
|
82
|
+
exports.EVAL_CACHE_DIR = "eval-cache";
|
|
83
|
+
/** Every per-checkout path vigiles writes under `.vigiles/`. The one list. */
|
|
84
|
+
exports.LOCAL_FILES = [
|
|
85
|
+
{ name: exports.COVERAGE_ARTIFACT_FILE, dir: false },
|
|
86
|
+
{ name: exports.LEDGER_FILE, dir: false },
|
|
87
|
+
{ name: exports.HOOK_STATE_DIR, dir: true },
|
|
88
|
+
{ name: exports.ACTIVE_SKILL_FILE, dir: false },
|
|
89
|
+
{ name: exports.ACTIVE_AGENT_FILE, dir: false },
|
|
90
|
+
{ name: exports.EFFECT_ACTIVE_FILE, dir: false },
|
|
91
|
+
{ name: exports.GUARD_LEDGER_FILE, dir: false },
|
|
92
|
+
{ name: exports.HOOK_OBSERVATIONS_FILE, dir: false },
|
|
93
|
+
{ name: exports.EVAL_CACHE_DIR, dir: true },
|
|
94
|
+
];
|
|
95
|
+
/**
|
|
96
|
+
* The names under `.vigiles/` that ARE the project's and must stay committable.
|
|
97
|
+
* Not used at runtime — it is the other half of the classification the guard in
|
|
98
|
+
* `local-files.test.ts` holds the source to, and the list that test checks is
|
|
99
|
+
* NOT ignored.
|
|
100
|
+
*/
|
|
101
|
+
exports.COMMITTED_PATHS = [
|
|
102
|
+
"hooks", // hook sources + their stamps (`hook-install.ts`)
|
|
103
|
+
"providers", // registered hook-context providers
|
|
104
|
+
"eval-locks", // committed eval staleness stamps (`eval-lock.ts`)
|
|
105
|
+
"eval-baseline.json", // the committed regression baseline (`eval-baseline.ts`)
|
|
106
|
+
"generated.d.ts", // linter-rule types for specs (`vigiles init` / `generate types`)
|
|
107
|
+
"schema.json", // YAML-LSP frontmatter schema (`vigiles init`)
|
|
108
|
+
"guards.json", // the declared guard set (`core/guards.ts`)
|
|
109
|
+
"action-gates.json", // declared action gates (`action-gate.ts`)
|
|
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";
|
|
114
|
+
exports.LOCAL_GITIGNORE_HEADER = [
|
|
115
|
+
"# Written by vigiles. These files describe one checkout on one machine and are",
|
|
116
|
+
"# never committed. Everything else in .vigiles/ is the project's: commit it.",
|
|
117
|
+
];
|
|
118
|
+
/** The ignore lines, in order: every local path, then the file itself. */
|
|
119
|
+
function localIgnoreEntries() {
|
|
120
|
+
return [
|
|
121
|
+
...exports.LOCAL_FILES.map((f) => `/${f.name}${f.dir ? "/" : ""}`),
|
|
122
|
+
`/${exports.LOCAL_GITIGNORE_FILE}`,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
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
|
+
*/
|
|
135
|
+
function inEffect(lines, entry) {
|
|
136
|
+
let on = false;
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
if (line === entry)
|
|
139
|
+
on = true;
|
|
140
|
+
else if (line.startsWith("!"))
|
|
141
|
+
on = false;
|
|
142
|
+
}
|
|
143
|
+
return on;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Keep `<vigilesDir>/.gitignore` listing every local path. Call it where a
|
|
147
|
+
* local file is written — not from `vigiles init`, which a project may never
|
|
148
|
+
* run, while the write always happens.
|
|
149
|
+
*
|
|
150
|
+
* - Absent → created with {@link LOCAL_GITIGNORE_HEADER} and every entry.
|
|
151
|
+
* - Every entry IN EFFECT → not touched (no rewrite, no mtime change).
|
|
152
|
+
* - Otherwise the entries not in effect are appended and nothing is removed
|
|
153
|
+
* or reordered. Appending is enough because git's last matching rule wins.
|
|
154
|
+
*
|
|
155
|
+
* "In effect" is judged the way git reads the file, not by text membership:
|
|
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).
|
|
159
|
+
* Both cases were measured with `git check-ignore`: `/coverage.json` followed
|
|
160
|
+
* by `!/coverage.json`, and ` /coverage.json`, each leave the file NOT
|
|
161
|
+
* ignored while a trimmed set would call the entry present.
|
|
162
|
+
*
|
|
163
|
+
* Costs one read on the hot path. Best-effort: any fs error is swallowed —
|
|
164
|
+
* keeping git tidy must never break a hook, a test run or an audit.
|
|
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
|
+
}
|
|
175
|
+
function ensureLocalFilesIgnored(vigilesDir) {
|
|
176
|
+
try {
|
|
177
|
+
const file = (0, node_path_1.resolve)(vigilesDir, exports.LOCAL_GITIGNORE_FILE);
|
|
178
|
+
const entries = localIgnoreEntries();
|
|
179
|
+
let current;
|
|
180
|
+
try {
|
|
181
|
+
current = (0, node_fs_1.readFileSync)(file, "utf-8");
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
current = undefined;
|
|
185
|
+
}
|
|
186
|
+
if (current === undefined) {
|
|
187
|
+
(0, node_fs_1.mkdirSync)(vigilesDir, { recursive: true });
|
|
188
|
+
(0, node_fs_1.writeFileSync)(file, [...exports.LOCAL_GITIGNORE_HEADER, ...entries, ""].join("\n"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const missing = entriesNotInEffect(current);
|
|
192
|
+
if (missing.length === 0)
|
|
193
|
+
return;
|
|
194
|
+
const header = current.split(/\r?\n/).includes(exports.LOCAL_GITIGNORE_HEADER[0])
|
|
195
|
+
? []
|
|
196
|
+
: exports.LOCAL_GITIGNORE_HEADER;
|
|
197
|
+
const sep = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
198
|
+
(0, node_fs_1.appendFileSync)(file, sep + [...header, ...missing, ""].join("\n"));
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* best-effort — an unwritable .vigiles/ is not a failure of anything */
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=local-files.js.map
|
package/dist/observe.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { LEDGER_FILE } from "./local-files.js";
|
|
1
2
|
/** Bumped when the record shape changes in a non-additive way. */
|
|
2
3
|
export declare const OBSERVE_VERSION = 1;
|
|
3
|
-
/** The ledger filename under the `.vigiles/` directory. */
|
|
4
|
-
export
|
|
4
|
+
/** The ledger filename under the `.vigiles/` directory — defined on the one local-files list. */
|
|
5
|
+
export { LEDGER_FILE };
|
|
5
6
|
/** Fields every record carries; `v`/`ts` are stamped by the writer, not the caller. */
|
|
6
7
|
export interface ObservationBase {
|
|
7
8
|
/** schema version (`OBSERVE_VERSION`) */
|
package/dist/observe.js
CHANGED
|
@@ -24,10 +24,10 @@ exports.formatLedgerSummary = formatLedgerSummary;
|
|
|
24
24
|
*/
|
|
25
25
|
const node_fs_1 = require("node:fs");
|
|
26
26
|
const node_path_1 = require("node:path");
|
|
27
|
+
const local_files_js_1 = require("./local-files.js");
|
|
28
|
+
Object.defineProperty(exports, "LEDGER_FILE", { enumerable: true, get: function () { return local_files_js_1.LEDGER_FILE; } });
|
|
27
29
|
/** Bumped when the record shape changes in a non-additive way. */
|
|
28
30
|
exports.OBSERVE_VERSION = 1;
|
|
29
|
-
/** The ledger filename under the `.vigiles/` directory. */
|
|
30
|
-
exports.LEDGER_FILE = "runs.jsonl";
|
|
31
31
|
/** Serialize one record to a single JSONL line (trailing newline included). */
|
|
32
32
|
function formatObservation(record) {
|
|
33
33
|
return JSON.stringify(record) + "\n";
|
|
@@ -39,14 +39,15 @@ function formatObservation(record) {
|
|
|
39
39
|
*/
|
|
40
40
|
function appendObservation(input, cwd = process.cwd()) {
|
|
41
41
|
try {
|
|
42
|
-
const dir = (0, node_path_1.resolve)(cwd,
|
|
42
|
+
const dir = (0, node_path_1.resolve)(cwd, local_files_js_1.VIGILES_DIR);
|
|
43
43
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
44
|
+
(0, local_files_js_1.ensureLocalFilesIgnored)(dir);
|
|
44
45
|
const record = {
|
|
45
46
|
v: exports.OBSERVE_VERSION,
|
|
46
47
|
ts: new Date().toISOString(),
|
|
47
48
|
...input,
|
|
48
49
|
};
|
|
49
|
-
(0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(dir,
|
|
50
|
+
(0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(dir, local_files_js_1.LEDGER_FILE), formatObservation(record));
|
|
50
51
|
}
|
|
51
52
|
catch {
|
|
52
53
|
/* best-effort — recording is never allowed to break a session */
|
|
@@ -59,7 +60,7 @@ function appendObservation(input, cwd = process.cwd()) {
|
|
|
59
60
|
function readObservations(cwd = process.cwd()) {
|
|
60
61
|
let raw;
|
|
61
62
|
try {
|
|
62
|
-
raw = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd,
|
|
63
|
+
raw = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, local_files_js_1.VIGILES_DIR, local_files_js_1.LEDGER_FILE), "utf8");
|
|
63
64
|
}
|
|
64
65
|
catch {
|
|
65
66
|
return [];
|
|
@@ -129,7 +130,7 @@ function formatLedgerSummary(records, committedLocks) {
|
|
|
129
130
|
if (records.length === 0)
|
|
130
131
|
return "";
|
|
131
132
|
const lines = [
|
|
132
|
-
`Flight recorder — ${records.length} record${records.length === 1 ? "" : "s"} in .vigiles/${
|
|
133
|
+
`Flight recorder — ${records.length} record${records.length === 1 ? "" : "s"} in .vigiles/${local_files_js_1.LEDGER_FILE}`,
|
|
133
134
|
];
|
|
134
135
|
const counts = new Map();
|
|
135
136
|
for (const r of records)
|
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",
|