vigiles 27.3.0 → 29.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/README.md +1 -1
- package/dist/adapter-registry.d.ts +39 -0
- package/dist/adapter-registry.js +45 -0
- package/dist/adapter.d.ts +8 -0
- package/dist/adapter.js +10 -1
- package/dist/adapters/claude-code/adapter.js +9 -2
- package/dist/adapters/claude-code/layout.d.ts +5 -0
- package/dist/adapters/claude-code/plugin-loader.d.ts +10 -1
- package/dist/adapters/claude-code/plugin-loader.js +10 -1
- package/dist/adapters/codex/adapter.js +7 -2
- package/dist/adapters/codex/layout.d.ts +51 -5
- package/dist/adapters/codex/layout.js +13 -4
- package/dist/adapters/opencode/adapter.js +6 -0
- package/dist/audit-report.template.html +1 -1
- package/dist/audit-score.d.ts +7 -0
- package/dist/audit-score.js +49 -2
- package/dist/cli-main.js +162 -39
- package/dist/core/adapter.d.ts +45 -1
- package/dist/core/compile.js +11 -1
- package/dist/core/config-schema.d.ts +244 -0
- package/dist/core/config-schema.js +452 -0
- package/dist/core/hook-program.d.ts +43 -0
- package/dist/core/hook-program.js +32 -0
- package/dist/core/refs.js +10 -1
- package/dist/core/surface-discovery.d.ts +270 -0
- package/dist/core/surface-discovery.js +425 -0
- package/dist/core/surface-scopes.d.ts +38 -1
- package/dist/core/surface-scopes.js +73 -1
- package/dist/core/symbols.d.ts +24 -2
- package/dist/core/symbols.js +66 -18
- package/dist/core/types.d.ts +36 -107
- package/dist/core/validate.d.ts +46 -18
- package/dist/core/validate.js +98 -172
- package/dist/exclude.d.ts +20 -0
- package/dist/exclude.js +11 -1
- package/dist/harness-test.js +3 -3
- 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/layout-registry.d.ts +14 -0
- package/dist/layout-registry.js +40 -0
- package/dist/load-hook.d.ts +1 -1
- package/dist/load-hook.js +2 -2
- package/dist/plugin-loader.d.ts +49 -1
- package/dist/plugin-loader.js +120 -14
- package/dist/run-hook.js +17 -1
- package/dist/scan-core.d.ts +19 -0
- package/dist/scan-core.js +30 -0
- package/dist/scan-files.js +15 -5
- package/dist/scan.d.ts +63 -0
- package/dist/scan.js +68 -12
- package/dist/score-core.js +8 -0
- package/dist/setup-plan.d.ts +2 -1
- package/dist/setup-plan.js +7 -2
- package/dist/surface-discovery-fs.d.ts +12 -0
- package/dist/surface-discovery-fs.js +108 -0
- package/dist/vigilesrc.schema.json +1689 -0
- package/package.json +10 -6
package/dist/hook-runtime.js
CHANGED
|
@@ -75,8 +75,8 @@ function injectableEventsFor(root) {
|
|
|
75
75
|
*/
|
|
76
76
|
exports.loadHookProgram = load_hook_js_1.loadHook;
|
|
77
77
|
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
78
|
-
async function loadProvider(file) {
|
|
79
|
-
const abs = (0, node_path_1.resolve)(
|
|
78
|
+
async function loadProvider(file, root = process.cwd()) {
|
|
79
|
+
const abs = (0, node_path_1.resolve)(root, file);
|
|
80
80
|
const { pathToFileURL } = require("node:url");
|
|
81
81
|
let mod;
|
|
82
82
|
try {
|
|
@@ -95,8 +95,8 @@ async function loadProvider(file) {
|
|
|
95
95
|
return def;
|
|
96
96
|
}
|
|
97
97
|
/** Path of the tamper-evident stamp sidecar for a hook file. */
|
|
98
|
-
function hookStampPath(file) {
|
|
99
|
-
return (0, node_path_1.resolve)(
|
|
98
|
+
function hookStampPath(file, root = process.cwd()) {
|
|
99
|
+
return (0, node_path_1.resolve)(root, ".vigiles/hooks", (0, node_path_1.basename)(file) + ".json");
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Perform the state writes a hook declared, after its output has been emitted.
|
|
@@ -104,14 +104,14 @@ function hookStampPath(file) {
|
|
|
104
104
|
* thrown on) is announced — silence here would be a hook that believes it
|
|
105
105
|
* remembered something.
|
|
106
106
|
*/
|
|
107
|
-
function applyHookWrites(file, outcome) {
|
|
107
|
+
function applyHookWrites(file, outcome, root) {
|
|
108
108
|
const { ok, refused } = (0, hook_program_js_1.outcomeWrites)(outcome);
|
|
109
109
|
for (const name of refused) {
|
|
110
110
|
console.error(`vigiles: refused to record ${name} from ${file} — not a valid state key.`);
|
|
111
111
|
}
|
|
112
112
|
for (const w of ok) {
|
|
113
113
|
try {
|
|
114
|
-
(0, hook_state_store_js_1.writeHookState)(file, w);
|
|
114
|
+
(0, hook_state_store_js_1.writeHookState)(file, w, { cwd: root });
|
|
115
115
|
}
|
|
116
116
|
catch (e) {
|
|
117
117
|
console.error(`vigiles: could not record ${w.name} from ${file}: ${String(e)}`);
|
|
@@ -124,13 +124,13 @@ function applyHookWrites(file, outcome) {
|
|
|
124
124
|
* that can't resolve yields its default (never throws). The pure registry +
|
|
125
125
|
* decision logic live in core/hook-providers.ts — this only injects the real IO.
|
|
126
126
|
*/
|
|
127
|
-
async function gatherHookContext(program, file) {
|
|
127
|
+
async function gatherHookContext(program, file, root) {
|
|
128
128
|
const needs = (0, hook_program_js_1.hookNeeds)(program);
|
|
129
129
|
if (needs.length === 0)
|
|
130
130
|
return {};
|
|
131
131
|
// Only load the registered-provider registry if a provider() ref is declared.
|
|
132
132
|
const hasRef = needs.some((n) => typeof n !== "string" && n.kind === "provider-ref");
|
|
133
|
-
const registry = hasRef ? await loadProviderRegistry() : {};
|
|
133
|
+
const registry = hasRef ? await loadProviderRegistry(root) : {};
|
|
134
134
|
const { execSync } = require("node:child_process");
|
|
135
135
|
const { isCI } = require("ci-info");
|
|
136
136
|
return (0, hook_providers_js_1.gatherContext)(needs, {
|
|
@@ -138,12 +138,12 @@ async function gatherHookContext(program, file) {
|
|
|
138
138
|
encoding: "utf-8",
|
|
139
139
|
stdio: ["ignore", "pipe", "ignore"],
|
|
140
140
|
}),
|
|
141
|
-
cwd:
|
|
141
|
+
cwd: root,
|
|
142
142
|
platform: process.platform,
|
|
143
143
|
isCI,
|
|
144
144
|
// The namespace is bound HERE, from the hook's own path — core never sees
|
|
145
145
|
// it, so no key a hook can spell reaches another owner's store.
|
|
146
|
-
readState: (key) => (0, hook_state_store_js_1.readHookState)(file, key),
|
|
146
|
+
readState: (key) => (0, hook_state_store_js_1.readHookState)(file, key, root),
|
|
147
147
|
now: Date.now(),
|
|
148
148
|
}, registry);
|
|
149
149
|
}
|
|
@@ -152,11 +152,11 @@ async function gatherHookContext(program, file) {
|
|
|
152
152
|
* for `provider()` ref resolution. A bad/unloadable provider file is skipped (the
|
|
153
153
|
* ref then yields its default ""), never crashes a live session.
|
|
154
154
|
*/
|
|
155
|
-
async function loadProviderRegistry() {
|
|
155
|
+
async function loadProviderRegistry(root) {
|
|
156
156
|
const registry = {};
|
|
157
|
-
for (const file of (0, hook_install_js_1.discoverProviderFiles)(
|
|
157
|
+
for (const file of (0, hook_install_js_1.discoverProviderFiles)(root)) {
|
|
158
158
|
try {
|
|
159
|
-
const def = await loadProvider(file);
|
|
159
|
+
const def = await loadProvider(file, root);
|
|
160
160
|
registry[def.name] = def;
|
|
161
161
|
}
|
|
162
162
|
catch {
|
|
@@ -166,9 +166,9 @@ async function loadProviderRegistry() {
|
|
|
166
166
|
return registry;
|
|
167
167
|
}
|
|
168
168
|
/** Append an observe-mode record to `.vigiles/hook-observations.jsonl` (best-effort). */
|
|
169
|
-
function recordObservation(file, on, would, reason) {
|
|
169
|
+
function recordObservation(file, on, would, reason, root) {
|
|
170
170
|
try {
|
|
171
|
-
const dir = (0, node_path_1.resolve)(
|
|
171
|
+
const dir = (0, node_path_1.resolve)(root, ".vigiles");
|
|
172
172
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
173
173
|
const line = JSON.stringify({
|
|
174
174
|
ts: new Date().toISOString(),
|
|
@@ -189,7 +189,7 @@ function recordObservation(file, on, would, reason) {
|
|
|
189
189
|
* shadow/rollout path. Harness-neutral — exit 2 / exit 0 are identical on Claude
|
|
190
190
|
* Code and Codex; the record is vigiles-local.
|
|
191
191
|
*/
|
|
192
|
-
function emitGate(decision, on, mode, file) {
|
|
192
|
+
function emitGate(decision, on, mode, file, root) {
|
|
193
193
|
const action = (0, hook_program_js_1.gateAction)(decision, mode);
|
|
194
194
|
switch (action.kind) {
|
|
195
195
|
case "block":
|
|
@@ -200,7 +200,7 @@ function emitGate(decision, on, mode, file) {
|
|
|
200
200
|
mode: "enforce",
|
|
201
201
|
rule: file,
|
|
202
202
|
reason: action.reason,
|
|
203
|
-
});
|
|
203
|
+
}, root);
|
|
204
204
|
console.error(action.reason);
|
|
205
205
|
process.exit(2);
|
|
206
206
|
return;
|
|
@@ -212,7 +212,7 @@ function emitGate(decision, on, mode, file) {
|
|
|
212
212
|
mode: "enforce",
|
|
213
213
|
rule: file,
|
|
214
214
|
reason: action.reason,
|
|
215
|
-
});
|
|
215
|
+
}, root);
|
|
216
216
|
process.stdout.write(JSON.stringify({
|
|
217
217
|
hookSpecificOutput: {
|
|
218
218
|
hookEventName: on,
|
|
@@ -229,8 +229,8 @@ function emitGate(decision, on, mode, file) {
|
|
|
229
229
|
mode: "observe",
|
|
230
230
|
rule: file,
|
|
231
231
|
reason: action.reason,
|
|
232
|
-
});
|
|
233
|
-
recordObservation(file, on, action.would, action.reason);
|
|
232
|
+
}, root);
|
|
233
|
+
recordObservation(file, on, action.would, action.reason, root);
|
|
234
234
|
console.error(`⚠ [vigiles observe] ${on}: would ${action.would} — ${action.reason}`);
|
|
235
235
|
return; // exit 0 — observe never blocks
|
|
236
236
|
case "allow":
|
|
@@ -247,9 +247,9 @@ function emitGate(decision, on, mode, file) {
|
|
|
247
247
|
* observed wedge came from a `package.json` the author was not thinking about at
|
|
248
248
|
* the time — it had merge-conflict markers in it, nothing to do with hooks.
|
|
249
249
|
*/
|
|
250
|
-
function hookLoadPathFiles(hookFile) {
|
|
250
|
+
function hookLoadPathFiles(hookFile, root) {
|
|
251
251
|
const files = [];
|
|
252
|
-
let dir = (0, node_path_1.dirname)((0, node_path_1.resolve)(
|
|
252
|
+
let dir = (0, node_path_1.dirname)((0, node_path_1.resolve)(root, hookFile));
|
|
253
253
|
for (;;) {
|
|
254
254
|
const pkg = (0, node_path_1.resolve)(dir, "package.json");
|
|
255
255
|
files.push(pkg);
|
|
@@ -275,15 +275,15 @@ function hookLoadPathFiles(hookFile) {
|
|
|
275
275
|
break;
|
|
276
276
|
dir = up;
|
|
277
277
|
}
|
|
278
|
-
files.push((0, node_path_1.resolve)(
|
|
278
|
+
files.push((0, node_path_1.resolve)(root, ".vigilesrc.json"));
|
|
279
279
|
return files;
|
|
280
280
|
}
|
|
281
281
|
/**
|
|
282
282
|
* The conflicted files on this hook's load path, if any — the difference between
|
|
283
283
|
* "your hook is broken" and "your repo is mid-merge and the hook is collateral".
|
|
284
284
|
*/
|
|
285
|
-
function conflictedLoadPathFiles(hookFile) {
|
|
286
|
-
return hookLoadPathFiles(hookFile)
|
|
285
|
+
function conflictedLoadPathFiles(hookFile, root) {
|
|
286
|
+
return hookLoadPathFiles(hookFile, root)
|
|
287
287
|
.filter((p) => {
|
|
288
288
|
try {
|
|
289
289
|
return ((0, node_fs_1.existsSync)(p) && (0, merge_conflict_js_1.hasMergeConflictMarkers)((0, node_fs_1.readFileSync)(p, "utf-8")));
|
|
@@ -292,7 +292,7 @@ function conflictedLoadPathFiles(hookFile) {
|
|
|
292
292
|
return false; // unreadable is a different problem; don't guess about it
|
|
293
293
|
}
|
|
294
294
|
})
|
|
295
|
-
.map((p) => (0, node_path_1.relative)(
|
|
295
|
+
.map((p) => (0, node_path_1.relative)(root, p) || p);
|
|
296
296
|
}
|
|
297
297
|
/**
|
|
298
298
|
* Print the loud stderr banner that accompanies a REPAIR-only pass-through, and
|
|
@@ -321,14 +321,15 @@ function announceRepairEscape(file, why) {
|
|
|
321
321
|
* `.claude/settings.json` to unwire the gate. Observed 2026-08-03.
|
|
322
322
|
*/
|
|
323
323
|
function verifyStampOrRefuse(file, event) {
|
|
324
|
-
const
|
|
324
|
+
const { root } = event;
|
|
325
|
+
const stampPath = hookStampPath(file, root);
|
|
325
326
|
if (!(0, node_fs_1.existsSync)(stampPath))
|
|
326
327
|
return;
|
|
327
328
|
try {
|
|
328
329
|
const { stamp } = JSON.parse((0, node_fs_1.readFileSync)(stampPath, "utf-8"));
|
|
329
|
-
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(
|
|
330
|
+
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(root, file), "utf-8");
|
|
330
331
|
if (stamp && !(0, hook_program_js_1.verifyHookStamp)(source, stamp)) {
|
|
331
|
-
if ((0, hook_program_js_1.isStampRepairEvent)(event, file,
|
|
332
|
+
if ((0, hook_program_js_1.isStampRepairEvent)(event, file, root)) {
|
|
332
333
|
announceRepairEscape(file, "does not match its compiled stamp");
|
|
333
334
|
return;
|
|
334
335
|
}
|
|
@@ -388,21 +389,39 @@ async function runHookProgramCommand(file) {
|
|
|
388
389
|
catch {
|
|
389
390
|
/* no stdin */
|
|
390
391
|
}
|
|
391
|
-
let
|
|
392
|
+
let payload = {};
|
|
392
393
|
try {
|
|
393
|
-
|
|
394
|
+
payload = JSON.parse(raw);
|
|
394
395
|
}
|
|
395
396
|
catch {
|
|
396
397
|
/* malformed → empty event */
|
|
397
398
|
}
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
399
|
+
// 🔴 THE ROOT IS RESOLVED ONCE, HERE, AND RIDES ON THE EVENT. Everything below
|
|
400
|
+
// reads `event.root`; nothing recomputes it and nothing is handed a root
|
|
401
|
+
// beside an event it might disagree with. That disagreement is the defect
|
|
402
|
+
// this shape exists to prevent — the stamp sidecar, the hook's own source,
|
|
403
|
+
// the state store, the ledger and the provider registry each used to resolve
|
|
404
|
+
// against `process.cwd()` while the decision layer resolved against the
|
|
405
|
+
// payload, so under a worktree the tamper check did not misfire, it did not
|
|
406
|
+
// run at all.
|
|
407
|
+
//
|
|
408
|
+
// This is the only `process.cwd()` on the runtime's own execution path, and
|
|
409
|
+
// it is the documented last resort for a payload that declares no root. The
|
|
410
|
+
// two others in this file are back-compat defaults on exported helpers
|
|
411
|
+
// (`loadProvider`, `hookStampPath`) for callers outside the runtime; the
|
|
412
|
+
// runtime itself always passes a root and never takes them.
|
|
413
|
+
const event = (0, hook_program_js_1.resolveHookEvent)(payload, process.env, process.cwd());
|
|
414
|
+
const root = event.root;
|
|
415
|
+
// ⚠️ THE DECISION LAYER MUST NOT SEE THE FALLBACK, and this is not a detail.
|
|
416
|
+
// `pathView` treats an undefined root as "I cannot place this path" and errs
|
|
417
|
+
// toward SILENCE. Handing it `process.cwd()` instead would turn that silence
|
|
418
|
+
// into confident decisions measured against a directory nobody declared —
|
|
419
|
+
// quietly widening what gates fire on. IO paths need a usable root; verdicts
|
|
420
|
+
// need an honest one, and they are not the same question.
|
|
421
|
+
const declaredRoot = event.rootDeclared ? event.root : undefined;
|
|
403
422
|
let program;
|
|
404
423
|
try {
|
|
405
|
-
program = await (0, exports.loadHookProgram)(file);
|
|
424
|
+
program = await (0, exports.loadHookProgram)(file, root);
|
|
406
425
|
}
|
|
407
426
|
catch (err) {
|
|
408
427
|
// A LOAD failure is a fact about the harness, not a verdict about the
|
|
@@ -430,7 +449,7 @@ async function runHookProgramCommand(file) {
|
|
|
430
449
|
// not go through PreToolUse(Bash)).
|
|
431
450
|
// Everything else stays BLOCKED, and the escapes are whitelists of commands
|
|
432
451
|
// that are WRITES — see `isLoadPathRepairEvent` for why no command is one.
|
|
433
|
-
const conflicted = conflictedLoadPathFiles(file);
|
|
452
|
+
const conflicted = conflictedLoadPathFiles(file, root);
|
|
434
453
|
// 🔴 THE THROWN MESSAGE IS THE ONLY THING THAT NAMES THE REAL CAUSE when the
|
|
435
454
|
// merge-conflict heuristic above does not fire. Without it this said just
|
|
436
455
|
// "cannot be loaded" — a diagnosis that sends the reader looking in the wrong
|
|
@@ -444,13 +463,13 @@ async function runHookProgramCommand(file) {
|
|
|
444
463
|
`may be fine)`
|
|
445
464
|
: `cannot be loaded — ${thrown}`;
|
|
446
465
|
if ((0, hook_program_js_1.isLoadPathRepairEvent)(event, file, {
|
|
447
|
-
// The root the REST of this runtime already uses
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
root
|
|
453
|
-
loadPathFiles: hookLoadPathFiles(file),
|
|
466
|
+
// The root the REST of this runtime already uses — now the PROJECT's,
|
|
467
|
+
// not the process's. A repair accepted against any other root would name
|
|
468
|
+
// a file the runtime never reads. The hook's own path cannot supply it (a
|
|
469
|
+
// hook sits at any depth, and a `.git` probe would be a disk read core
|
|
470
|
+
// does not do), so it is passed in.
|
|
471
|
+
root,
|
|
472
|
+
loadPathFiles: hookLoadPathFiles(file, root),
|
|
454
473
|
})) {
|
|
455
474
|
announceRepairEscape(file, cause);
|
|
456
475
|
return;
|
|
@@ -466,7 +485,7 @@ async function runHookProgramCommand(file) {
|
|
|
466
485
|
`${file}, ${merge_conflict_js_1.HARNESS_CONFIG_FILES.join(", ")} is broken — those writes are ` +
|
|
467
486
|
`allowed even while this refuses, and a Bash gate never gated file tools ` +
|
|
468
487
|
`at all. The hook then loads and the gate decides normally again.\n` +
|
|
469
|
-
`vigiles: those paths resolve under ${
|
|
488
|
+
`vigiles: those paths resolve under ${root} — plus any ancestor ` +
|
|
470
489
|
`\`package.json\` Node actually reads, so whatever is named above as the ` +
|
|
471
490
|
`cause is writable. A path in a DIFFERENT checkout is refused: it cannot ` +
|
|
472
491
|
`repair this failure.\n` +
|
|
@@ -494,7 +513,7 @@ async function runHookProgramCommand(file) {
|
|
|
494
513
|
verifyStampOrRefuse(file, event);
|
|
495
514
|
switch ((0, hook_program_js_1.dispatchKind)(program)) {
|
|
496
515
|
case "inject": {
|
|
497
|
-
const ctx = await gatherHookContext(program, file);
|
|
516
|
+
const ctx = await gatherHookContext(program, file, root);
|
|
498
517
|
const injection = (0, hook_program_js_1.injectionOf)(program, event, ctx);
|
|
499
518
|
process.stdout.write(JSON.stringify({
|
|
500
519
|
hookSpecificOutput: {
|
|
@@ -508,20 +527,20 @@ async function runHookProgramCommand(file) {
|
|
|
508
527
|
kind: "injection",
|
|
509
528
|
context: injection.context,
|
|
510
529
|
records: injection.records,
|
|
511
|
-
});
|
|
530
|
+
}, root);
|
|
512
531
|
return;
|
|
513
532
|
}
|
|
514
533
|
case "react": {
|
|
515
|
-
const ctx = await gatherHookContext(program, file);
|
|
516
|
-
warnIfPathUndecidable(event,
|
|
517
|
-
const reaction = (0, hook_program_js_1.runReact)(program, event, ctx,
|
|
534
|
+
const ctx = await gatherHookContext(program, file, root);
|
|
535
|
+
warnIfPathUndecidable(event, declaredRoot);
|
|
536
|
+
const reaction = (0, hook_program_js_1.runReact)(program, event, ctx, declaredRoot);
|
|
518
537
|
// A notice has to REACH someone. stderr at exit 0 goes to the debug log
|
|
519
538
|
// and nothing else (the host's docs are explicit: "Claude never sees it"),
|
|
520
539
|
// and a react always exits 0 because its type has no `deny` — so stderr
|
|
521
540
|
// alone delivered nowhere. Emit the same `additionalContext` shape the
|
|
522
541
|
// shipped refs/eval-lock nudges use, gated on the ACTIVE adapter's
|
|
523
542
|
// `injectableEvents` so this is per-harness fact, not a CC literal.
|
|
524
|
-
const injectable = injectableEventsFor(
|
|
543
|
+
const injectable = injectableEventsFor(root);
|
|
525
544
|
const delivery = (0, hook_program_js_1.noticeDelivery)(reaction, program.on, injectable);
|
|
526
545
|
if (delivery.kind === "inject") {
|
|
527
546
|
process.stdout.write(JSON.stringify({
|
|
@@ -537,7 +556,7 @@ async function runHookProgramCommand(file) {
|
|
|
537
556
|
// Removing it would break existing consumers to gain nothing.
|
|
538
557
|
if (reaction.kind === "notice")
|
|
539
558
|
console.error(reaction.message);
|
|
540
|
-
applyHookWrites(file, { kind: "reaction", reaction });
|
|
559
|
+
applyHookWrites(file, { kind: "reaction", reaction }, root);
|
|
541
560
|
if (reaction.kind === "run") {
|
|
542
561
|
const { spawnSync } = require("node:child_process");
|
|
543
562
|
const res = spawnSync(reaction.command, {
|
|
@@ -549,29 +568,29 @@ async function runHookProgramCommand(file) {
|
|
|
549
568
|
return;
|
|
550
569
|
}
|
|
551
570
|
case "file-gate": {
|
|
552
|
-
const ctx = await gatherHookContext(program, file);
|
|
553
|
-
warnIfPathUndecidable(event,
|
|
554
|
-
emitGate((0, hook_program_js_1.decideFileGate)(program, event, ctx,
|
|
571
|
+
const ctx = await gatherHookContext(program, file, root);
|
|
572
|
+
warnIfPathUndecidable(event, declaredRoot);
|
|
573
|
+
emitGate((0, hook_program_js_1.decideFileGate)(program, event, ctx, declaredRoot), program.on, (0, hook_program_js_1.hookMode)(program), file, root);
|
|
555
574
|
return;
|
|
556
575
|
}
|
|
557
576
|
case "bash-gate": {
|
|
558
|
-
const ctx = await gatherHookContext(program, file);
|
|
559
|
-
// The same `
|
|
577
|
+
const ctx = await gatherHookContext(program, file, root);
|
|
578
|
+
// The same `declaredRoot` the file gates get: without it every
|
|
560
579
|
// repo-relative prefix in a DENYLIST matcher (`touches`/`writesTo`) is
|
|
561
580
|
// matched by over-blocking alone, and with it an absolute token is placed
|
|
562
581
|
// exactly. Measured bypass this closes: `sed -i s/a/b/ <abs>/paper.tex`
|
|
563
582
|
// exited 0 against a guard that blocked the relative spelling.
|
|
564
|
-
emitGate((0, hook_program_js_1.decideProgram)(program, event, ctx,
|
|
583
|
+
emitGate((0, hook_program_js_1.decideProgram)(program, event, ctx, declaredRoot), program.on, (0, hook_program_js_1.hookMode)(program), file, root);
|
|
565
584
|
return;
|
|
566
585
|
}
|
|
567
586
|
case "prompt-gate": {
|
|
568
|
-
const ctx = await gatherHookContext(program, file);
|
|
569
|
-
emitGate((0, hook_program_js_1.decidePromptGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
587
|
+
const ctx = await gatherHookContext(program, file, root);
|
|
588
|
+
emitGate((0, hook_program_js_1.decidePromptGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file, root);
|
|
570
589
|
return;
|
|
571
590
|
}
|
|
572
591
|
case "stop-gate": {
|
|
573
|
-
const ctx = await gatherHookContext(program, file);
|
|
574
|
-
emitGate((0, hook_program_js_1.decideStopGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
592
|
+
const ctx = await gatherHookContext(program, file, root);
|
|
593
|
+
emitGate((0, hook_program_js_1.decideStopGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file, root);
|
|
575
594
|
return;
|
|
576
595
|
}
|
|
577
596
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginLayout } from "./core/layout.js";
|
|
2
|
+
/** Every SHIPPED harness layout, in registry order. */
|
|
3
|
+
export declare const REGISTERED_LAYOUTS: readonly PluginLayout[];
|
|
4
|
+
/**
|
|
5
|
+
* "Is this repo-relative path read by some harness vigiles knows about?" — one
|
|
6
|
+
* predicate per registered layout, the input to `unclaimedSurfaces`.
|
|
7
|
+
*
|
|
8
|
+
* Note it is the layouts of every REGISTERED harness, not of the DETECTED one.
|
|
9
|
+
* A repo carrying both `.claude/skills` and `.agents/skills` has each half
|
|
10
|
+
* claimed by a different harness and neither is a finding; a repo whose skills
|
|
11
|
+
* sit under `.ai/` has them claimed by nobody, and that is the finding (#240).
|
|
12
|
+
*/
|
|
13
|
+
export declare const registeredClaims: ReadonlyArray<(path: string) => boolean>;
|
|
14
|
+
//# sourceMappingURL=layout-registry.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registeredClaims = exports.REGISTERED_LAYOUTS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The registered harnesses' LAYOUTS, as pure data — the claim side of surface
|
|
6
|
+
* discovery, available where the adapter bundle is not.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 WHY THIS IS NOT JUST `ADAPTERS.map(a => a.layout)`. `src/adapter-registry.ts`
|
|
9
|
+
* imports the full `HarnessAdapter` bundles, and an adapter's `detect(root)` does
|
|
10
|
+
* real filesystem work — so that module pulls `node:fs` in. `src/scan-files.ts`
|
|
11
|
+
* is the BROWSER-SAFE twin of the scan ("NO filesystem, NO child_process, NO disk
|
|
12
|
+
* I/O at all") and needs exactly one thing from the registry: which paths a
|
|
13
|
+
* shipped harness reads. A `PluginLayout` is a plain object of strings, so this
|
|
14
|
+
* list is importable from both sides.
|
|
15
|
+
*
|
|
16
|
+
* The drift that split lists invite is CHECKED, not hoped for:
|
|
17
|
+
* `src/layout-registry.test.ts` asserts this list is exactly the layouts the
|
|
18
|
+
* `ADAPTERS` registry carries, and that every adapter's `claims` agrees with
|
|
19
|
+
* `layoutClaims` over its own layout — so adding a harness to one registry and
|
|
20
|
+
* not the other fails a test instead of quietly halving discovery.
|
|
21
|
+
*/
|
|
22
|
+
const layout_js_1 = require("./adapters/claude-code/layout.js");
|
|
23
|
+
const layout_js_2 = require("./adapters/codex/layout.js");
|
|
24
|
+
const surface_discovery_js_1 = require("./core/surface-discovery.js");
|
|
25
|
+
/** Every SHIPPED harness layout, in registry order. */
|
|
26
|
+
exports.REGISTERED_LAYOUTS = [
|
|
27
|
+
layout_js_1.claudeCodeLayout,
|
|
28
|
+
layout_js_2.codexLayout,
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* "Is this repo-relative path read by some harness vigiles knows about?" — one
|
|
32
|
+
* predicate per registered layout, the input to `unclaimedSurfaces`.
|
|
33
|
+
*
|
|
34
|
+
* Note it is the layouts of every REGISTERED harness, not of the DETECTED one.
|
|
35
|
+
* A repo carrying both `.claude/skills` and `.agents/skills` has each half
|
|
36
|
+
* claimed by a different harness and neither is a finding; a repo whose skills
|
|
37
|
+
* sit under `.ai/` has them claimed by nobody, and that is the finding (#240).
|
|
38
|
+
*/
|
|
39
|
+
exports.registeredClaims = exports.REGISTERED_LAYOUTS.map((l) => (path) => (0, surface_discovery_js_1.layoutClaims)(l, path));
|
|
40
|
+
//# sourceMappingURL=layout-registry.js.map
|
package/dist/load-hook.d.ts
CHANGED
|
@@ -20,5 +20,5 @@ import { type AnyHook } from "./core/hook-program.js";
|
|
|
20
20
|
* @throws {HookCompileError} when the file can't be imported, or has no
|
|
21
21
|
* default-exported hook program.
|
|
22
22
|
*/
|
|
23
|
-
export declare function loadHook(file: string): Promise<AnyHook>;
|
|
23
|
+
export declare function loadHook(file: string, root?: string): Promise<AnyHook>;
|
|
24
24
|
//# sourceMappingURL=load-hook.d.ts.map
|
package/dist/load-hook.js
CHANGED
|
@@ -39,8 +39,8 @@ const hook_program_js_1 = require("./core/hook-program.js");
|
|
|
39
39
|
* @throws {HookCompileError} when the file can't be imported, or has no
|
|
40
40
|
* default-exported hook program.
|
|
41
41
|
*/
|
|
42
|
-
async function loadHook(file) {
|
|
43
|
-
const abs = (0, node_path_1.resolve)(
|
|
42
|
+
async function loadHook(file, root = process.cwd()) {
|
|
43
|
+
const abs = (0, node_path_1.resolve)(root, file);
|
|
44
44
|
let mod;
|
|
45
45
|
try {
|
|
46
46
|
mod = (await import((0, node_url_1.pathToFileURL)(abs).href));
|
package/dist/plugin-loader.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PluginLayout } from "./core/layout.js";
|
|
2
|
+
import type { ExcludeSet } from "./exclude.js";
|
|
2
3
|
export interface LoadedPlugin {
|
|
3
4
|
/** A `.claude/settings.json`-shaped object with hooks resolved. */
|
|
4
5
|
readonly settings: {
|
|
@@ -29,8 +30,55 @@ export interface LoadedPlugin {
|
|
|
29
30
|
* the files (CLAUDE.md + skills + agents + commands) to write into the test
|
|
30
31
|
* sandbox, and `warnings` for surfaces the deterministic tier can't drive. Merge
|
|
31
32
|
* `settings` with any inline settings and spread `files` into the fixture.
|
|
33
|
+
*
|
|
34
|
+
* `surfaceRoots` is this function's parameter name for what the repo owner
|
|
35
|
+
* writes as `.vigilesrc.json#harnesses["<name>"].roots` — extra repo-relative
|
|
36
|
+
* bases to read `<base>/<surfaceDir>/…` from, for a repo that keeps its skills
|
|
37
|
+
* somewhere no harness reads by default. The config key is nested UNDER a
|
|
38
|
+
* harness name precisely so a root cannot be declared without saying whose
|
|
39
|
+
* layout reads it; this parameter receives one harness's slice of that, which
|
|
40
|
+
* is why it is still a bare list here. `excludes` still wins over it:
|
|
41
|
+
* both the per-tree check in {@link materializeSurfaces} and `readTree` drop an
|
|
42
|
+
* excluded path whatever declared it, so a root that is declared AND excluded is
|
|
43
|
+
* read exactly as if it had never been declared.
|
|
32
44
|
*/
|
|
33
|
-
export declare function loadPlugin(pluginPath: string, layout: PluginLayout): LoadedPlugin;
|
|
45
|
+
export declare function loadPlugin(pluginPath: string, layout: PluginLayout, excludes?: ExcludeSet, surfaceRoots?: readonly string[]): LoadedPlugin;
|
|
46
|
+
/**
|
|
47
|
+
* ONE declared harness for {@link loadPlugins}: the layout to read the repo
|
|
48
|
+
* under, and the extra roots declared for THAT harness.
|
|
49
|
+
*/
|
|
50
|
+
export interface HarnessLoad {
|
|
51
|
+
readonly layout: PluginLayout;
|
|
52
|
+
readonly roots?: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Load the repo once PER DECLARED HARNESS and merge the results into one
|
|
56
|
+
* `LoadedPlugin` (#240).
|
|
57
|
+
*
|
|
58
|
+
* 🔴 THE MERGE IS WHY THIS EXISTS, AND THE DEDUPLICATION IS ITS WHOLE CONTRACT.
|
|
59
|
+
* A repo that declares two harnesses is a repo whose grade must cover both — the
|
|
60
|
+
* flat `harness` array could not do that, because whichever name sat first
|
|
61
|
+
* decided the ONE layout everything was read under: measured on a repo with
|
|
62
|
+
* `AGENTS.md` + `.ai/skills/alpha/SKILL.md`, Claude-Code-first graded the skill
|
|
63
|
+
* and reported 0 chars of always-loaded instructions, Codex-first read
|
|
64
|
+
* `AGENTS.md` and reported no skill at all. Neither order produced both halves.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ AND THE OBVIOUS FIX HAS AN OBVIOUS SECOND BUG: a repo honest enough to say
|
|
67
|
+
* one tree serves both tools would then have that tree read twice and every
|
|
68
|
+
* skill in it counted twice, so declaring the truth would lower the grade. So the
|
|
69
|
+
* merge keys on the REAL ON-DISK PATH (`sources`), not on the materialized key:
|
|
70
|
+
* the first harness to claim a file keeps it, later ones skip it, and the counts
|
|
71
|
+
* are of files rather than of claims. Keying on the materialized key would not
|
|
72
|
+
* do — two layouts can reach one file under two different keys (`.ai/.agents`
|
|
73
|
+
* + `skills` and `.ai` + `.agents/skills` are the same directory), and that is
|
|
74
|
+
* exactly the case an honest dual declaration produces.
|
|
75
|
+
*
|
|
76
|
+
* Settings come from the FIRST harness that yields any, and the file-map merge
|
|
77
|
+
* is first-wins for the same reason: the primary harness is the one whose
|
|
78
|
+
* dialect the report is rendered in, so its reading of a shared path is the one
|
|
79
|
+
* the rest of the report is consistent with.
|
|
80
|
+
*/
|
|
81
|
+
export declare function loadPlugins(pluginPath: string, harnesses: readonly HarnessLoad[], excludes?: ExcludeSet): LoadedPlugin;
|
|
34
82
|
export declare function danglingRefs(root: string, layout: PluginLayout): string[];
|
|
35
83
|
/**
|
|
36
84
|
* Resolve the effective harness for a test/eval (arm): load the plugin if given,
|