vigiles 16.1.2 → 17.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.
Files changed (42) hide show
  1. package/dist/adapter-conformance.js +17 -0
  2. package/dist/adapters/claude-code/dialect.d.ts +19 -13
  3. package/dist/adapters/claude-code/dialect.js +40 -62
  4. package/dist/adapters/claude-code/run-scripts.d.ts +19 -3
  5. package/dist/adapters/claude-code/run-scripts.js +17 -7
  6. package/dist/adapters/claude-code/vocabulary.d.ts +133 -0
  7. package/dist/adapters/claude-code/vocabulary.js +208 -0
  8. package/dist/adapters/codex/eval.js +2 -0
  9. package/dist/audit-score.js +1 -1
  10. package/dist/cli.js +7 -2
  11. package/dist/core/compile.js +6 -1
  12. package/dist/core/dialect.d.ts +27 -0
  13. package/dist/core/eval-load-phase.d.ts +78 -0
  14. package/dist/core/eval-load-phase.js +104 -0
  15. package/dist/core/hook-events.d.ts +32 -15
  16. package/dist/core/hook-events.js +23 -29
  17. package/dist/core/hook-program.js +12 -4
  18. package/dist/core/rule-meta.js +2 -2
  19. package/dist/core/tool-contract.d.ts +69 -30
  20. package/dist/core/tool-contract.js +59 -57
  21. package/dist/core/vocabulary-consistency.d.ts +35 -0
  22. package/dist/core/vocabulary-consistency.js +81 -0
  23. package/dist/core/vocabulary.d.ts +138 -0
  24. package/dist/core/vocabulary.js +262 -0
  25. package/dist/eval-define.d.ts +166 -0
  26. package/dist/eval-define.js +182 -0
  27. package/dist/eval-entry.d.ts +41 -0
  28. package/dist/eval-entry.js +203 -0
  29. package/dist/eval.js +2 -0
  30. package/dist/judge.js +2 -0
  31. package/dist/scan-behavioral.js +2 -0
  32. package/dist/scan-core.d.ts +8 -1
  33. package/dist/scan-core.js +64 -6
  34. package/dist/scan-files.js +4 -1
  35. package/dist/scan.d.ts +45 -0
  36. package/dist/scan.js +13 -1
  37. package/dist/test-coverage.d.ts +45 -0
  38. package/dist/test-coverage.js +91 -3
  39. package/dist/test.d.ts +2 -0
  40. package/dist/test.js +8 -1
  41. package/package.json +1 -1
  42. package/skills/test-harness/SKILL.md +31 -22
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claudeCodeSubagentToolVocabulary = exports.claudeCodeHookEventVocabulary = exports.claudeCodeSideEffectingAgentTools = exports.claudeCodeConditionalAgentToolNames = exports.claudeCodeWithheldAgentTools = exports.claudeCodeAvailableAgentTools = exports.claudeCodeHookEventNames = void 0;
4
+ /** The vendor artifact + version both catalogs below were captured from. */
5
+ const CAPTURE = "code.claude.com/docs (claude-code 2.1.233, read 2026-08-17)";
6
+ /**
7
+ * The Claude Code hook events — the 31 `###` subsections under `## Hook events`
8
+ * at https://code.claude.com/docs/en/hooks, in document order.
9
+ *
10
+ * `Setup` is the third. It is a real event with its own `Setup input` and
11
+ * `Setup decision control` subsections; the old catalog's absence of it is what
12
+ * produced the two-letter grade inversion this file exists to fix.
13
+ */
14
+ exports.claudeCodeHookEventNames = [
15
+ "SessionStart",
16
+ "Setup",
17
+ "InstructionsLoaded",
18
+ "UserPromptSubmit",
19
+ "UserPromptExpansion",
20
+ "MessageDisplay",
21
+ "PreToolUse",
22
+ "PermissionRequest",
23
+ "PostToolUse",
24
+ "PostToolUseFailure",
25
+ "PostToolBatch",
26
+ "PermissionDenied",
27
+ "Notification",
28
+ "SubagentStart",
29
+ "SubagentStop",
30
+ "TaskCreated",
31
+ "TaskCompleted",
32
+ "Stop",
33
+ "StopFailure",
34
+ "TeammateIdle",
35
+ "ConfigChange",
36
+ "CwdChanged",
37
+ "DirectoryAdded",
38
+ "FileChanged",
39
+ "WorktreeCreate",
40
+ "WorktreeRemove",
41
+ "PreCompact",
42
+ "PostCompact",
43
+ "SessionEnd",
44
+ "Elicitation",
45
+ "ElicitationResult",
46
+ ];
47
+ /**
48
+ * The 19 built-ins the vendor names as the set a BACKGROUND subagent keeps —
49
+ * the default case, and the only tool set vigiles can assume without reading
50
+ * conditions it cannot see.
51
+ */
52
+ exports.claudeCodeAvailableAgentTools = [
53
+ "Read",
54
+ "Grep",
55
+ "Glob",
56
+ "Bash",
57
+ "PowerShell",
58
+ "Edit",
59
+ "Write",
60
+ "NotebookEdit",
61
+ "WebFetch",
62
+ "WebSearch",
63
+ "TodoWrite",
64
+ "Skill",
65
+ "ToolSearch",
66
+ "EnterWorktree",
67
+ "ExitWorktree",
68
+ "Monitor",
69
+ "TaskStop",
70
+ "SendMessage",
71
+ "Artifact",
72
+ ];
73
+ /**
74
+ * The 7 unconditional entries of the vendor's first filter — "removes these
75
+ * tools, even when listed in the `tools` field", with no qualifier attached.
76
+ * Listing one is a genuine dead reference, and the only tool verdict that
77
+ * still enters the grade.
78
+ */
79
+ exports.claudeCodeWithheldAgentTools = [
80
+ "AskUserQuestion",
81
+ "EndConversation",
82
+ "EnterPlanMode",
83
+ "ScheduleWakeup",
84
+ "TaskOutput",
85
+ "WaitForMcpServers",
86
+ "Workflow",
87
+ ];
88
+ /** Vendor conditions quoted once, so the term table below stays readable. */
89
+ const DEPTH_LIMIT = "only when the subagent is at the depth limit; in a fork the tool stays " +
90
+ "listed but returns an error instead of spawning";
91
+ const BACKGROUND = "from a background subagent (the default) — a foreground subagent or a fork " +
92
+ "keeps it";
93
+ const BACKGROUND_AND_MODEL = `${BACKGROUND}; also withheld from Opus 4.8 / Sonnet 5 / Fable 5 / Mythos 5 ` +
94
+ "and later unless CLAUDE_CODE_ENABLE_TODO_TOOLS=1";
95
+ /**
96
+ * Tools the platform removes only under a stated condition — the qualified
97
+ * entries of the first filter, plus the built-ins that survive in the
98
+ * foreground but not in the background. Declaring one is legitimate, so none
99
+ * of these is scored; the condition is reported and vigiles stops there.
100
+ */
101
+ exports.claudeCodeConditionalAgentToolNames = [
102
+ "Agent",
103
+ // The rename runs the OTHER WAY from what vigiles used to encode: `Task` is
104
+ // the old name and still works as an alias, `Agent` is the current one.
105
+ "Task",
106
+ "ExitPlanMode",
107
+ "ListAgents",
108
+ "LSP",
109
+ "ShareOnboardingGuide",
110
+ "CronCreate",
111
+ "CronDelete",
112
+ "CronList",
113
+ "TaskCreate",
114
+ "TaskGet",
115
+ "TaskList",
116
+ "TaskUpdate",
117
+ ];
118
+ /**
119
+ * The vendor's qualifier for each conditional tool.
120
+ *
121
+ * Typed as a `Record` over the name tuple ON PURPOSE: a conditional tool with no
122
+ * stated condition cannot be reported as conditional (the condition IS the
123
+ * report), so adding a name above without a line here is a `tsc` error rather
124
+ * than a runtime surprise. The equivalent runtime check in
125
+ * `vocabularyProjectionProblems` stays for adapters that build a vocabulary
126
+ * dynamically, but for this one the gap is unrepresentable.
127
+ */
128
+ const CONDITIONS = {
129
+ Agent: DEPTH_LIMIT,
130
+ Task: DEPTH_LIMIT,
131
+ ExitPlanMode: "unless the subagent's permissionMode is plan",
132
+ ListAgents: "from a background subagent; a foreground subagent inherits it only in " +
133
+ "sessions where cross-session messaging is enabled",
134
+ LSP: BACKGROUND,
135
+ ShareOnboardingGuide: BACKGROUND,
136
+ CronCreate: BACKGROUND,
137
+ CronDelete: BACKGROUND,
138
+ CronList: BACKGROUND,
139
+ TaskCreate: BACKGROUND_AND_MODEL,
140
+ TaskGet: BACKGROUND_AND_MODEL,
141
+ TaskList: BACKGROUND_AND_MODEL,
142
+ TaskUpdate: BACKGROUND_AND_MODEL,
143
+ };
144
+ /** Still-honoured deprecated spellings, pointing at the current name. */
145
+ const ALIASES = { Task: "Agent" };
146
+ /**
147
+ * The side-effecting subset of everything a subagent may declare (the
148
+ * complement is read-only). Two rules decided the additions here, both from the
149
+ * vendor's own tool descriptions rather than from the tool's name:
150
+ *
151
+ * - `Monitor` "writes a small script, runs it in the background" and can open
152
+ * a WebSocket — execution plus network, so it is side-effecting despite
153
+ * reading like an observer.
154
+ * - `LSP`, `ToolSearch`, `ListAgents`, `TaskGet`, `TaskList` and `CronList`
155
+ * only report state, so they stay read-only.
156
+ *
157
+ * This matters beyond tidiness: `classifyToolEffect` treats "in the built-in
158
+ * catalog and NOT here" as read-only, so a name added to the catalog without a
159
+ * decision here would silently be declared harmless — and a spawning tool
160
+ * misfiled as read-only would let a subagent pass the lethal-trifecta check it
161
+ * should fail.
162
+ */
163
+ exports.claudeCodeSideEffectingAgentTools = [
164
+ "Bash",
165
+ "PowerShell",
166
+ "Edit",
167
+ "Write",
168
+ "NotebookEdit",
169
+ "WebFetch",
170
+ "WebSearch",
171
+ "Skill",
172
+ "TodoWrite",
173
+ "Monitor",
174
+ "SendMessage",
175
+ "Artifact",
176
+ "EnterWorktree",
177
+ "ExitWorktree",
178
+ "TaskStop",
179
+ "Agent",
180
+ "Task",
181
+ "TaskCreate",
182
+ "TaskUpdate",
183
+ "CronCreate",
184
+ "CronDelete",
185
+ "ShareOnboardingGuide",
186
+ ];
187
+ /** The hook-event vocabulary — every documented event, all currently available. */
188
+ exports.claudeCodeHookEventVocabulary = {
189
+ kind: "claude-code hook event",
190
+ capturedFrom: `${CAPTURE} § hooks / Hook events`,
191
+ terms: exports.claudeCodeHookEventNames.map((name) => ({ name, status: "available" })),
192
+ };
193
+ /** The subagent-tool vocabulary — available / withheld / conditional. */
194
+ exports.claudeCodeSubagentToolVocabulary = {
195
+ kind: "claude-code subagent tool",
196
+ capturedFrom: `${CAPTURE} § sub-agents / Available tools`,
197
+ terms: [
198
+ ...exports.claudeCodeAvailableAgentTools.map((name) => ({ name, status: "available" })),
199
+ ...exports.claudeCodeWithheldAgentTools.map((name) => ({ name, status: "withheld" })),
200
+ ...exports.claudeCodeConditionalAgentToolNames.map((name) => ({
201
+ name,
202
+ status: "conditional",
203
+ condition: CONDITIONS[name],
204
+ ...(ALIASES[name] !== undefined ? { aliasOf: ALIASES[name] } : {}),
205
+ })),
206
+ ],
207
+ };
208
+ //# sourceMappingURL=vocabulary.js.map
@@ -36,6 +36,7 @@ exports.codexEvalAgentRunner = codexEvalAgentRunner;
36
36
  exports.codexEvalRunner = codexEvalRunner;
37
37
  const node_child_process_1 = require("node:child_process");
38
38
  const foreign_runner_js_1 = require("../../core/foreign-runner.js");
39
+ const eval_load_phase_js_1 = require("../../core/eval-load-phase.js");
39
40
  const node_fs_1 = require("node:fs");
40
41
  const node_path_1 = require("node:path");
41
42
  const str = (v) => (typeof v === "string" ? v : "");
@@ -229,6 +230,7 @@ function codexEvalRunner(args) {
229
230
  // The DOCUMENTED alternative to the default Claude runner, and the one the
230
231
  // untested-skill nudge now recommends — so it is exactly as able to be
231
232
  // collected by a stray `npx vitest run`, and exactly as expensive.
233
+ (0, eval_load_phase_js_1.refuseDuringEvalLoad)("driving `codex exec`");
232
234
  (0, foreign_runner_js_1.refuseUnderForeignRunner)("driving `codex exec`");
233
235
  const r = (0, node_child_process_1.spawnSync)("codex", [
234
236
  "exec",
@@ -49,7 +49,7 @@ const score_core_js_1 = require("./score-core.js");
49
49
  const W_UNTESTED = 3;
50
50
  /** The command that answers the firing question — named, not alluded to. */
51
51
  const MEASURE_FIRING_COMMAND = "run `npx vigiles audit` interactively to measure, or add a `*.eval.mjs` " +
52
- "(`paid_measureTriggerRate`, vigiles/eval)";
52
+ "(`export default defineEval({ measureTriggerRate: … })`, vigiles)";
53
53
  /** Resolve the terse "thing(s)" plural placeholder against a count:
54
54
  * n===1 drops the "(s)" ("1 tool"); otherwise it becomes "s" ("3 tools"). */
55
55
  function pluralizeLabel(n, label) {
package/dist/cli.js CHANGED
@@ -1706,7 +1706,7 @@ function formatTriggerNudge(triggerableSkills) {
1706
1706
  return "";
1707
1707
  const n = triggerableSkills;
1708
1708
  return (`ℹ Do your ${String(n)} skill${n === 1 ? "" : "s"} actually fire? The deterministic read can't tell — ` +
1709
- `run \`audit\` interactively to measure, or test with \`paid_measureTriggerRate\` (vigiles/eval).`);
1709
+ `run \`audit\` interactively to measure, or add a \`*.eval.mjs\` declaring \`measureTriggerRate\` (vigiles).`);
1710
1710
  }
1711
1711
  /** A terminal summary of the rule map: the CONFIDENT lane counts + the POSSIBLE
1712
1712
  * (review) and SKIPPED tiers, with the honest caveat that detection is a
@@ -4336,7 +4336,12 @@ async function handleRunScripts(kind, args, restArgs) {
4336
4336
  if (trialsFlag)
4337
4337
  env.VIGILES_TRIALS = trialsFlag.split("=")[1];
4338
4338
  console.log(`Running ${String(files.length)} ${kind} file(s):\n`);
4339
- const results = (0, run_scripts_js_1.runScripts)(files, cwd, env);
4339
+ // An eval file DESCRIBES its eval; `dist/eval-entry.js` is what imports the
4340
+ // description and runs what it declares. A harness script is still its own
4341
+ // program (it is free, so "import spends money" never applied to it).
4342
+ const results = (0, run_scripts_js_1.runScripts)(files, cwd, env, {
4343
+ ...(kind === "eval" ? { entry: (0, node_path_1.resolve)(__dirname, "eval-entry.js") } : {}),
4344
+ });
4340
4345
  // Write down WHAT the run exercised, so `lint`/`audit` can answer "tested?"
4341
4346
  // from execution instead of from a matching file name. Not a new verb and not
4342
4347
  // a flag: the run already happened, and this is the runner recording what it
@@ -827,7 +827,12 @@ function compileSkill(spec, options = {}) {
827
827
  * detection lives in the shared `verifyToolContract` detector (one-detector-no-
828
828
  * drift: compile + scan + the subagent-tool-contract lint rule call the same code). */
829
829
  function validateAgentTools(tools, dialect) {
830
- return (0, tool_contract_js_1.verifyToolContract)(tools, dialect).map((issue) => ({
830
+ // `authoringIssues` drops the `conditional` verdicts: `Agent`, `ExitPlanMode`
831
+ // and the foreground-only built-ins are REAL tools, legitimate to declare, and
832
+ // erroring on them is what made `tools: Agent, Read, Bash` — a worked example
833
+ // in the vendor's own docs — fail to compile. Everything else stays an error,
834
+ // because authoring your own spec is a closed world.
835
+ return (0, tool_contract_js_1.authoringIssues)((0, tool_contract_js_1.verifyToolContract)(tools, dialect)).map((issue) => ({
831
836
  type: "unknown-tool",
832
837
  message: issue.message,
833
838
  }));
@@ -22,6 +22,7 @@
22
22
  * Which SKILL.md frontmatter keys a harness understands — see
23
23
  * `HarnessDialect.skillFrontmatter`.
24
24
  */
25
+ import type { HarnessVocabulary } from "./vocabulary.js";
25
26
  export type SkillFrontmatterProfile = "claude-code" | "minimal";
26
27
  export interface HarnessDialect {
27
28
  /** Stable identifier, e.g. "claude-code". */
@@ -83,5 +84,31 @@ export interface HarnessDialect {
83
84
  * Optional (additive, non-breaking) — absent ⇒ no tool is known-side-effecting.
84
85
  */
85
86
  readonly sideEffectingTools?: readonly string[];
87
+ /**
88
+ * The hook-event catalog as a {@link HarnessVocabulary} — a status and a
89
+ * recorded vendor capture per term, rather than bare membership in
90
+ * `hookEvents`. When present it is what `verifyHookEvents` classifies against,
91
+ * so a name the catalog doesn't hold produces an `unrecognised` ADVISORY
92
+ * (naming vigiles's capture as the possibly-stale party) instead of the old
93
+ * behaviour, where an unknown name drew an accusation or silence depending on
94
+ * its edit distance to the list.
95
+ *
96
+ * Optional (additive, non-breaking). Absent ⇒ one is synthesised from
97
+ * `hookEvents` via `vocabularyFromLists`, so a legacy adapter keeps working
98
+ * and its unknowns become advisories rather than silence.
99
+ */
100
+ readonly hookEventVocabulary?: HarnessVocabulary;
101
+ /**
102
+ * The subagent-tool catalog as a {@link HarnessVocabulary}. Same contract as
103
+ * `hookEventVocabulary`; absent ⇒ synthesised from `builtinAgentTools`
104
+ * (available) + `neverAvailableTools` (withheld).
105
+ *
106
+ * The third status is why this exists: the vendor removes `Agent` only at the
107
+ * spawn depth limit and `ExitPlanMode` only outside plan mode, and removes
108
+ * most built-ins from a background subagent but not a foreground one — so
109
+ * "available to a subagent" is not a property of the name, and a two-way
110
+ * split had to encode one of those conditions as an unconditional fact.
111
+ */
112
+ readonly subagentToolVocabulary?: HarnessVocabulary;
86
113
  }
87
114
  //# sourceMappingURL=dialect.d.ts.map
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The paid tier is closed while an eval file is being LOADED.
3
+ *
4
+ * ## The failure this removes
5
+ *
6
+ * An eval file used to do its work at the top level:
7
+ *
8
+ * const report = await measureTriggerRate({ … }); // ← module body
9
+ *
10
+ * In ESM, `import` IS execution, so *reading* such a file spends real money.
11
+ * Measured 2026-08-12 (and reproduced 2026-08-18 with an inert stand-in that
12
+ * writes a marker file instead of spawning): `node -e 'import("./x.eval.mjs")'`
13
+ * ran the whole body and the one guard that exists — `refuseUnderForeignRunner`
14
+ * — stayed SILENT, because under `node -e` there is no `process.argv[1]` at all:
15
+ *
16
+ * node -e 'import(x)' argv[1] = undefined → foreignRunner(…) = null
17
+ *
18
+ * That guard is not broken; it answers a different question ("does somebody
19
+ * else's test runner own this process?"). No process fact distinguishes
20
+ * `node -e 'import(x)'` from a legitimate runner doing `import(x)`, so no
21
+ * argv-shaped guard can close this door without also refusing the correct
22
+ * invocation — the exact failure mode that got `process.env.VITEST` rejected in
23
+ * `foreign-runner.ts`.
24
+ *
25
+ * ## What closes it instead
26
+ *
27
+ * The SHAPE changed: an eval file now DESCRIBES its eval (`defineEval`) and
28
+ * `vigiles eval` runs it. A description cannot spend, so for a conforming file
29
+ * the door is shut by construction and this module is not needed.
30
+ *
31
+ * This module covers the file that is only HALF migrated — a `defineEval` export
32
+ * plus a leftover top-level `measure(…)`. During the import that the eval runner
33
+ * performs, the paid tier refuses. So the usual way anyone runs an eval turns a
34
+ * silent bill into a loud error that names the fix, instead of paying it.
35
+ *
36
+ * ⚠️ HONEST BOUNDARY, stated rather than implied. This is scoped to the runner's
37
+ * own import. `node -e 'import("./half-migrated.eval.mjs")'` still spends,
38
+ * because that file still contains the original defect — the redesign made the
39
+ * defect *avoidable and detectable*, it cannot retroactively fix a file that did
40
+ * not adopt it. The free syntax check is `node --check <file>`, which never
41
+ * executes anything.
42
+ *
43
+ * ## Why the default is OPEN
44
+ *
45
+ * `paid_runEval` and friends are public API; somebody's own script calling them
46
+ * at top level is their business. A gate that defaulted to closed would refuse
47
+ * every correct direct call — again, "a guard that fires on correct input is a
48
+ * guard people delete". So the window is opened by nobody and closed by exactly
49
+ * one caller: `eval-entry.ts`, around its `import()`.
50
+ *
51
+ * Imports NOTHING, so every spawn door (including the adapters) can call it
52
+ * without a cycle — the same reason `foreign-runner.ts` is a leaf.
53
+ */
54
+ /**
55
+ * Open the no-spend window. Called by `eval-entry.ts` immediately before it
56
+ * imports an eval file, and paired with {@link endEvalLoad} in a `finally`.
57
+ */
58
+ export declare function beginEvalLoad(): void;
59
+ /** Close the no-spend window: the description is loaded, the runner may spend. */
60
+ export declare function endEvalLoad(): void;
61
+ /** Whether the no-spend window is open (exported for tests + the entry). */
62
+ export declare function inEvalLoad(): boolean;
63
+ /**
64
+ * The refusal message. Separate from the check so a test can assert the WORDS:
65
+ * this fires on a file whose author has not seen the new shape yet, so it has to
66
+ * teach it, not just stop.
67
+ */
68
+ export declare function evalLoadRefusal(what: string): string;
69
+ /**
70
+ * Refuse to spend model budget while an eval file is being imported.
71
+ *
72
+ * 🔴 CALL THIS AT EVERY REAL-MODEL SPAWN, beside `refuseUnderForeignRunner`, and
73
+ * OUTSIDE any `try` that swallows — `judge` and `deriveAttackReal` both wrap
74
+ * their spawn in `try { … } catch { return fallback }`, so a refusal thrown
75
+ * inside would be downgraded to a score of 0 instead of stopping the run.
76
+ */
77
+ export declare function refuseDuringEvalLoad(what: string): void;
78
+ //# sourceMappingURL=eval-load-phase.d.ts.map
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /**
3
+ * The paid tier is closed while an eval file is being LOADED.
4
+ *
5
+ * ## The failure this removes
6
+ *
7
+ * An eval file used to do its work at the top level:
8
+ *
9
+ * const report = await measureTriggerRate({ … }); // ← module body
10
+ *
11
+ * In ESM, `import` IS execution, so *reading* such a file spends real money.
12
+ * Measured 2026-08-12 (and reproduced 2026-08-18 with an inert stand-in that
13
+ * writes a marker file instead of spawning): `node -e 'import("./x.eval.mjs")'`
14
+ * ran the whole body and the one guard that exists — `refuseUnderForeignRunner`
15
+ * — stayed SILENT, because under `node -e` there is no `process.argv[1]` at all:
16
+ *
17
+ * node -e 'import(x)' argv[1] = undefined → foreignRunner(…) = null
18
+ *
19
+ * That guard is not broken; it answers a different question ("does somebody
20
+ * else's test runner own this process?"). No process fact distinguishes
21
+ * `node -e 'import(x)'` from a legitimate runner doing `import(x)`, so no
22
+ * argv-shaped guard can close this door without also refusing the correct
23
+ * invocation — the exact failure mode that got `process.env.VITEST` rejected in
24
+ * `foreign-runner.ts`.
25
+ *
26
+ * ## What closes it instead
27
+ *
28
+ * The SHAPE changed: an eval file now DESCRIBES its eval (`defineEval`) and
29
+ * `vigiles eval` runs it. A description cannot spend, so for a conforming file
30
+ * the door is shut by construction and this module is not needed.
31
+ *
32
+ * This module covers the file that is only HALF migrated — a `defineEval` export
33
+ * plus a leftover top-level `measure(…)`. During the import that the eval runner
34
+ * performs, the paid tier refuses. So the usual way anyone runs an eval turns a
35
+ * silent bill into a loud error that names the fix, instead of paying it.
36
+ *
37
+ * ⚠️ HONEST BOUNDARY, stated rather than implied. This is scoped to the runner's
38
+ * own import. `node -e 'import("./half-migrated.eval.mjs")'` still spends,
39
+ * because that file still contains the original defect — the redesign made the
40
+ * defect *avoidable and detectable*, it cannot retroactively fix a file that did
41
+ * not adopt it. The free syntax check is `node --check <file>`, which never
42
+ * executes anything.
43
+ *
44
+ * ## Why the default is OPEN
45
+ *
46
+ * `paid_runEval` and friends are public API; somebody's own script calling them
47
+ * at top level is their business. A gate that defaulted to closed would refuse
48
+ * every correct direct call — again, "a guard that fires on correct input is a
49
+ * guard people delete". So the window is opened by nobody and closed by exactly
50
+ * one caller: `eval-entry.ts`, around its `import()`.
51
+ *
52
+ * Imports NOTHING, so every spawn door (including the adapters) can call it
53
+ * without a cycle — the same reason `foreign-runner.ts` is a leaf.
54
+ */
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.beginEvalLoad = beginEvalLoad;
57
+ exports.endEvalLoad = endEvalLoad;
58
+ exports.inEvalLoad = inEvalLoad;
59
+ exports.evalLoadRefusal = evalLoadRefusal;
60
+ exports.refuseDuringEvalLoad = refuseDuringEvalLoad;
61
+ /** Whether an eval module is being imported by the eval runner right now. */
62
+ let loading = false;
63
+ /**
64
+ * Open the no-spend window. Called by `eval-entry.ts` immediately before it
65
+ * imports an eval file, and paired with {@link endEvalLoad} in a `finally`.
66
+ */
67
+ function beginEvalLoad() {
68
+ loading = true;
69
+ }
70
+ /** Close the no-spend window: the description is loaded, the runner may spend. */
71
+ function endEvalLoad() {
72
+ loading = false;
73
+ }
74
+ /** Whether the no-spend window is open (exported for tests + the entry). */
75
+ function inEvalLoad() {
76
+ return loading;
77
+ }
78
+ /**
79
+ * The refusal message. Separate from the check so a test can assert the WORDS:
80
+ * this fires on a file whose author has not seen the new shape yet, so it has to
81
+ * teach it, not just stop.
82
+ */
83
+ function evalLoadRefusal(what) {
84
+ return (`vigiles refused to spawn a model: ${what} while an eval file was being IMPORTED.\n` +
85
+ ` An eval file DESCRIBES its eval; it must not run one at the top level — importing such a\n` +
86
+ ` file spends real money (that is the defect this shape removes).\n` +
87
+ ` Move the runner call into the description, keyed by the runner's own name:\n` +
88
+ ` import { defineEval } from "vigiles";\n` +
89
+ ` export default defineEval({ measureTriggerRate: { …the spec you passed… } });\n` +
90
+ ` and read the report in \`assert(report)\`. See docs/harness-testing.md § Eval files.`);
91
+ }
92
+ /**
93
+ * Refuse to spend model budget while an eval file is being imported.
94
+ *
95
+ * 🔴 CALL THIS AT EVERY REAL-MODEL SPAWN, beside `refuseUnderForeignRunner`, and
96
+ * OUTSIDE any `try` that swallows — `judge` and `deriveAttackReal` both wrap
97
+ * their spawn in `try { … } catch { return fallback }`, so a refusal thrown
98
+ * inside would be downgraded to a score of 0 instead of stopping the run.
99
+ */
100
+ function refuseDuringEvalLoad(what) {
101
+ if (loading)
102
+ throw new Error(evalLoadRefusal(what));
103
+ }
104
+ //# sourceMappingURL=eval-load-phase.js.map
@@ -4,31 +4,48 @@
4
4
  * (`PreToolUse`, `SessionStart`, …); a TYPO (`PreToolUSe`) means the hook
5
5
  * silently never fires — a dead registration no generic JSON linter catches.
6
6
  *
7
- * Like the tool catalog, the event set is NOT closed in practice: frameworks
8
- * extend it (TheBushidoCollective/han ships a custom runtime with `TeammateIdle`,
9
- * `WorktreeRemove`, … in its own `hooks.json`). So the audit path (scan/lint) is
10
- * HIGH-PRECISION `confidentHookEventIssues` keeps only a close typo
11
- * (a did-you-mean within edit distance 2), never a bare unrecognized event that
12
- * may be a custom/future one. ONE detector (one-detector-no-drift): scan + the
13
- * `hook-events` lint rule call the same code. Dialect injected (core adapter).
7
+ * The event set is NOT closed in practice: the vendor keeps adding events, and
8
+ * frameworks ship custom runtimes with their own (TheBushidoCollective/han fires
9
+ * `TeammateIdle`, `WorktreeRemove`, … from its own `hooks.json` both of which
10
+ * have since become real Claude Code events). This check used to handle that by
11
+ * reporting an unknown event ONLY when it sat within edit distance 2 of a known
12
+ * one. That is not a confidence signal, and it failed both ways at once:
13
+ * `Setup`, a documented event, was accused of never firing and told to become
14
+ * `Stop`; twenty-one other documented events drew nothing, because they happened
15
+ * to be further than two characters from anything in a nine-name list.
16
+ *
17
+ * Now every name is CLASSIFIED against the dialect's vocabulary
18
+ * (`core/vocabulary.ts`) and every verdict is reported — with the severity
19
+ * coming from the verdict rather than from the caller. An event vigiles doesn't
20
+ * hold is an `advisory` that names vigiles's own capture as the thing that may
21
+ * be stale; it is surfaced and never scored, so a newer or custom event cannot
22
+ * cost anyone a grade. ONE detector (one-detector-no-drift): scan + the
23
+ * `hook-events` lint rule + compiled-hook `on:` validation call the same code.
24
+ * Dialect injected (core ⊄ adapter).
14
25
  */
15
26
  import type { HarnessDialect } from "./dialect.js";
27
+ import { type HarnessVocabulary, type IssueSeverity, type TermVerdict } from "./vocabulary.js";
16
28
  export interface HookEventIssue {
17
29
  readonly event: string;
18
- /** Closest known event (did-you-mean), or null. */
30
+ /** Which vocabulary verdict produced this — the input to every policy. */
31
+ readonly verdict: TermVerdict["kind"];
32
+ /** Closest known event (did-you-mean), or null. Message decoration only. */
19
33
  readonly suggestion: string | null;
34
+ /** `"scored"` counts toward the grade; `"advisory"` never does. */
35
+ readonly severity: IssueSeverity;
20
36
  readonly message: string;
21
37
  }
22
38
  /**
23
- * The HIGH-CONFIDENCE subset (what scan / lint act on): only an unrecognized
24
- * event that's a close typo of a real one. A bare unknown (no near match) is
25
- * likely a framework/custom event, not a defect — never flagged when auditing.
39
+ * The event vocabulary this dialect verifies against its declared one, else a
40
+ * synthesised one built from the flat `hookEvents` list so an adapter that
41
+ * predates vocabularies keeps working.
26
42
  */
27
- export declare function confidentHookEventIssues(issues: readonly HookEventIssue[]): HookEventIssue[];
43
+ export declare function hookEventVocabulary(dialect: HarnessDialect): HarnessVocabulary;
28
44
  /**
29
- * Verify hook-event names against the dialect catalog. Returns one issue per
30
- * unrecognized event. Like the tool-contract check, a suggestion (edit distance
31
- * 2) is the confidence signal that an unknown is really a typo of a real event.
45
+ * Verify hook-event names against the dialect vocabulary. Returns one issue per
46
+ * name that isn't plainly available, each already carrying its severity — see
47
+ * {@link scoredIssues} / {@link advisoryIssues} to split them.
32
48
  */
33
49
  export declare function verifyHookEvents(events: readonly string[], dialect: HarnessDialect): HookEventIssue[];
50
+ export { scoredIssues, advisoryIssues, authoringIssues } from "./vocabulary.js";
34
51
  //# sourceMappingURL=hook-events.d.ts.map
@@ -1,48 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.confidentHookEventIssues = confidentHookEventIssues;
3
+ exports.authoringIssues = exports.advisoryIssues = exports.scoredIssues = void 0;
4
+ exports.hookEventVocabulary = hookEventVocabulary;
4
5
  exports.verifyHookEvents = verifyHookEvents;
5
- const edit_distance_js_1 = require("./edit-distance.js");
6
- /** Closest known hook event by edit distance (≤ 2) — a confidence signal. */
7
- function closestEvent(event, dialect) {
8
- let best = null;
9
- let bestDistance = Infinity;
10
- for (const known of dialect.hookEvents) {
11
- const d = (0, edit_distance_js_1.editDistance)(event.toLowerCase(), known.toLowerCase());
12
- if (d < bestDistance) {
13
- bestDistance = d;
14
- best = known;
15
- }
16
- }
17
- return bestDistance <= 2 ? best : null;
18
- }
6
+ const vocabulary_js_1 = require("./vocabulary.js");
19
7
  /**
20
- * The HIGH-CONFIDENCE subset (what scan / lint act on): only an unrecognized
21
- * event that's a close typo of a real one. A bare unknown (no near match) is
22
- * likely a framework/custom event, not a defect — never flagged when auditing.
8
+ * The event vocabulary this dialect verifies against its declared one, else a
9
+ * synthesised one built from the flat `hookEvents` list so an adapter that
10
+ * predates vocabularies keeps working.
23
11
  */
24
- function confidentHookEventIssues(issues) {
25
- return issues.filter((i) => i.suggestion !== null);
12
+ function hookEventVocabulary(dialect) {
13
+ return (dialect.hookEventVocabulary ??
14
+ (0, vocabulary_js_1.vocabularyFromLists)(`${dialect.name} hook event`, `${dialect.name} adapter (no recorded capture)`, dialect.hookEvents));
26
15
  }
27
16
  /**
28
- * Verify hook-event names against the dialect catalog. Returns one issue per
29
- * unrecognized event. Like the tool-contract check, a suggestion (edit distance
30
- * 2) is the confidence signal that an unknown is really a typo of a real event.
17
+ * Verify hook-event names against the dialect vocabulary. Returns one issue per
18
+ * name that isn't plainly available, each already carrying its severity — see
19
+ * {@link scoredIssues} / {@link advisoryIssues} to split them.
31
20
  */
32
21
  function verifyHookEvents(events, dialect) {
33
- const known = new Set(dialect.hookEvents);
22
+ const vocab = hookEventVocabulary(dialect);
34
23
  const issues = [];
35
24
  for (const event of events) {
36
- if (known.has(event))
25
+ const issue = (0, vocabulary_js_1.termIssue)(vocab, (0, vocabulary_js_1.classify)(vocab, event), "Hook event", "a hook here never fires");
26
+ if (issue === null)
37
27
  continue;
38
- const near = closestEvent(event, dialect);
39
- const hint = near ? ` Did you mean "${near}"?` : "";
40
28
  issues.push({
41
29
  event,
42
- suggestion: near,
43
- message: `Unknown hook event "${event}" — a hook here never fires. Valid events: ${dialect.hookEvents.join(", ")}.${hint}`,
30
+ verdict: issue.verdict,
31
+ suggestion: issue.suggestion,
32
+ severity: issue.severity,
33
+ message: issue.message,
44
34
  });
45
35
  }
46
36
  return issues;
47
37
  }
38
+ var vocabulary_js_2 = require("./vocabulary.js");
39
+ Object.defineProperty(exports, "scoredIssues", { enumerable: true, get: function () { return vocabulary_js_2.scoredIssues; } });
40
+ Object.defineProperty(exports, "advisoryIssues", { enumerable: true, get: function () { return vocabulary_js_2.advisoryIssues; } });
41
+ Object.defineProperty(exports, "authoringIssues", { enumerable: true, get: function () { return vocabulary_js_2.authoringIssues; } });
48
42
  //# sourceMappingURL=hook-events.js.map