vigiles 18.1.2 → 19.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/adapters/claude-code/run-scripts.d.ts +1 -1
- package/dist/adapters/claude-code/run-scripts.js +39 -4
- package/dist/cli.js +49 -18
- package/dist/core/doc-refs.d.ts +7 -1
- package/dist/core/doc-refs.js +8 -2
- package/dist/core/hook-program.d.ts +24 -0
- package/dist/core/hook-program.js +24 -0
- package/dist/core/rule-meta.js +8 -0
- package/dist/core/skill-resources.js +40 -2
- package/dist/core/types.d.ts +19 -0
- package/dist/core/validate.js +6 -0
- package/dist/hook.d.ts +1 -1
- package/dist/hook.js +16 -8
- package/dist/setup-plan.d.ts +1 -1
- package/dist/setup-plan.js +4 -0
- package/package.json +1 -1
|
@@ -61,7 +61,7 @@ export declare const SKIP_EXIT_CODE = 77;
|
|
|
61
61
|
* `assertNoWrite` already draws: "nobody looked" must not read as "nothing
|
|
62
62
|
* happened".
|
|
63
63
|
*/
|
|
64
|
-
export declare function statusFor(code: number, checks: number | undefined): ScriptStatus;
|
|
64
|
+
export declare function statusFor(code: number, checks: number | undefined, output?: string): ScriptStatus;
|
|
65
65
|
/** Filename extensions accepted for harness/eval scripts (JS and TS). */
|
|
66
66
|
export declare const SCRIPT_EXTS: readonly ["mjs", "cjs", "js", "mts", "cts", "ts"];
|
|
67
67
|
/** Glob suffix matching every accepted script extension, e.g. `harness`. */
|
|
@@ -60,13 +60,47 @@ exports.SKIP_EXIT_CODE = 77;
|
|
|
60
60
|
* `assertNoWrite` already draws: "nobody looked" must not read as "nothing
|
|
61
61
|
* happened".
|
|
62
62
|
*/
|
|
63
|
-
function statusFor(code, checks) {
|
|
63
|
+
function statusFor(code, checks, output) {
|
|
64
64
|
if (code === exports.SKIP_EXIT_CODE)
|
|
65
65
|
return "skip";
|
|
66
66
|
if (code !== 0)
|
|
67
|
-
return "fail";
|
|
67
|
+
return didNotLoad(output) ? "skip" : "fail";
|
|
68
68
|
return checks === 0 ? "vacuous" : "pass";
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Did the script fail to LOAD, rather than fail?
|
|
72
|
+
*
|
|
73
|
+
* 🔴 The distinction is not cosmetic, because `fail` RETRACTS coverage while
|
|
74
|
+
* `skip` does not (see the table on `runsFromResults`). The rule for `fail` —
|
|
75
|
+
* "it ran and proved nothing, so it must not keep yesterday's green record
|
|
76
|
+
* alive" — is right, and it does not describe a file the runtime never
|
|
77
|
+
* evaluated. A module that cannot resolve executed no assertion; it is the same
|
|
78
|
+
* "did not run" as a missing `claude` CLI, arriving through a different door.
|
|
79
|
+
*
|
|
80
|
+
* MEASURED, twice in one session on 2026-08-20: a container restore left an old
|
|
81
|
+
* `node_modules` behind, every harness in the consumer repo died on `Named export
|
|
82
|
+
* 'recordCheck' not found`, and the ledger dropped from 48 records to 34 and from
|
|
83
|
+
* 47 to 33. Nothing about those surfaces had changed — the machine had.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ This does NOT make a broken environment quiet. A skip still prints `⊘
|
|
86
|
+
* SKIPPED`, and `--no-skip` — which this repo's own CI passes — still fails the
|
|
87
|
+
* run. What changes is only whether a machine problem is allowed to delete a
|
|
88
|
+
* measurement taken on a machine that worked.
|
|
89
|
+
*
|
|
90
|
+
* Deliberately literal, and only the loader's own vocabulary: these strings come
|
|
91
|
+
* from Node's module resolution, not from user code. A test that legitimately
|
|
92
|
+
* asserts on one of them exits 0 or asserts, and never reaches here.
|
|
93
|
+
*/
|
|
94
|
+
function didNotLoad(output) {
|
|
95
|
+
if (output === undefined || output === "")
|
|
96
|
+
return false;
|
|
97
|
+
return (output.includes("ERR_MODULE_NOT_FOUND") ||
|
|
98
|
+
output.includes("Cannot find package") ||
|
|
99
|
+
output.includes("Cannot find module") ||
|
|
100
|
+
/SyntaxError: Named export '[^']*' not found/.test(output) ||
|
|
101
|
+
output.includes("ERR_UNSUPPORTED_DIR_IMPORT") ||
|
|
102
|
+
output.includes("ERR_PACKAGE_PATH_NOT_EXPORTED"));
|
|
103
|
+
}
|
|
70
104
|
/** Filename extensions accepted for harness/eval scripts (JS and TS). */
|
|
71
105
|
exports.SCRIPT_EXTS = ["mjs", "cjs", "js", "mts", "cts", "ts"];
|
|
72
106
|
/** Glob suffix matching every accepted script extension, e.g. `harness`. */
|
|
@@ -225,12 +259,13 @@ async function runScripts(files, cwd, env = {}, opts = {}) {
|
|
|
225
259
|
child.stdout.on("data", (c) => chunks.push(c));
|
|
226
260
|
child.stderr.on("data", (c) => chunks.push(c));
|
|
227
261
|
const finish = (code) => {
|
|
228
|
-
|
|
262
|
+
const output = Buffer.concat(chunks).toString("utf8");
|
|
263
|
+
process.stdout.write(output);
|
|
229
264
|
const report = readCheckReport(countFile);
|
|
230
265
|
resolveRun({
|
|
231
266
|
file,
|
|
232
267
|
code,
|
|
233
|
-
status: statusFor(code, report?.checks),
|
|
268
|
+
status: statusFor(code, report?.checks, output),
|
|
234
269
|
checks: report?.checks,
|
|
235
270
|
...(report ? { surfaces: report.surfaces } : {}),
|
|
236
271
|
});
|
package/dist/cli.js
CHANGED
|
@@ -701,11 +701,14 @@ function lintExitCode(report) {
|
|
|
701
701
|
report.hookBlockErrors > 0 ||
|
|
702
702
|
report.hookMatcherErrors > 0 ||
|
|
703
703
|
report.symbolRefErrors > 0 ||
|
|
704
|
-
report.mcpRefErrors > 0
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
704
|
+
report.mcpRefErrors > 0 ||
|
|
705
|
+
// `doc-refs` is opt-in and this counter is only non-zero when the user set
|
|
706
|
+
// it to "error", so it belongs in the hard tier with every other explicit
|
|
707
|
+
// error — it used to sit at exit 1 because it fired unasked and could not be
|
|
708
|
+
// turned off.
|
|
708
709
|
report.docRefErrors > 0)
|
|
710
|
+
return 2;
|
|
711
|
+
if (report.duplicatePairs > 0 || report.orphanCount > 0)
|
|
709
712
|
return 1;
|
|
710
713
|
// Guidance counts are informational, not failures
|
|
711
714
|
return 0;
|
|
@@ -1135,11 +1138,28 @@ async function runLint(restArgs, flags, config) {
|
|
|
1135
1138
|
// typo, an uncompilable or unreachable MCP pattern, one too narrow for real
|
|
1136
1139
|
// server naming, or an undeclared MCP server).
|
|
1137
1140
|
const hookMatcher = checkHookMatcher(config, silent, adapter, scanRoot);
|
|
1138
|
-
// 8. Validate vigiles builder calls inside markdown code blocks
|
|
1139
|
-
//
|
|
1140
|
-
// `<!-- vigiles:ignore -->` (single block) or
|
|
1141
|
-
//
|
|
1142
|
-
|
|
1141
|
+
// 8. Validate vigiles builder calls inside markdown code blocks — the
|
|
1142
|
+
// `doc-refs` rule, DEFAULT OFF. Illustrative blocks opt out via
|
|
1143
|
+
// `<!-- vigiles:ignore -->` (single block) or `<!-- vigiles:ignore-file -->`
|
|
1144
|
+
// (whole file). Same engine as spec.ts.
|
|
1145
|
+
//
|
|
1146
|
+
// 🔴 WHY OFF BY DEFAULT — the measurement, not a preference. Run across two
|
|
1147
|
+
// real repositories on 2026-08-19: 2 582 markdown files, 52 builder refs,
|
|
1148
|
+
// **0 true positives**, and every error it had ever raised was false — a
|
|
1149
|
+
// design note writing `cmd("npm test")` for a package that doesn't have that
|
|
1150
|
+
// script yet, a third-party `CLAUDE.md` captured verbatim as benchmark data.
|
|
1151
|
+
// The cause is structural: a fenced block in prose is a DRAWING of config, and
|
|
1152
|
+
// this pass read it as config. The consumer repo reached a clean `lint` only by
|
|
1153
|
+
// excluding a third of itself, after which the pass walked 604 files and found
|
|
1154
|
+
// 0 refs — fully inert, still paying for the walk. So it is now opt-in, and
|
|
1155
|
+
// when off the walk does not happen at all (that is the whole subtraction).
|
|
1156
|
+
//
|
|
1157
|
+
// ⚠️ Known gap for whoever improves it before flipping this back: the walker
|
|
1158
|
+
// globs without `dot`, so `.claude/**` — where real skills and agents live —
|
|
1159
|
+
// has never been scanned. The rule has never once looked at a deployed
|
|
1160
|
+
// instruction file; every ref it has ever judged was in prose.
|
|
1161
|
+
const docRefSeverity = (0, types_js_1.ruleSeverity)(config?.rules?.["doc-refs"]);
|
|
1162
|
+
if (!silent && docRefSeverity)
|
|
1143
1163
|
console.log("\nMarkdown code block refs:\n");
|
|
1144
1164
|
// 🔴 `config.exclude` REACHES THIS PASS. It did not, and that single omission is
|
|
1145
1165
|
// why `lint` could not exit 0 on a repository that vendors other people's
|
|
@@ -1154,12 +1174,20 @@ async function runLint(restArgs, flags, config) {
|
|
|
1154
1174
|
// With no way to reach 0 the step was made `continue-on-error: true`, and a lint
|
|
1155
1175
|
// whose exit code is discarded gates nothing — after which hand-written CI steps
|
|
1156
1176
|
// grew to do the gating instead. One unpassed argument, that whole chain.
|
|
1157
|
-
const docRefReport =
|
|
1158
|
-
basePath: process.cwd(),
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1177
|
+
const docRefReport = docRefSeverity
|
|
1178
|
+
? (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd(), ignore: config?.exclude })
|
|
1179
|
+
: {
|
|
1180
|
+
filesScanned: 0,
|
|
1181
|
+
filesIgnored: 0,
|
|
1182
|
+
blocksIgnored: 0,
|
|
1183
|
+
refs: [],
|
|
1184
|
+
errors: [],
|
|
1185
|
+
unverified: 0,
|
|
1186
|
+
placeholders: 0,
|
|
1187
|
+
};
|
|
1188
|
+
if (!silent && docRefSeverity) {
|
|
1189
|
+
const rendered = (0, doc_refs_js_1.formatDocRefReport)(docRefReport, docRefSeverity === "error" ? "error" : "warn");
|
|
1190
|
+
for (const line of rendered.split("\n")) {
|
|
1163
1191
|
console.log(` ${line}`);
|
|
1164
1192
|
}
|
|
1165
1193
|
}
|
|
@@ -1168,9 +1196,9 @@ async function runLint(restArgs, flags, config) {
|
|
|
1168
1196
|
// Previously this check reported to stdout only; the inline/spec checks already
|
|
1169
1197
|
// annotate per-line, so this closes the gap that left doc-ref findings invisible
|
|
1170
1198
|
// on the PR. CI-only (isGitHubActions); skipped under --json/--summary.
|
|
1171
|
-
if (isGitHubActions() && !silent) {
|
|
1199
|
+
if (isGitHubActions() && !silent && docRefSeverity) {
|
|
1172
1200
|
for (const e of docRefReport.errors) {
|
|
1173
|
-
ghAnnotate("error", `${e.kind}("${e.value}") — ${e.message}`, e.file, e.line);
|
|
1201
|
+
ghAnnotate(docRefSeverity === "error" ? "error" : "warning", `${e.kind}("${e.value}") — ${e.message}`, e.file, e.line);
|
|
1174
1202
|
}
|
|
1175
1203
|
}
|
|
1176
1204
|
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
@@ -1234,7 +1262,10 @@ async function runLint(restArgs, flags, config) {
|
|
|
1234
1262
|
hookBlockErrors: hookBlock.errors,
|
|
1235
1263
|
hookMatcherIssues: hookMatcher.issues,
|
|
1236
1264
|
hookMatcherErrors: hookMatcher.errors,
|
|
1237
|
-
|
|
1265
|
+
// Only the "error" tier gates. At "warn" the findings are printed and
|
|
1266
|
+
// annotated, and the exit code is untouched — same contract as every other
|
|
1267
|
+
// opt-in rule here.
|
|
1268
|
+
docRefErrors: docRefSeverity === "error" ? docRefReport.errors.length : 0,
|
|
1238
1269
|
symbolRefErrors,
|
|
1239
1270
|
mcpRefErrors,
|
|
1240
1271
|
files,
|
package/dist/core/doc-refs.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ interface ExtractResult {
|
|
|
55
55
|
*/
|
|
56
56
|
export declare function findDocRefs(options?: FindDocRefsOptions): DocRefReport;
|
|
57
57
|
/** Format a DocRefReport as human-readable text. */
|
|
58
|
-
export declare function formatDocRefReport(report: DocRefReport
|
|
58
|
+
export declare function formatDocRefReport(report: DocRefReport,
|
|
59
|
+
/**
|
|
60
|
+
* The severity `doc-refs` is configured at. A `✗` next to an exit code of 0
|
|
61
|
+
* reads as a gate that failed to gate, so the marker follows the severity the
|
|
62
|
+
* user actually set rather than always claiming the hard tier.
|
|
63
|
+
*/
|
|
64
|
+
severity?: "error" | "warn"): string;
|
|
59
65
|
export {};
|
|
60
66
|
//# sourceMappingURL=doc-refs.d.ts.map
|
package/dist/core/doc-refs.js
CHANGED
|
@@ -272,7 +272,13 @@ function findDocRefs(options = {}) {
|
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
274
|
/** Format a DocRefReport as human-readable text. */
|
|
275
|
-
function formatDocRefReport(report
|
|
275
|
+
function formatDocRefReport(report,
|
|
276
|
+
/**
|
|
277
|
+
* The severity `doc-refs` is configured at. A `✗` next to an exit code of 0
|
|
278
|
+
* reads as a gate that failed to gate, so the marker follows the severity the
|
|
279
|
+
* user actually set rather than always claiming the hard tier.
|
|
280
|
+
*/
|
|
281
|
+
severity = "error") {
|
|
276
282
|
const lines = [];
|
|
277
283
|
const meta = [];
|
|
278
284
|
if (report.filesIgnored > 0)
|
|
@@ -288,7 +294,7 @@ function formatDocRefReport(report) {
|
|
|
288
294
|
lines.push(`${String(report.refs.length)} vigiles refs in code blocks${report.errors.length === 0 ? " — all valid" : ""}`);
|
|
289
295
|
if (report.errors.length === 0)
|
|
290
296
|
return lines.join("\n");
|
|
291
|
-
lines.push(
|
|
297
|
+
lines.push(`${severity === "error" ? "✗" : "ℹ"} ${String(report.errors.length)} broken ref(s):`);
|
|
292
298
|
for (const e of report.errors.slice(0, 12)) {
|
|
293
299
|
const trunc = e.message.length > 80 ? `${e.message.slice(0, 80)}…` : e.message;
|
|
294
300
|
lines.push(` ${e.file}:${String(e.line)} ${e.kind}("${e.value}") — ${trunc}`);
|
|
@@ -262,6 +262,10 @@ export interface HookProgram<N extends readonly NeedSpec[] = readonly ProviderNa
|
|
|
262
262
|
export declare const tool: (name: string) => {
|
|
263
263
|
tool: string;
|
|
264
264
|
};
|
|
265
|
+
/**
|
|
266
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
267
|
+
* Imported as `experimental_defineHook`; alias it at the import site.
|
|
268
|
+
*/
|
|
265
269
|
export declare function defineHook<const N extends readonly NeedSpec[] = readonly []>(p: HookProgram<N>): HookProgram<N>;
|
|
266
270
|
/**
|
|
267
271
|
* Build the typed event from a raw PreToolUse event, then decide.
|
|
@@ -506,6 +510,10 @@ export interface FileGateHook<N extends readonly NeedSpec[] = readonly ProviderN
|
|
|
506
510
|
export declare const tools: (...names: string[]) => {
|
|
507
511
|
tools: string[];
|
|
508
512
|
};
|
|
513
|
+
/**
|
|
514
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
515
|
+
* Imported as `experimental_defineFileGate`; alias it at the import site.
|
|
516
|
+
*/
|
|
509
517
|
export declare function defineFileGate<const N extends readonly NeedSpec[] = readonly []>(p: Omit<FileGateHook<N>, "role">): FileGateHook<N>;
|
|
510
518
|
/**
|
|
511
519
|
* Run a file-tool gate against a raw PreToolUse event (reads `file_path`).
|
|
@@ -542,6 +550,10 @@ export interface PromptGateHook<N extends readonly NeedSpec[] = readonly Provide
|
|
|
542
550
|
/** Decide over the prompt. `deny` blocks/erases the prompt; `ask` defers to the user. */
|
|
543
551
|
readonly decide: (e: PromptEvent<N>) => Decision;
|
|
544
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
555
|
+
* Imported as `experimental_definePromptGate`; alias it at the import site.
|
|
556
|
+
*/
|
|
545
557
|
export declare function definePromptGate<const N extends readonly NeedSpec[] = readonly []>(p: Omit<PromptGateHook<N>, "role">): PromptGateHook<N>;
|
|
546
558
|
/** Run a prompt gate against a raw UserPromptSubmit event (reads `prompt`). */
|
|
547
559
|
export declare function decidePromptGate<N extends readonly NeedSpec[]>(hook: PromptGateHook<N>, raw: {
|
|
@@ -573,6 +585,10 @@ export interface StopGateHook<N extends readonly NeedSpec[] = readonly ProviderN
|
|
|
573
585
|
/** Decide whether the agent may stop. `deny` keeps it going; `allow` lets it stop. */
|
|
574
586
|
readonly decide: (e: StopEvent<N>) => Decision;
|
|
575
587
|
}
|
|
588
|
+
/**
|
|
589
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
590
|
+
* Imported as `experimental_defineStopGate`; alias it at the import site.
|
|
591
|
+
*/
|
|
576
592
|
export declare function defineStopGate<const N extends readonly NeedSpec[] = readonly []>(p: Omit<StopGateHook<N>, "role">): StopGateHook<N>;
|
|
577
593
|
/** Run a Stop gate against a raw Stop/SubagentStop event (reads `stop_hook_active`). */
|
|
578
594
|
export declare function decideStopGate<N extends readonly NeedSpec[]>(hook: StopGateHook<N>, raw: {
|
|
@@ -618,6 +634,10 @@ export interface InjectHook<N extends readonly NeedSpec[] = readonly ProviderNam
|
|
|
618
634
|
/** Produces context to add. Its return type (Injection) has no `deny` — by design. */
|
|
619
635
|
readonly produce: (e: SessionEvent<N>) => Injection;
|
|
620
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
639
|
+
* Imported as `experimental_defineInject`; alias it at the import site.
|
|
640
|
+
*/
|
|
621
641
|
export declare function defineInject<const N extends readonly NeedSpec[] = readonly []>(p: Omit<InjectHook<N>, "role">): InjectHook<N>;
|
|
622
642
|
/**
|
|
623
643
|
* Run an inject hook → the CC JSON the author never hand-writes. The compiler
|
|
@@ -719,6 +739,10 @@ export interface ReactHook<N extends readonly NeedSpec[] = readonly ProviderName
|
|
|
719
739
|
/** Reacts to a tool that already ran. Returns a Reaction — NO `deny` exists here. */
|
|
720
740
|
readonly react: (e: ReactEvent<N>) => Reaction;
|
|
721
741
|
}
|
|
742
|
+
/**
|
|
743
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
744
|
+
* Imported as `experimental_defineReact`; alias it at the import site.
|
|
745
|
+
*/
|
|
722
746
|
export declare function defineReact<const N extends readonly NeedSpec[] = readonly []>(p: Omit<ReactHook<N>, "role">): ReactHook<N>;
|
|
723
747
|
/**
|
|
724
748
|
* Run a react hook against a raw PostToolUse event → the (classified) Reaction.
|
|
@@ -535,6 +535,10 @@ function commandView(raw, root) {
|
|
|
535
535
|
}
|
|
536
536
|
const tool = (name) => ({ tool: name });
|
|
537
537
|
exports.tool = tool;
|
|
538
|
+
/**
|
|
539
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
540
|
+
* Imported as `experimental_defineHook`; alias it at the import site.
|
|
541
|
+
*/
|
|
538
542
|
function defineHook(p) {
|
|
539
543
|
return p;
|
|
540
544
|
}
|
|
@@ -913,6 +917,10 @@ const tools = (...names) => ({
|
|
|
913
917
|
tools: names,
|
|
914
918
|
});
|
|
915
919
|
exports.tools = tools;
|
|
920
|
+
/**
|
|
921
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
922
|
+
* Imported as `experimental_defineFileGate`; alias it at the import site.
|
|
923
|
+
*/
|
|
916
924
|
function defineFileGate(p) {
|
|
917
925
|
return { role: "gate", ...p };
|
|
918
926
|
}
|
|
@@ -940,6 +948,10 @@ function decideFileGate(hook, raw, ctx = {}, root = typeof raw.cwd === "string"
|
|
|
940
948
|
ctx: ctx,
|
|
941
949
|
});
|
|
942
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
953
|
+
* Imported as `experimental_definePromptGate`; alias it at the import site.
|
|
954
|
+
*/
|
|
943
955
|
function definePromptGate(p) {
|
|
944
956
|
return { role: "prompt-gate", ...p };
|
|
945
957
|
}
|
|
@@ -952,6 +964,10 @@ function decidePromptGate(hook, raw, ctx = {}) {
|
|
|
952
964
|
ctx: ctx,
|
|
953
965
|
});
|
|
954
966
|
}
|
|
967
|
+
/**
|
|
968
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
969
|
+
* Imported as `experimental_defineStopGate`; alias it at the import site.
|
|
970
|
+
*/
|
|
955
971
|
function defineStopGate(p) {
|
|
956
972
|
return { role: "stop-gate", ...p };
|
|
957
973
|
}
|
|
@@ -977,6 +993,10 @@ const inject = (context, ...records) => ({
|
|
|
977
993
|
records,
|
|
978
994
|
});
|
|
979
995
|
exports.inject = inject;
|
|
996
|
+
/**
|
|
997
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
998
|
+
* Imported as `experimental_defineInject`; alias it at the import site.
|
|
999
|
+
*/
|
|
980
1000
|
function defineInject(p) {
|
|
981
1001
|
return { role: "inject", ...p };
|
|
982
1002
|
}
|
|
@@ -1055,6 +1075,10 @@ const nothing = (...records) => ({
|
|
|
1055
1075
|
records,
|
|
1056
1076
|
});
|
|
1057
1077
|
exports.nothing = nothing;
|
|
1078
|
+
/**
|
|
1079
|
+
* @experimental Compiled hooks are provisional — see docs/experimental.md.
|
|
1080
|
+
* Imported as `experimental_defineReact`; alias it at the import site.
|
|
1081
|
+
*/
|
|
1058
1082
|
function defineReact(p) {
|
|
1059
1083
|
return { role: "react", ...p };
|
|
1060
1084
|
}
|
package/dist/core/rule-meta.js
CHANGED
|
@@ -143,6 +143,14 @@ exports.RULE_META = {
|
|
|
143
143
|
detector: "hookMatcherIssues",
|
|
144
144
|
upstreamPrevention: "compiled hook tool()/tools() matcher is typed",
|
|
145
145
|
},
|
|
146
|
+
"doc-refs": {
|
|
147
|
+
id: "doc-refs",
|
|
148
|
+
bucket: "external-decidable",
|
|
149
|
+
surface: ["docs"],
|
|
150
|
+
defaultSeverity: "off",
|
|
151
|
+
summary: "Builder calls quoted in markdown ```ts fences resolve for real (opt-in: a fence is usually a drawing of config, not config).",
|
|
152
|
+
detector: "findDocRefs",
|
|
153
|
+
},
|
|
146
154
|
"prefer-compiled-hooks": {
|
|
147
155
|
id: "prefer-compiled-hooks",
|
|
148
156
|
bucket: "heuristic-behavioral",
|
|
@@ -210,6 +210,44 @@ const ILLUSTRATIVE_CUE = /\b(example|examples|e\.g\.?|i\.e\.?|such as|for instan
|
|
|
210
210
|
* net is affordable where a blanket one is not.
|
|
211
211
|
*/
|
|
212
212
|
const MD_HEADING = /^\s{0,3}#{1,6}\s/;
|
|
213
|
+
/**
|
|
214
|
+
* Whether the line describes a file the skill PRODUCES rather than one it ships.
|
|
215
|
+
*
|
|
216
|
+
* 🔴 WHY THIS IS SEPARATE FROM {@link ILLUSTRATIVE_CUE}. That one vetoes a
|
|
217
|
+
* HYPOTHETICAL mention — "a `scripts/rotate.py` would be helpful". This one
|
|
218
|
+
* vetoes a mention that is entirely real and entirely concrete, and still names
|
|
219
|
+
* a file that cannot be a bundled resource, because the skill's own prose says
|
|
220
|
+
* the file is written at runtime. Same veto, different reason, and collapsing
|
|
221
|
+
* them into one list would make the next reader think a gitignored cache is a
|
|
222
|
+
* kind of example.
|
|
223
|
+
*
|
|
224
|
+
* It is the read the module already takes elsewhere: {@link USE_DIRECTIVE}
|
|
225
|
+
* deliberately EXCLUDES authoring verbs (store/create/add/move/write) because
|
|
226
|
+
* "a file to CREATE" is not "a file the skill already ships". A cache written
|
|
227
|
+
* by the skill's own script is that same case; the verb list simply never
|
|
228
|
+
* covered the writing side.
|
|
229
|
+
*
|
|
230
|
+
* MEASURED 2026-08-20 on a real consumer skill (`verify-citations`), which read:
|
|
231
|
+
*
|
|
232
|
+
* API responses are cached to `scripts/.cite-cache.json` (gitignored) so
|
|
233
|
+
* re-runs are cheap.
|
|
234
|
+
*
|
|
235
|
+
* That produced `bundled resource not found`. Isolated to one word with a
|
|
236
|
+
* three-line fixture differing only in its tail: the same sentence ending
|
|
237
|
+
* "so subsequent invocations are cheap" stayed silent, and the same sentence
|
|
238
|
+
* with no tail stayed silent. The culprit is `re-runs` — JavaScript's `\b`
|
|
239
|
+
* counts a hyphen as a word boundary, so `USE_DIRECTIVE`'s `runs` matches
|
|
240
|
+
* INSIDE it, and a plural noun about repetition was read as an instruction to
|
|
241
|
+
* run the file.
|
|
242
|
+
*
|
|
243
|
+
* Fixing the hyphen instead was rejected by that same measurement: "re-run
|
|
244
|
+
* `scripts/x.sh` before submitting" is a genuine directive, so a rule about
|
|
245
|
+
* hyphens would trade this false positive for a false negative. The honest
|
|
246
|
+
* discriminator is not the hyphen, it is that the line says the file is made,
|
|
247
|
+
* not read. `gitignored` is the strongest form — a file the author states is
|
|
248
|
+
* outside the repository cannot be shipped inside it.
|
|
249
|
+
*/
|
|
250
|
+
const GENERATED_FILE_CUE = /\bgit-?ignored\b|\.gitignore\b|\bcached? to\b|\bcache file\b|\bwrit(?:ten|es) to\b|\b(?:generated|created) at runtime\b/i;
|
|
213
251
|
/**
|
|
214
252
|
* Whether a bundle-path reference on this line reads as a REAL reference (the
|
|
215
253
|
* agent is told to use the file) rather than an illustrative mention. Requires
|
|
@@ -222,7 +260,7 @@ const MD_HEADING = /^\s{0,3}#{1,6}\s/;
|
|
|
222
260
|
* real dead ref.
|
|
223
261
|
*/
|
|
224
262
|
function inlinePathIsUsed(line) {
|
|
225
|
-
if (ILLUSTRATIVE_CUE.test(line))
|
|
263
|
+
if (ILLUSTRATIVE_CUE.test(line) || GENERATED_FILE_CUE.test(line))
|
|
226
264
|
return false;
|
|
227
265
|
return USE_DIRECTIVE.test(line) || MD_HEADING.test(line);
|
|
228
266
|
}
|
|
@@ -241,7 +279,7 @@ function candidateFor(ref, line) {
|
|
|
241
279
|
// Suppress it ONLY on an illustrative cue; do NOT also require a use
|
|
242
280
|
// directive, or a plain `Resources: [API](references/api.md)` (no verb)
|
|
243
281
|
// goes unchecked (Codex review — that under-detection).
|
|
244
|
-
if (ILLUSTRATIVE_CUE.test(line))
|
|
282
|
+
if (ILLUSTRATIVE_CUE.test(line) || GENERATED_FILE_CUE.test(line))
|
|
245
283
|
return null;
|
|
246
284
|
const resolved = localResourceTarget(ref.value);
|
|
247
285
|
if (resolved === null)
|
package/dist/core/types.d.ts
CHANGED
|
@@ -321,6 +321,25 @@ export interface RulesConfig {
|
|
|
321
321
|
* docs/rules/hook-matcher.md.
|
|
322
322
|
*/
|
|
323
323
|
"hook-matcher"?: RuleSeverity;
|
|
324
|
+
/**
|
|
325
|
+
* Validate vigiles-builder calls quoted inside ```ts fences in markdown —
|
|
326
|
+
* `enforce()` / `file()` / `cmd()` / `ref()` — against the real linter catalog,
|
|
327
|
+
* filesystem and package scripts.
|
|
328
|
+
*
|
|
329
|
+
* DEFAULT OFF, and the default is the finding. Measured on two repositories
|
|
330
|
+
* 2026-08-19 (2 582 markdown files, 52 refs): **zero true positives, and every
|
|
331
|
+
* error it has ever raised was a false one** — a design sketch writing
|
|
332
|
+
* `cmd("npm test")` for a package that doesn't exist yet, or a third-party
|
|
333
|
+
* `CLAUDE.md` captured verbatim as benchmark data. The reason is structural,
|
|
334
|
+
* not calibration: a fenced block in prose is a DRAWING of config, and this
|
|
335
|
+
* rule reads it as config. The consumer repo could only reach a clean `lint`
|
|
336
|
+
* by excluding a third of itself, after which the pass scanned 604 files and
|
|
337
|
+
* found 0 refs — inert, and still paying for the walk.
|
|
338
|
+
*
|
|
339
|
+
* Turn it on where markdown really is the source (a docs site whose fences are
|
|
340
|
+
* copy-pasted into live specs). See docs/rules/doc-refs.md.
|
|
341
|
+
*/
|
|
342
|
+
"doc-refs"?: RuleSeverity;
|
|
324
343
|
}
|
|
325
344
|
/** Extract severity from a rule value (handles both simple and tuple forms). */
|
|
326
345
|
export declare function ruleSeverity<T>(rule: RuleWithOptions<T> | undefined): RuleSeverity;
|
package/dist/core/validate.js
CHANGED
|
@@ -98,6 +98,12 @@ exports.DEFAULT_RULES = {
|
|
|
98
98
|
// that matches no tool name, or one too narrow for real server names) — WARN
|
|
99
99
|
// by default (high-precision); raise to error to gate CI.
|
|
100
100
|
"hook-matcher": "warn",
|
|
101
|
+
// Builder calls quoted inside ```ts fences in markdown — default OFF. Measured
|
|
102
|
+
// over 2 582 markdown files across two repos: 52 refs, 0 true positives, and
|
|
103
|
+
// every error raised was a false one (a design sketch's `cmd("npm test")`, a
|
|
104
|
+
// vendored third-party CLAUDE.md). A fence in prose is a DRAWING of config;
|
|
105
|
+
// reading it as config is the defect. Opt in where markdown IS the source.
|
|
106
|
+
"doc-refs": false,
|
|
101
107
|
};
|
|
102
108
|
const DEFAULT_CONFIG = {
|
|
103
109
|
ruleMarkers: ["headings", "checkboxes"],
|
package/dist/hook.d.ts
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* tool calls. A gate is a strong default, never an unbypassable wall. See
|
|
46
46
|
* `docs/compiled-hooks.md`.
|
|
47
47
|
*/
|
|
48
|
-
export { defineHook, defineFileGate, definePromptGate, defineStopGate, tool, tools, allow, deny, ask, commandView, pathView, gateAction, hookMode, defineInject, inject, defineReact, run, notice, nothing, responseView, decideProgram, decideFileGate, decidePromptGate, decideStopGate, runInject, runReact, runHookProgram, decisionExitCode, dispatchKind, hookRouting, hookNeeds, injectionOf, outcomeWrites, matchesTool, invalidToolPatterns, compileHookProgram, checkHookImports, stampHook, verifyHookStamp, HookCompileError, } from "./core/hook-program.js";
|
|
48
|
+
export { defineHook as experimental_defineHook, defineFileGate as experimental_defineFileGate, definePromptGate as experimental_definePromptGate, defineStopGate as experimental_defineStopGate, tool, tools, allow, deny, ask, commandView, pathView, gateAction, hookMode, defineInject as experimental_defineInject, inject, defineReact as experimental_defineReact, run, notice, nothing, responseView, decideProgram, decideFileGate, decidePromptGate, decideStopGate, runInject, runReact, runHookProgram, decisionExitCode, dispatchKind, hookRouting, hookNeeds, injectionOf, outcomeWrites, matchesTool, invalidToolPatterns, compileHookProgram, checkHookImports, stampHook, verifyHookStamp, HookCompileError, } from "./core/hook-program.js";
|
|
49
49
|
export type { Decision, HookMode, GateAction, CommandView, PathView, ResponseView, BashToolEvent, FileToolEvent, PromptEvent, StopEvent, ReactEvent, SessionEvent, HookProgram, FileGateHook, PromptGateHook, StopGateHook, InjectHook, ReactHook, AnyHook, DispatchKind, Injection, Reaction, RunReaction, CompiledHookProgram, CompileHookOptions, RawHookEvent, HookProgramOutcome, } from "./core/hook-program.js";
|
|
50
50
|
export { provide, dangerously, defineProvider, provider, } from "./core/hook-providers.js";
|
|
51
51
|
export { state, record, stateFact, isValidStateKey, isStateNeed, isStateWrite, admissibleWrites, durationSeconds, HookStateError, } from "./core/hook-state.js";
|
package/dist/hook.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isStateWrite = exports.isStateNeed = exports.isValidStateKey = exports.stateFact = exports.record = exports.state = exports.provider = exports.defineProvider = exports.dangerously = exports.provide = exports.HookCompileError = exports.verifyHookStamp = exports.stampHook = exports.checkHookImports = exports.compileHookProgram = exports.invalidToolPatterns = exports.matchesTool = exports.outcomeWrites = exports.injectionOf = exports.hookNeeds = exports.hookRouting = exports.dispatchKind = exports.decisionExitCode = exports.runHookProgram = exports.runReact = exports.runInject = exports.decideStopGate = exports.decidePromptGate = exports.decideFileGate = exports.decideProgram = exports.responseView = exports.nothing = exports.notice = exports.run = exports.
|
|
3
|
+
exports.isStateWrite = exports.isStateNeed = exports.isValidStateKey = exports.stateFact = exports.record = exports.state = exports.provider = exports.defineProvider = exports.dangerously = exports.provide = exports.HookCompileError = exports.verifyHookStamp = exports.stampHook = exports.checkHookImports = exports.compileHookProgram = exports.invalidToolPatterns = exports.matchesTool = exports.outcomeWrites = exports.injectionOf = exports.hookNeeds = exports.hookRouting = exports.dispatchKind = exports.decisionExitCode = exports.runHookProgram = exports.runReact = exports.runInject = exports.decideStopGate = exports.decidePromptGate = exports.decideFileGate = exports.decideProgram = exports.responseView = exports.nothing = exports.notice = exports.run = exports.experimental_defineReact = exports.inject = exports.experimental_defineInject = exports.hookMode = exports.gateAction = exports.pathView = exports.commandView = exports.ask = exports.deny = exports.allow = exports.tools = exports.tool = exports.experimental_defineStopGate = exports.experimental_definePromptGate = exports.experimental_defineFileGate = exports.experimental_defineHook = void 0;
|
|
4
4
|
exports.leafCommandsNormalized = exports.HookStateError = exports.durationSeconds = exports.admissibleWrites = void 0;
|
|
5
5
|
/**
|
|
6
6
|
* `vigiles/hook` — the **closed vocabulary** for authoring a compiled hook.
|
|
@@ -50,11 +50,19 @@ exports.leafCommandsNormalized = exports.HookStateError = exports.durationSecond
|
|
|
50
50
|
* `docs/compiled-hooks.md`.
|
|
51
51
|
*/
|
|
52
52
|
var hook_program_js_1 = require("./core/hook-program.js");
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
// ── the six ENTRY POINTS carry the experimental marking ─────────────────────
|
|
54
|
+
// Same shape as `experimental_skill`, and for the same stated reason: every
|
|
55
|
+
// other name in this file — `allow`, `deny`, `tool`, `pathView`, `state`,
|
|
56
|
+
// `record` — is reachable ONLY from inside a `define*` call, so prefixing the
|
|
57
|
+
// entry points makes the marking structural for the whole vocabulary. A
|
|
58
|
+
// per-name prefix could not guarantee that; a chokepoint can.
|
|
59
|
+
//
|
|
60
|
+
// Import them aliased, so the word crosses the package boundary exactly once:
|
|
61
|
+
// import { experimental_defineInject as defineInject, state } from "vigiles/hook";
|
|
62
|
+
Object.defineProperty(exports, "experimental_defineHook", { enumerable: true, get: function () { return hook_program_js_1.defineHook; } });
|
|
63
|
+
Object.defineProperty(exports, "experimental_defineFileGate", { enumerable: true, get: function () { return hook_program_js_1.defineFileGate; } });
|
|
64
|
+
Object.defineProperty(exports, "experimental_definePromptGate", { enumerable: true, get: function () { return hook_program_js_1.definePromptGate; } });
|
|
65
|
+
Object.defineProperty(exports, "experimental_defineStopGate", { enumerable: true, get: function () { return hook_program_js_1.defineStopGate; } });
|
|
58
66
|
Object.defineProperty(exports, "tool", { enumerable: true, get: function () { return hook_program_js_1.tool; } });
|
|
59
67
|
Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return hook_program_js_1.tools; } });
|
|
60
68
|
Object.defineProperty(exports, "allow", { enumerable: true, get: function () { return hook_program_js_1.allow; } });
|
|
@@ -65,10 +73,10 @@ Object.defineProperty(exports, "pathView", { enumerable: true, get: function ()
|
|
|
65
73
|
Object.defineProperty(exports, "gateAction", { enumerable: true, get: function () { return hook_program_js_1.gateAction; } });
|
|
66
74
|
Object.defineProperty(exports, "hookMode", { enumerable: true, get: function () { return hook_program_js_1.hookMode; } });
|
|
67
75
|
// inject vocabulary
|
|
68
|
-
Object.defineProperty(exports, "
|
|
76
|
+
Object.defineProperty(exports, "experimental_defineInject", { enumerable: true, get: function () { return hook_program_js_1.defineInject; } });
|
|
69
77
|
Object.defineProperty(exports, "inject", { enumerable: true, get: function () { return hook_program_js_1.inject; } });
|
|
70
78
|
// react vocabulary
|
|
71
|
-
Object.defineProperty(exports, "
|
|
79
|
+
Object.defineProperty(exports, "experimental_defineReact", { enumerable: true, get: function () { return hook_program_js_1.defineReact; } });
|
|
72
80
|
Object.defineProperty(exports, "run", { enumerable: true, get: function () { return hook_program_js_1.run; } });
|
|
73
81
|
Object.defineProperty(exports, "notice", { enumerable: true, get: function () { return hook_program_js_1.notice; } });
|
|
74
82
|
Object.defineProperty(exports, "nothing", { enumerable: true, get: function () { return hook_program_js_1.nothing; } });
|
package/dist/setup-plan.d.ts
CHANGED
|
@@ -123,7 +123,7 @@ export declare const WORKFLOW_RULES: readonly ["require-instructions-spec", "unt
|
|
|
123
123
|
* keep their own default severities. Named for the group taxonomy
|
|
124
124
|
* (research/install-enforcement-dx.md).
|
|
125
125
|
*/
|
|
126
|
-
export declare const NUDGE_RULES: readonly ["skill-description-budget", "frontmatter-valid", "skill-frontmatter", "prefer-compiled-hooks", "unmarked-refs", "lethal-trifecta", "skill-resource-resolves", "skill-missing-fence", "plugin-dir-layout", "delegation-trifecta", "hook-block-ineffective", "hook-matcher"];
|
|
126
|
+
export declare const NUDGE_RULES: readonly ["skill-description-budget", "frontmatter-valid", "skill-frontmatter", "prefer-compiled-hooks", "unmarked-refs", "lethal-trifecta", "skill-resource-resolves", "skill-missing-fence", "plugin-dir-layout", "delegation-trifecta", "hook-block-ineffective", "hook-matcher", "doc-refs"];
|
|
127
127
|
export declare function mergeProjectConfig(existing: Record<string, unknown>, opts: {
|
|
128
128
|
harness: string | string[];
|
|
129
129
|
strict: boolean;
|
package/dist/setup-plan.js
CHANGED
|
@@ -140,6 +140,10 @@ exports.NUDGE_RULES = [
|
|
|
140
140
|
"delegation-trifecta",
|
|
141
141
|
"hook-block-ineffective",
|
|
142
142
|
"hook-matcher",
|
|
143
|
+
// Default OFF for a measured reason, not a rollout one: 0 true positives over
|
|
144
|
+
// 2 582 markdown files, because a fence in prose is a drawing of config rather
|
|
145
|
+
// than config. See docs/rules/doc-refs.md.
|
|
146
|
+
"doc-refs",
|
|
143
147
|
];
|
|
144
148
|
function mergeProjectConfig(existing, opts) {
|
|
145
149
|
const config = { ...existing };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "19.0.0",
|
|
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",
|