vigiles 27.2.0 → 28.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter-conformance.js +2 -2
- package/dist/adapters/claude-code/adapter.js +3 -2
- package/dist/adapters/claude-code/run-scripts.js +47 -8
- package/dist/adapters/codex/adapter.js +1 -2
- package/dist/cli-main.js +124 -25
- package/dist/core/adapter.d.ts +22 -1
- package/dist/core/hook-program.d.ts +43 -0
- package/dist/core/hook-program.js +32 -0
- package/dist/core/linters.js +3 -3
- package/dist/core/test-utils.d.ts +1 -2
- package/dist/core/test-utils.js +7 -9
- package/dist/core/tmp-root.d.ts +12 -0
- package/dist/core/tmp-root.js +59 -0
- package/dist/core/validate.d.ts +15 -1
- package/dist/core/validate.js +16 -2
- package/dist/eval.js +6 -5
- package/dist/harness-test.js +5 -5
- package/dist/hook-install.d.ts +53 -0
- package/dist/hook-install.js +60 -0
- package/dist/hook-runtime.d.ts +2 -2
- package/dist/hook-runtime.js +82 -63
- package/dist/load-hook.d.ts +1 -1
- package/dist/load-hook.js +2 -2
- package/dist/run-hook.js +17 -1
- package/dist/run-script.js +3 -3
- package/dist/sandbox.js +2 -2
- package/dist/scan-behavioral.js +2 -2
- package/dist/test.d.ts +1 -0
- package/dist/test.js +17 -2
- package/package.json +1 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ClaudeSpec } from "./spec.js";
|
|
2
|
-
export
|
|
3
|
-
export declare function cleanupTmpDir(dir: string): void;
|
|
2
|
+
export { makeTmpDir, cleanupTmpDir } from "./tmp-root.js";
|
|
4
3
|
export declare function makeSpec(overrides?: Partial<ClaudeSpec>): ClaudeSpec;
|
|
5
4
|
declare function git(cwd: string, cmd: string): string;
|
|
6
5
|
export declare function initGitRepo(dir: string): void;
|
package/dist/core/test-utils.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.makeTmpDir =
|
|
4
|
-
exports.cleanupTmpDir = cleanupTmpDir;
|
|
3
|
+
exports.cleanupTmpDir = exports.makeTmpDir = void 0;
|
|
5
4
|
exports.makeSpec = makeSpec;
|
|
6
5
|
exports.initGitRepo = initGitRepo;
|
|
7
6
|
exports.git = git;
|
|
8
7
|
const node_fs_1 = require("node:fs");
|
|
9
8
|
const node_path_1 = require("node:path");
|
|
10
|
-
const node_os_1 = require("node:os");
|
|
11
9
|
const node_child_process_1 = require("node:child_process");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
10
|
+
// Re-exported, not redefined: the temp root lives in `tmp-root.ts` because the
|
|
11
|
+
// runtime modules that need one must not pull in `makeSpec`/`initGitRepo` and
|
|
12
|
+
// their dependencies. One definition, two doors.
|
|
13
|
+
var tmp_root_js_1 = require("./tmp-root.js");
|
|
14
|
+
Object.defineProperty(exports, "makeTmpDir", { enumerable: true, get: function () { return tmp_root_js_1.makeTmpDir; } });
|
|
15
|
+
Object.defineProperty(exports, "cleanupTmpDir", { enumerable: true, get: function () { return tmp_root_js_1.cleanupTmpDir; } });
|
|
18
16
|
function makeSpec(overrides) {
|
|
19
17
|
return {
|
|
20
18
|
_specType: "claude",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a temporary directory and return the path with every symlink resolved.
|
|
3
|
+
*
|
|
4
|
+
* The `realpathSync` wrapper is the whole point: it must stay OUTSIDE
|
|
5
|
+
* `mkdtempSync`, because the directory has to exist before it can be resolved.
|
|
6
|
+
*
|
|
7
|
+
* @param suffix distinguishes roots in a listing; the name is `vigiles-<suffix>-*`
|
|
8
|
+
*/
|
|
9
|
+
export declare function makeTmpDir(suffix?: string): string;
|
|
10
|
+
/** Remove a root made by {@link makeTmpDir}. Safe on a path that is already gone. */
|
|
11
|
+
export declare function cleanupTmpDir(dir: string): void;
|
|
12
|
+
//# sourceMappingURL=tmp-root.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeTmpDir = makeTmpDir;
|
|
4
|
+
exports.cleanupTmpDir = cleanupTmpDir;
|
|
5
|
+
/**
|
|
6
|
+
* The ONE way to make a temporary fixture root, with its symlinks resolved.
|
|
7
|
+
*
|
|
8
|
+
* ── THE TRAP THIS EXISTS TO ABOLISH (issue #241, measured) ──────────────────────
|
|
9
|
+
* On macOS `os.tmpdir()` returns a path under `/var/folders/…`, and `/var` is
|
|
10
|
+
* itself a symlink to `/private/var`. Node resolves a module's own URL to the
|
|
11
|
+
* REALPATH but leaves `process.argv[1]`, and any path a test composed itself,
|
|
12
|
+
* exactly as typed. A fixture built under `tmpdir()` therefore carries two
|
|
13
|
+
* spellings of one directory, and anything comparing them is red on macOS and
|
|
14
|
+
* green on Linux:
|
|
15
|
+
*
|
|
16
|
+
* const d = mkdtempSync(join(tmpdir(), "probe-"));
|
|
17
|
+
* // import.meta.url → "file:///private/var/folders/…/probe.mjs"
|
|
18
|
+
* // process.argv[1] → "/var/folders/…/probe.mjs"
|
|
19
|
+
*
|
|
20
|
+
* A consumer hit that three times in one suite: a resolver's return value against
|
|
21
|
+
* a composed expectation, an `isMain` control case, and a git fixture whose
|
|
22
|
+
* repository root git reported realpath'd while the relative path was computed
|
|
23
|
+
* against the other spelling (`zernie/research-paper-pipeline#9`).
|
|
24
|
+
*
|
|
25
|
+
* 🔴 WHY A SHIPPED HELPER AND NOT THREE FIXED CALL SITES. Those call sites were
|
|
26
|
+
* hand-rolled because the product shipped nothing to roll: `mkdtempSync(join(
|
|
27
|
+
* tmpdir(), …))` is the shape a harness author reaches for, and it is the shape
|
|
28
|
+
* that carries the trap. Fixing our own sites leaves every future author to
|
|
29
|
+
* rediscover it. So this is exported from the harness surface (`vigiles`), where
|
|
30
|
+
* `recordCheck` and `skip` already live.
|
|
31
|
+
*
|
|
32
|
+
* 🔴 WHY ITS OWN MODULE. `core/test-utils.ts` also carries `makeSpec` and
|
|
33
|
+
* `initGitRepo`, which pull in spec types and `execSync`; the runtime modules
|
|
34
|
+
* that need a temp root must not drag those in. `test-utils` re-exports these two
|
|
35
|
+
* so existing imports keep working, and there is still exactly one definition.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ On Linux `realpathSync` is the identity here, so no behavioural test on this
|
|
38
|
+
* platform can hold the fix in place. What holds it is the regression test beside
|
|
39
|
+
* this file, which builds its own symlink rather than relying on the platform's.
|
|
40
|
+
*/
|
|
41
|
+
const node_fs_1 = require("node:fs");
|
|
42
|
+
const node_path_1 = require("node:path");
|
|
43
|
+
const node_os_1 = require("node:os");
|
|
44
|
+
/**
|
|
45
|
+
* Create a temporary directory and return the path with every symlink resolved.
|
|
46
|
+
*
|
|
47
|
+
* The `realpathSync` wrapper is the whole point: it must stay OUTSIDE
|
|
48
|
+
* `mkdtempSync`, because the directory has to exist before it can be resolved.
|
|
49
|
+
*
|
|
50
|
+
* @param suffix distinguishes roots in a listing; the name is `vigiles-<suffix>-*`
|
|
51
|
+
*/
|
|
52
|
+
function makeTmpDir(suffix = "test") {
|
|
53
|
+
return (0, node_fs_1.realpathSync)((0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), `vigiles-${suffix}-`)));
|
|
54
|
+
}
|
|
55
|
+
/** Remove a root made by {@link makeTmpDir}. Safe on a path that is already gone. */
|
|
56
|
+
function cleanupTmpDir(dir) {
|
|
57
|
+
(0, node_fs_1.rmSync)(dir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=tmp-root.js.map
|
package/dist/core/validate.d.ts
CHANGED
|
@@ -19,7 +19,21 @@ export declare function normalizeSeverity(v: unknown): unknown;
|
|
|
19
19
|
* A non-string/array value falls back with a warning (the `ruleMarkers` pattern).
|
|
20
20
|
*/
|
|
21
21
|
export declare function asStringArray(v: unknown, fallback: readonly string[], key: string): readonly string[];
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Read `.vigilesrc.json`.
|
|
24
|
+
*
|
|
25
|
+
* 🔴 `searchFrom` IS NOT A CONVENIENCE. cosmiconfig defaults to the process's
|
|
26
|
+
* working directory and walks up — right for a CLI verb, where the user is
|
|
27
|
+
* standing in the project they mean, and wrong for a hook, whose process has no
|
|
28
|
+
* stable cwd. A hook rail that omits it reads a DIFFERENT project's config, or
|
|
29
|
+
* none, and the failure runs the wrong way: a missing file means defaults, so a
|
|
30
|
+
* rule the author switched OFF comes back on, silently, because the file saying
|
|
31
|
+
* "off" was never found. Nothing in the output distinguishes that from a project
|
|
32
|
+
* that never configured the rule.
|
|
33
|
+
*
|
|
34
|
+
* Every CLI verb still calls this with no argument and is unaffected.
|
|
35
|
+
*/
|
|
36
|
+
export declare function loadConfig(searchFrom?: string): VigilesConfig;
|
|
23
37
|
export declare function parseRules(content: string, { ruleMarkers }?: ParseOptions): ParsedRule[];
|
|
24
38
|
export declare function validate(content: string, { ruleMarkers, rules: rulesConfig, filePath, dialect }?: ValidateOptions): ValidationResult;
|
|
25
39
|
export declare function readInstructionFile(filePath: string, options?: ReadOptions): ReadResult;
|
package/dist/core/validate.js
CHANGED
|
@@ -172,13 +172,27 @@ function asStringArray(v, fallback, key) {
|
|
|
172
172
|
console.warn(`Invalid ${key} in config: expected a string or string[], got ${JSON.stringify(v)}. Ignoring.`);
|
|
173
173
|
return fallback;
|
|
174
174
|
}
|
|
175
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Read `.vigilesrc.json`.
|
|
177
|
+
*
|
|
178
|
+
* 🔴 `searchFrom` IS NOT A CONVENIENCE. cosmiconfig defaults to the process's
|
|
179
|
+
* working directory and walks up — right for a CLI verb, where the user is
|
|
180
|
+
* standing in the project they mean, and wrong for a hook, whose process has no
|
|
181
|
+
* stable cwd. A hook rail that omits it reads a DIFFERENT project's config, or
|
|
182
|
+
* none, and the failure runs the wrong way: a missing file means defaults, so a
|
|
183
|
+
* rule the author switched OFF comes back on, silently, because the file saying
|
|
184
|
+
* "off" was never found. Nothing in the output distinguishes that from a project
|
|
185
|
+
* that never configured the rule.
|
|
186
|
+
*
|
|
187
|
+
* Every CLI verb still calls this with no argument and is unaffected.
|
|
188
|
+
*/
|
|
189
|
+
function loadConfig(searchFrom) {
|
|
176
190
|
try {
|
|
177
191
|
const explorer = (0, cosmiconfig_1.cosmiconfigSync)("vigiles", {
|
|
178
192
|
searchPlaces: [".vigilesrc.json"],
|
|
179
193
|
mergeSearchPlaces: false,
|
|
180
194
|
});
|
|
181
|
-
const result = explorer.search();
|
|
195
|
+
const result = explorer.search(searchFrom);
|
|
182
196
|
if (!result?.config)
|
|
183
197
|
return { ...DEFAULT_CONFIG };
|
|
184
198
|
const userConfig = result.config;
|
package/dist/eval.js
CHANGED
|
@@ -84,6 +84,7 @@ const check_count_js_1 = require("./check-count.js");
|
|
|
84
84
|
const coverage_probe_js_1 = require("./coverage-probe.js");
|
|
85
85
|
const tool_intercept_js_1 = require("./tool-intercept.js");
|
|
86
86
|
const tool_stub_js_1 = require("./tool-stub.js");
|
|
87
|
+
const tmp_root_js_1 = require("./core/tmp-root.js");
|
|
87
88
|
function writeFiles(cwd, files) {
|
|
88
89
|
for (const [p, content] of Object.entries(files)) {
|
|
89
90
|
const full = (0, node_path_1.resolve)(cwd, p);
|
|
@@ -650,7 +651,7 @@ function whichSkillsFired(trace) {
|
|
|
650
651
|
* falls out of one pass over the prompts (N× cheaper than re-running per pair).
|
|
651
652
|
*/
|
|
652
653
|
async function runSkillSelectionTrial(args) {
|
|
653
|
-
const cwd = (0,
|
|
654
|
+
const cwd = (0, tmp_root_js_1.makeTmpDir)("selection");
|
|
654
655
|
try {
|
|
655
656
|
if (args.fixture)
|
|
656
657
|
writeFiles(cwd, args.fixture);
|
|
@@ -1008,7 +1009,7 @@ function seedEphemeralHome(throwawayHome, realHome, keep = exports.EPHEMERAL_HOM
|
|
|
1008
1009
|
}
|
|
1009
1010
|
/** Execute one trial in a fresh sandbox; returns its metric row + usage. */
|
|
1010
1011
|
async function executeTrial(spec, arm, trialIndex, runner, cfg) {
|
|
1011
|
-
const cwd = (0,
|
|
1012
|
+
const cwd = (0, tmp_root_js_1.makeTmpDir)("eval");
|
|
1012
1013
|
try {
|
|
1013
1014
|
const resolved = (0, plugin_loader_js_1.resolveHarness)({
|
|
1014
1015
|
plugin: arm.plugin,
|
|
@@ -1456,7 +1457,7 @@ function packageSkillsDir(skillsDir, opts = {}) {
|
|
|
1456
1457
|
const abs = (0, node_path_1.resolve)(skillsDir);
|
|
1457
1458
|
if (!(0, node_fs_1.existsSync)(abs))
|
|
1458
1459
|
throw new Error(`skillsDir not found: ${skillsDir} (resolved ${abs})`);
|
|
1459
|
-
const root = (0,
|
|
1460
|
+
const root = (0, tmp_root_js_1.makeTmpDir)("skills");
|
|
1460
1461
|
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(root, ".claude-plugin"), { recursive: true });
|
|
1461
1462
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, ".claude-plugin", "plugin.json"), JSON.stringify({ name: opts.name ?? "vigiles-loose-skills", version: "0.0.0" }, null, 2));
|
|
1462
1463
|
const skillsOut = (0, node_path_1.join)(root, "skills");
|
|
@@ -1649,7 +1650,7 @@ function copySkillsInto(src, skillsOut, stub, present) {
|
|
|
1649
1650
|
* dir. Pure (filesystem only).
|
|
1650
1651
|
*/
|
|
1651
1652
|
function packageInstallSet(opts) {
|
|
1652
|
-
const root = (0,
|
|
1653
|
+
const root = (0, tmp_root_js_1.makeTmpDir)("harness");
|
|
1653
1654
|
try {
|
|
1654
1655
|
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(root, ".claude-plugin"), { recursive: true });
|
|
1655
1656
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, ".claude-plugin", "plugin.json"), JSON.stringify({ name: opts.name, version: "0.0.0" }, null, 2));
|
|
@@ -1737,7 +1738,7 @@ function resolveTriggerPluginDir(spec) {
|
|
|
1737
1738
|
/** Run one prompt set × trials through `runner`, aggregating fired counts. */
|
|
1738
1739
|
/** Run one trigger trial in a throwaway cwd (fixture seeded) → fired 0/1. */
|
|
1739
1740
|
async function runTriggerTrial(prompt, cfg, runner) {
|
|
1740
|
-
const cwd = (0,
|
|
1741
|
+
const cwd = (0, tmp_root_js_1.makeTmpDir)("trigger");
|
|
1741
1742
|
try {
|
|
1742
1743
|
if (cfg.fixture)
|
|
1743
1744
|
writeFiles(cwd, cfg.fixture);
|
package/dist/harness-test.js
CHANGED
|
@@ -43,7 +43,6 @@ exports.runHarness = runHarness;
|
|
|
43
43
|
*/
|
|
44
44
|
const node_child_process_1 = require("node:child_process");
|
|
45
45
|
const node_fs_1 = require("node:fs");
|
|
46
|
-
const node_os_1 = require("node:os");
|
|
47
46
|
const node_path_1 = require("node:path");
|
|
48
47
|
const adapter_conformance_js_1 = require("./adapter-conformance.js");
|
|
49
48
|
const check_count_js_1 = require("./check-count.js");
|
|
@@ -61,6 +60,7 @@ var sandbox_js_2 = require("./sandbox.js");
|
|
|
61
60
|
Object.defineProperty(exports, "decideSandbox", { enumerable: true, get: function () { return sandbox_js_2.decideSandbox; } });
|
|
62
61
|
Object.defineProperty(exports, "specTrusted", { enumerable: true, get: function () { return sandbox_js_2.specTrusted; } });
|
|
63
62
|
Object.defineProperty(exports, "sandboxAvailable", { enumerable: true, get: function () { return sandbox_js_2.sandboxAvailable; } });
|
|
63
|
+
const tmp_root_js_1 = require("./core/tmp-root.js");
|
|
64
64
|
function contentText(content) {
|
|
65
65
|
if (typeof content === "string")
|
|
66
66
|
return content;
|
|
@@ -430,7 +430,7 @@ async function runHarnessTest(spec, opts = {}) {
|
|
|
430
430
|
// Default (no adapter): the unchanged Claude Code driver — keeps the
|
|
431
431
|
// sandbox/confined path and behaviour byte-for-byte identical.
|
|
432
432
|
const driver = adapter
|
|
433
|
-
? requireDriver(adapter)
|
|
433
|
+
? await requireDriver(adapter)
|
|
434
434
|
: exports.claudeCodeDriver;
|
|
435
435
|
const isClaudeCode = driver.runtime.name === runtime_js_1.claudeCodeRuntime.name;
|
|
436
436
|
const decision = (0, sandbox_js_1.decideSandbox)({
|
|
@@ -443,7 +443,7 @@ async function runHarnessTest(spec, opts = {}) {
|
|
|
443
443
|
if (decision.action === "sandbox" && !isClaudeCode) {
|
|
444
444
|
throw new Error(`sandbox not supported for ${driver.runtime.name}: confined execution is Claude Code only. Pass sandbox: false to run ${driver.runtime.name} unconfined (you audited the code, or trust the outer container).`);
|
|
445
445
|
}
|
|
446
|
-
const cwd = (0,
|
|
446
|
+
const cwd = (0, tmp_root_js_1.makeTmpDir)("harness");
|
|
447
447
|
const { files, settings } = (0, plugin_loader_js_1.resolveHarness)({
|
|
448
448
|
plugin: spec.plugin,
|
|
449
449
|
settings: spec.settings,
|
|
@@ -528,12 +528,12 @@ async function runHarness(spec, opts = {}) {
|
|
|
528
528
|
return runHarnessTest(spec, opts);
|
|
529
529
|
}
|
|
530
530
|
/** Pull the pillar-2 driver off an adapter, asserting it supports testing. */
|
|
531
|
-
function requireDriver(adapter) {
|
|
531
|
+
async function requireDriver(adapter) {
|
|
532
532
|
(0, adapter_conformance_js_1.assertHarnessTestable)(adapter);
|
|
533
533
|
if (!adapter.harnessTestDriver) {
|
|
534
534
|
throw new Error(`Adapter "${adapter.name}" declares harnessTesting but carries no harnessTestDriver — it cannot drive runHarnessTest.`);
|
|
535
535
|
}
|
|
536
|
-
return adapter.harnessTestDriver;
|
|
536
|
+
return await adapter.harnessTestDriver();
|
|
537
537
|
}
|
|
538
538
|
/* v8 ignore stop */
|
|
539
539
|
//# sourceMappingURL=harness-test.js.map
|
package/dist/hook-install.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DispatchKind } from "./core/hook-program.js";
|
|
1
2
|
/** The agnostic, committed home for hook SOURCE — one dir, cross-adapter. */
|
|
2
3
|
export declare const HOOKS_DIR = ".vigiles/hooks";
|
|
3
4
|
/** The committed home for registered context-provider SOURCE (v2). */
|
|
@@ -55,6 +56,58 @@ export declare function normalizeHookRef(hookPath: string, cwd?: string): string
|
|
|
55
56
|
* `bareToken(hookGateRef(ref, tokens)) === ref` is what keeps a recompile idempotent, and
|
|
56
57
|
* it is asserted directly rather than left to inspection.
|
|
57
58
|
*/
|
|
59
|
+
/**
|
|
60
|
+
* Where the hook runtime lives, spelled so the shell can find it WITHOUT `npx`.
|
|
61
|
+
*
|
|
62
|
+
* 🔴 MEASURED 2026-09-19, warm cache, five runs each:
|
|
63
|
+
*
|
|
64
|
+
* node <local>/dist/cli.js hook-runtime run-program … 193 ms
|
|
65
|
+
* npx vigiles hook-runtime run-program … 2545 ms
|
|
66
|
+
*
|
|
67
|
+
* Thirteen times, on every tool call, because `npx` re-resolves the package on
|
|
68
|
+
* each invocation — local, then global, then the registry. That search is the
|
|
69
|
+
* single largest cost in a hook's life; everything the runtime does inside adds
|
|
70
|
+
* up to less than a fifth of it.
|
|
71
|
+
*
|
|
72
|
+
* A harness with no project-root token gets the relative spelling, which is all
|
|
73
|
+
* it can be given — see {@link hookGateRef} for the same fallback.
|
|
74
|
+
*/
|
|
75
|
+
export declare function hookRuntimeRef(projectRootTokens: readonly string[] | undefined): string;
|
|
76
|
+
/**
|
|
77
|
+
* What the shell must do when the runtime above CANNOT START — a missing
|
|
78
|
+
* `node_modules/vigiles`, an unreadable file, an interpreter that dies before a
|
|
79
|
+
* single line of ours runs. No code of ours executes in that case, so the policy
|
|
80
|
+
* has to be expressed in the emitted command or not at all.
|
|
81
|
+
*
|
|
82
|
+
* 🔴 THIS IS NOT A NEW POLICY. `runHookProgramCommand`'s load-failure branch has
|
|
83
|
+
* decided it since 2026-08: *"an inject's purpose is to ADD context, not to
|
|
84
|
+
* ENFORCE a decision … Gates (file, bash, prompt, stop) remain conservative and
|
|
85
|
+
* fail closed."* That branch only reaches failures that happen AFTER the runtime
|
|
86
|
+
* starts. This carries the same rule one layer out, to the failures that happen
|
|
87
|
+
* before it.
|
|
88
|
+
*
|
|
89
|
+
* WHY THE SPLIT, RATHER THAN ONE ANSWER FOR EVERYTHING — the two failures are
|
|
90
|
+
* not comparable:
|
|
91
|
+
*
|
|
92
|
+
* A GATE THAT SILENTLY PASSES IS WORSE THAN NO GATE. Its whole value is the
|
|
93
|
+
* refusal, and a harness that reports protection it is not providing is the
|
|
94
|
+
* one state worse than admitting it has none. So a gate whose runtime is
|
|
95
|
+
* missing exits 2: loud, blocking, and the cause is on stderr.
|
|
96
|
+
*
|
|
97
|
+
* A NUDGE THAT BLOCKS COSTS THE WHOLE REPOSITORY. Measured here 2026-08-10:
|
|
98
|
+
* merge-conflict markers in `package.json` stopped every hook loading, the
|
|
99
|
+
* Bash gate then refused `git merge --abort` — the one command that undoes the
|
|
100
|
+
* cause — and the session could not be repaired from inside. A reminder is
|
|
101
|
+
* never worth that, so a nudge exits 0 and says nothing it cannot say.
|
|
102
|
+
*
|
|
103
|
+
* The role is not a flag someone can flip: `Reaction` has no `deny` and an
|
|
104
|
+
* inject returns context, so "nudge" is a fact about the TYPE the author chose.
|
|
105
|
+
*
|
|
106
|
+
* (Industry does not agree on one answer either — husky and lefthook skip,
|
|
107
|
+
* pre-commit fails. Which is itself the argument for deciding by role instead
|
|
108
|
+
* of picking one and imposing it on both.)
|
|
109
|
+
*/
|
|
110
|
+
export declare function hookRuntimeMissingExit(kind: DispatchKind): 0 | 2;
|
|
58
111
|
export declare function hookGateRef(ref: string, projectRootTokens: readonly string[] | undefined): string;
|
|
59
112
|
/**
|
|
60
113
|
* Idempotently merge a compiled hook's block into an existing `settings.json`
|
package/dist/hook-install.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.PROVIDERS_DIR = exports.HOOKS_DIR = void 0;
|
|
|
4
4
|
exports.discoverHookFiles = discoverHookFiles;
|
|
5
5
|
exports.discoverProviderFiles = discoverProviderFiles;
|
|
6
6
|
exports.normalizeHookRef = normalizeHookRef;
|
|
7
|
+
exports.hookRuntimeRef = hookRuntimeRef;
|
|
8
|
+
exports.hookRuntimeMissingExit = hookRuntimeMissingExit;
|
|
7
9
|
exports.hookGateRef = hookGateRef;
|
|
8
10
|
exports.mergeHooksJson = mergeHooksJson;
|
|
9
11
|
exports.mergeHooksToml = mergeHooksToml;
|
|
@@ -99,6 +101,64 @@ function normalizeHookRef(hookPath, cwd = process.cwd()) {
|
|
|
99
101
|
* `bareToken(hookGateRef(ref, tokens)) === ref` is what keeps a recompile idempotent, and
|
|
100
102
|
* it is asserted directly rather than left to inspection.
|
|
101
103
|
*/
|
|
104
|
+
/**
|
|
105
|
+
* Where the hook runtime lives, spelled so the shell can find it WITHOUT `npx`.
|
|
106
|
+
*
|
|
107
|
+
* 🔴 MEASURED 2026-09-19, warm cache, five runs each:
|
|
108
|
+
*
|
|
109
|
+
* node <local>/dist/cli.js hook-runtime run-program … 193 ms
|
|
110
|
+
* npx vigiles hook-runtime run-program … 2545 ms
|
|
111
|
+
*
|
|
112
|
+
* Thirteen times, on every tool call, because `npx` re-resolves the package on
|
|
113
|
+
* each invocation — local, then global, then the registry. That search is the
|
|
114
|
+
* single largest cost in a hook's life; everything the runtime does inside adds
|
|
115
|
+
* up to less than a fifth of it.
|
|
116
|
+
*
|
|
117
|
+
* A harness with no project-root token gets the relative spelling, which is all
|
|
118
|
+
* it can be given — see {@link hookGateRef} for the same fallback.
|
|
119
|
+
*/
|
|
120
|
+
function hookRuntimeRef(projectRootTokens) {
|
|
121
|
+
const rel = "node_modules/vigiles/dist/cli.js";
|
|
122
|
+
const token = projectRootTokens?.[0];
|
|
123
|
+
return token === undefined ? `node ${rel}` : `node "${token}/${rel}"`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What the shell must do when the runtime above CANNOT START — a missing
|
|
127
|
+
* `node_modules/vigiles`, an unreadable file, an interpreter that dies before a
|
|
128
|
+
* single line of ours runs. No code of ours executes in that case, so the policy
|
|
129
|
+
* has to be expressed in the emitted command or not at all.
|
|
130
|
+
*
|
|
131
|
+
* 🔴 THIS IS NOT A NEW POLICY. `runHookProgramCommand`'s load-failure branch has
|
|
132
|
+
* decided it since 2026-08: *"an inject's purpose is to ADD context, not to
|
|
133
|
+
* ENFORCE a decision … Gates (file, bash, prompt, stop) remain conservative and
|
|
134
|
+
* fail closed."* That branch only reaches failures that happen AFTER the runtime
|
|
135
|
+
* starts. This carries the same rule one layer out, to the failures that happen
|
|
136
|
+
* before it.
|
|
137
|
+
*
|
|
138
|
+
* WHY THE SPLIT, RATHER THAN ONE ANSWER FOR EVERYTHING — the two failures are
|
|
139
|
+
* not comparable:
|
|
140
|
+
*
|
|
141
|
+
* A GATE THAT SILENTLY PASSES IS WORSE THAN NO GATE. Its whole value is the
|
|
142
|
+
* refusal, and a harness that reports protection it is not providing is the
|
|
143
|
+
* one state worse than admitting it has none. So a gate whose runtime is
|
|
144
|
+
* missing exits 2: loud, blocking, and the cause is on stderr.
|
|
145
|
+
*
|
|
146
|
+
* A NUDGE THAT BLOCKS COSTS THE WHOLE REPOSITORY. Measured here 2026-08-10:
|
|
147
|
+
* merge-conflict markers in `package.json` stopped every hook loading, the
|
|
148
|
+
* Bash gate then refused `git merge --abort` — the one command that undoes the
|
|
149
|
+
* cause — and the session could not be repaired from inside. A reminder is
|
|
150
|
+
* never worth that, so a nudge exits 0 and says nothing it cannot say.
|
|
151
|
+
*
|
|
152
|
+
* The role is not a flag someone can flip: `Reaction` has no `deny` and an
|
|
153
|
+
* inject returns context, so "nudge" is a fact about the TYPE the author chose.
|
|
154
|
+
*
|
|
155
|
+
* (Industry does not agree on one answer either — husky and lefthook skip,
|
|
156
|
+
* pre-commit fails. Which is itself the argument for deciding by role instead
|
|
157
|
+
* of picking one and imposing it on both.)
|
|
158
|
+
*/
|
|
159
|
+
function hookRuntimeMissingExit(kind) {
|
|
160
|
+
return kind === "inject" || kind === "react" ? 0 : 2;
|
|
161
|
+
}
|
|
102
162
|
function hookGateRef(ref, projectRootTokens) {
|
|
103
163
|
const token = projectRootTokens?.[0];
|
|
104
164
|
return token === undefined ? ref : `"${token}/${ref}"`;
|
package/dist/hook-runtime.d.ts
CHANGED
|
@@ -45,9 +45,9 @@ import { loadHook } from "./load-hook.js";
|
|
|
45
45
|
*/
|
|
46
46
|
export declare const loadHookProgram: typeof loadHook;
|
|
47
47
|
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
48
|
-
export declare function loadProvider(file: string): Promise<RegisteredProvider>;
|
|
48
|
+
export declare function loadProvider(file: string, root?: string): Promise<RegisteredProvider>;
|
|
49
49
|
/** Path of the tamper-evident stamp sidecar for a hook file. */
|
|
50
|
-
export declare function hookStampPath(file: string): string;
|
|
50
|
+
export declare function hookStampPath(file: string, root?: string): string;
|
|
51
51
|
/**
|
|
52
52
|
* `vigiles hook-runtime run-program <file>` — the runtime the compiled hooks block
|
|
53
53
|
* points at. Reads the live event on stdin, loads the typed program, verifies
|