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
@@ -679,11 +679,19 @@ function compileHookProgram(source, hook, opts = {}) {
679
679
  `regex (that is why "Edit|Write" works), so it must parse as one.`);
680
680
  }
681
681
  }
682
- // A hook registered under an event the harness never fires is dead — reject it.
682
+ // A hook registered under an event the harness never fires is dead — reject
683
+ // it. AUTHORING is a closed world (you are writing this hook now, against the
684
+ // vigiles you have), so an unrecognised event is still an error — the typo
685
+ // guarantee this exists for. What changed on 2026-08-17 is the catalog it
686
+ // asks: this used to throw on `Setup`, `PostCompact`, `ConfigChange` and 19
687
+ // other REAL events, because vigiles held 9 of the vendor's 31. The fix is the
688
+ // right vocabulary, not a weaker check. A genuinely newer event still fails
689
+ // here, and now says so — the message names vigiles's capture as the thing
690
+ // that may be stale, instead of asserting the event does not exist.
683
691
  if (opts.dialect) {
684
- const issues = (0, hook_events_js_1.verifyHookEvents)([on], opts.dialect);
685
- if (issues.length > 0) {
686
- throw new HookCompileError(issues[0].message);
692
+ const fatal = (0, hook_events_js_1.authoringIssues)((0, hook_events_js_1.verifyHookEvents)([on], opts.dialect));
693
+ if (fatal.length > 0) {
694
+ throw new HookCompileError(fatal[0].message);
687
695
  }
688
696
  }
689
697
  // A `needs` entry that isn't a built-in provider never resolves — reject it
@@ -85,7 +85,7 @@ exports.RULE_META = {
85
85
  surface: ["subagent"],
86
86
  defaultSeverity: "warn",
87
87
  summary: "A subagent's tools: are all real (no never-available / typo).",
88
- detector: "confidentToolIssues",
88
+ detector: "verifyToolContract / scoredIssues",
89
89
  upstreamPrevention: "typed agent() vocabulary + compileAgent — an unknown tool is a tsc/compile error",
90
90
  },
91
91
  "disallowed-tools-contract": {
@@ -113,7 +113,7 @@ exports.RULE_META = {
113
113
  surface: ["hook"],
114
114
  defaultSeverity: "warn",
115
115
  summary: "A hook's event name is one the harness defines (it can fire).",
116
- detector: "confidentHookEventIssues",
116
+ detector: "verifyHookEvents / scoredIssues",
117
117
  upstreamPrevention: "compiled hook on: is dialect-validated at compile",
118
118
  },
119
119
  "hook-script-exists": {
@@ -2,8 +2,8 @@
2
2
  * Tool-contract verification — the cross-referencing moat ("valid is not true")
3
3
  * applied to a subagent's declared `tools:` rail. A subagent may only run
4
4
  * built-in tools from the harness dialect's catalog or an MCP tool; anything else
5
- * is a typo or a nonexistent / never-available tool — a guaranteed-dead reference
6
- * a compiler catches, not a runtime surprise.
5
+ * is a typo or a nonexistent tool — a guaranteed-dead reference a compiler
6
+ * catches, not a runtime surprise.
7
7
  *
8
8
  * ONE pure detector (`one-detector-no-drift`), reused by THREE callers so they
9
9
  * can't disagree: `compileAgent` (spec authoring), `scan` (read-only audit of a
@@ -11,51 +11,89 @@
11
11
  * commit gate). The dialect is injected (core ⊄ adapter) — the composition root
12
12
  * passes `claudeCodeDialect` / `codexDialect`.
13
13
  *
14
- * Scope note: this validates a SUBAGENT contract against the SUBAGENT catalog
15
- * (`builtinAgentTools` / `neverAvailableTools`). A skill's `allowed-tools` is a
16
- * DIFFERENT namespace (skills legitimately use `AskUserQuestion`, `TaskCreate`,
17
- * … which are never-available to a subagent), so it is deliberately NOT validated
18
- * here — doing so against the agent catalog would be a false-positive factory.
14
+ * WHAT CHANGED, 2026-08-17. This used to split names two ways — in
15
+ * `builtinAgentTools` (fine) or in `neverAvailableTools` (dead) — and decide
16
+ * what to say about a name in neither by its edit distance to the first list.
17
+ * Two failures came out of that shape:
18
+ *
19
+ * - `Agent` was in the DENYLIST while its own deprecated alias `Task` was in the
20
+ * catalog, so vigiles rejected the platform's current name, accepted the old
21
+ * one, and told orchestrator subagents to remove the tool they exist to use.
22
+ * Nothing could notice, because the two lists were never compared.
23
+ * - Real tools vigiles didn't know (`EndConversation`, `TaskOutput`,
24
+ * `Workflow`) and outright invented ones passed in silence, while typos of
25
+ * known names were caught — so the more wrong a name was, the likelier it
26
+ * went unreported.
27
+ *
28
+ * Names are now CLASSIFIED against the dialect's vocabulary
29
+ * (`core/vocabulary.ts`), which has a third status for what the two-way split
30
+ * could not express: the vendor removes `Agent` only at the spawn depth limit,
31
+ * `ExitPlanMode` only outside plan mode, and most built-ins only from a
32
+ * background subagent. Those are `conditional` — reported as a note with the
33
+ * condition quoted, never as "remove it". Severity travels on the issue, so
34
+ * `scan`, `lint` and `compileAgent` cannot drift apart on which issues count.
35
+ *
36
+ * Scope note: this validates a SUBAGENT contract against the SUBAGENT catalog. A
37
+ * skill's `allowed-tools` is a DIFFERENT namespace (skills legitimately use
38
+ * `AskUserQuestion`, `TaskCreate`, … which a subagent doesn't get), so it is
39
+ * deliberately NOT validated here — doing so against the agent catalog would be
40
+ * a false-positive factory.
19
41
  */
20
42
  import type { HarnessDialect } from "./dialect.js";
21
- export type ToolIssueKind = "never-available" | "unknown";
43
+ import { type HarnessVocabulary, type IssueSeverity, type TermVerdict } from "./vocabulary.js";
44
+ export type ToolIssueKind =
45
+ /** The platform removes it unconditionally — a real, scored defect. */
46
+ "never-available"
47
+ /** Not in vigiles's catalog — advisory; may be newer than our capture. */
48
+ | "unknown"
49
+ /** Real, but removed under a condition vigiles can't see — advisory. */
50
+ | "conditional";
22
51
  export interface ToolIssue {
23
52
  readonly tool: string;
24
53
  readonly kind: ToolIssueKind;
25
- /** Closest known built-in tool (did-you-mean), or null. */
54
+ /** Which vocabulary verdict produced this — the input to every policy. */
55
+ readonly verdict: TermVerdict["kind"];
56
+ /** Closest known built-in tool (did-you-mean), or null. Message only. */
26
57
  readonly suggestion: string | null;
58
+ /**
59
+ * The vendor condition — present ONLY for a `conditional` verdict. Carried so
60
+ * a report can group the tools sharing one condition rather than repeat the
61
+ * same sentence per tool.
62
+ */
63
+ readonly condition?: string;
64
+ /** `"scored"` counts toward the grade; `"advisory"` never does. */
65
+ readonly severity: IssueSeverity;
27
66
  /** A ready-to-show, actionable message. */
28
67
  readonly message: string;
29
68
  }
30
69
  /**
31
- * Closest known built-in tool by edit distance (≤ 2), for a "did you mean" hint.
32
- * The ≤ 2 bound is deliberately tight: a suggestion is a CONFIDENCE signal (this
33
- * `unknown` is really a typo of a real tool), and a loose bound mis-suggests —
34
- * `TaskGet → Task?` (distance 3) is a real tool set, not a typo of `Task`.
70
+ * The tool vocabulary this dialect verifies against — its declared one, else a
71
+ * synthesised one from the flat lists so a legacy adapter keeps working.
35
72
  */
36
- export declare function closestTool(tool: string, dialect: HarnessDialect): string | null;
73
+ export declare function subagentToolVocabulary(dialect: HarnessDialect): HarnessVocabulary;
37
74
  /**
38
- * The HIGH-CONFIDENCE subset of a contract's issues — the ones safe to flag when
39
- * AUDITING a third-party plugin (scan / lint), where the catalog can't know
40
- * every tool (plugin-/MCP-provided, newer platform tools). Only two are confident:
41
- * a `never-available` tool (a curated denylist) and an `unknown` with a close
42
- * typo suggestion (`Edt → Edit`). A bare `unknown` with no near match is NOT
43
- * flagged here — it is more likely a tool vigiles doesn't know than a defect
44
- * (sweeping real plugins surfaced a 280★ plugin using `TaskCreate/TaskGet/…`
45
- * consistently; flagging those would be crying wolf). `compileAgent` stays strict
46
- * — when you author your OWN spec, every unrecognized tool is worth an error.
75
+ * Closest known built-in tool by edit distance (≤ 2), for a "did you mean" hint.
76
+ * A MESSAGE DECORATION, never a gate: whether to report is already settled by
77
+ * the verdict before this is called. The ≤ 2 bound stays tight because a loose
78
+ * bound mis-suggests — `TaskGet → Task?` is a different real tool, not a typo.
47
79
  */
48
- export declare function confidentToolIssues(issues: readonly ToolIssue[]): ToolIssue[];
80
+ export declare function closestTool(tool: string, dialect: HarnessDialect): string | null;
49
81
  /**
50
82
  * Verify a subagent's `disallowedTools:` BLOCK-list — the mirror of the allow
51
83
  * contract. A typo here is dangerous: you meant to block `Bash` but wrote `Bsh`,
52
84
  * so nothing is blocked and the dangerous tool stays available, silently. Returns
53
- * one {@link ToolIssue} per entry that's a CLOSE TYPO of a real built-in (the
54
- * high-confidence signal). Deliberately NOT flagged: a real built-in (it IS being
55
- * blocked — correct), a never-available tool (harmless to block), an MCP tool (a
56
- * legitimate plugin tool to block), or a bare unknown with no near match (likely
57
- * a plugin/MCP tool, not a typo — the cry-wolf trap). The block-list inverts the
58
- * allow check: never-available is fine to list, a typo is the actual defect.
85
+ * one {@link ToolIssue} per entry that's a CLOSE TYPO of a real tool.
86
+ * Deliberately NOT flagged: any name the vocabulary knows (blocking it is the
87
+ * point — including a withheld one, which is merely redundant), an MCP tool (a
88
+ * legitimate plugin tool to block), or a bare unknown with no near match.
89
+ *
90
+ * This is the ONE place a near match still gates a finding, and it is not the
91
+ * confidence proxy the allow-side check was rightly stripped of. On a block-list
92
+ * the risk inverts: an entry naming nothing is harmless UNLESS you meant a real
93
+ * tool and mistyped it, and "meant a real tool" is precisely what a one-character
94
+ * distance evidences. `disallowedTools: [Zzzz]` blocks nothing and nobody
95
+ * intended otherwise; `disallowedTools: [Bsh]` leaves `Bash` wide open. So the
96
+ * distance here is the actual semantic signal, not a stand-in for one.
59
97
  */
60
98
  export declare function disallowedToolIssues(tools: readonly string[], dialect: HarnessDialect): ToolIssue[];
61
99
  /**
@@ -65,4 +103,5 @@ export declare function disallowedToolIssues(tools: readonly string[], dialect:
65
103
  * is stripped to its base tool before checking.
66
104
  */
67
105
  export declare function verifyToolContract(tools: readonly string[], dialect: HarnessDialect): ToolIssue[];
106
+ export { scoredIssues, advisoryIssues, authoringIssues } from "./vocabulary.js";
68
107
  //# sourceMappingURL=tool-contract.d.ts.map
@@ -1,73 +1,77 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authoringIssues = exports.advisoryIssues = exports.scoredIssues = void 0;
4
+ exports.subagentToolVocabulary = subagentToolVocabulary;
3
5
  exports.closestTool = closestTool;
4
- exports.confidentToolIssues = confidentToolIssues;
5
6
  exports.disallowedToolIssues = disallowedToolIssues;
6
7
  exports.verifyToolContract = verifyToolContract;
7
- const edit_distance_js_1 = require("./edit-distance.js");
8
+ const vocabulary_js_1 = require("./vocabulary.js");
8
9
  /**
9
- * Closest known built-in tool by edit distance (≤ 2), for a "did you mean" hint.
10
- * The ≤ 2 bound is deliberately tight: a suggestion is a CONFIDENCE signal (this
11
- * `unknown` is really a typo of a real tool), and a loose bound mis-suggests —
12
- * `TaskGet → Task?` (distance 3) is a real tool set, not a typo of `Task`.
10
+ * The wire-shape `kind` each verdict maps to. `never-available` and `unknown`
11
+ * predate the vocabulary and keep their meaning for existing consumers;
12
+ * `conditional` is the new one the two-way split could not express.
13
13
  */
14
- function closestTool(tool, dialect) {
15
- let best = null;
16
- let bestDistance = Infinity;
17
- for (const known of dialect.builtinAgentTools) {
18
- const d = (0, edit_distance_js_1.editDistance)(tool.toLowerCase(), known.toLowerCase());
19
- if (d < bestDistance) {
20
- bestDistance = d;
21
- best = known;
22
- }
23
- }
24
- return bestDistance <= 2 ? best : null;
14
+ const TOOL_ISSUE_KIND = {
15
+ withheld: "never-available",
16
+ conditional: "conditional",
17
+ unrecognised: "unknown",
18
+ // `available` never reaches here — termIssue returns null for it.
19
+ available: "unknown",
20
+ };
21
+ /**
22
+ * The tool vocabulary this dialect verifies against — its declared one, else a
23
+ * synthesised one from the flat lists so a legacy adapter keeps working.
24
+ */
25
+ function subagentToolVocabulary(dialect) {
26
+ return (dialect.subagentToolVocabulary ??
27
+ (0, vocabulary_js_1.vocabularyFromLists)(`${dialect.name} subagent tool`, `${dialect.name} adapter (no recorded capture)`, dialect.builtinAgentTools, dialect.neverAvailableTools));
25
28
  }
26
29
  /**
27
- * The HIGH-CONFIDENCE subset of a contract's issues — the ones safe to flag when
28
- * AUDITING a third-party plugin (scan / lint), where the catalog can't know
29
- * every tool (plugin-/MCP-provided, newer platform tools). Only two are confident:
30
- * a `never-available` tool (a curated denylist) and an `unknown` with a close
31
- * typo suggestion (`Edt → Edit`). A bare `unknown` with no near match is NOT
32
- * flagged here — it is more likely a tool vigiles doesn't know than a defect
33
- * (sweeping real plugins surfaced a 280★ plugin using `TaskCreate/TaskGet/…`
34
- * consistently; flagging those would be crying wolf). `compileAgent` stays strict
35
- * — when you author your OWN spec, every unrecognized tool is worth an error.
30
+ * Closest known built-in tool by edit distance (≤ 2), for a "did you mean" hint.
31
+ * A MESSAGE DECORATION, never a gate: whether to report is already settled by
32
+ * the verdict before this is called. The ≤ 2 bound stays tight because a loose
33
+ * bound mis-suggests — `TaskGet → Task?` is a different real tool, not a typo.
36
34
  */
37
- function confidentToolIssues(issues) {
38
- return issues.filter((i) => i.kind === "never-available" || i.suggestion !== null);
35
+ function closestTool(tool, dialect) {
36
+ return (0, vocabulary_js_1.suggest)(subagentToolVocabulary(dialect), tool);
39
37
  }
40
38
  /**
41
39
  * Verify a subagent's `disallowedTools:` BLOCK-list — the mirror of the allow
42
40
  * contract. A typo here is dangerous: you meant to block `Bash` but wrote `Bsh`,
43
41
  * so nothing is blocked and the dangerous tool stays available, silently. Returns
44
- * one {@link ToolIssue} per entry that's a CLOSE TYPO of a real built-in (the
45
- * high-confidence signal). Deliberately NOT flagged: a real built-in (it IS being
46
- * blocked — correct), a never-available tool (harmless to block), an MCP tool (a
47
- * legitimate plugin tool to block), or a bare unknown with no near match (likely
48
- * a plugin/MCP tool, not a typo — the cry-wolf trap). The block-list inverts the
49
- * allow check: never-available is fine to list, a typo is the actual defect.
42
+ * one {@link ToolIssue} per entry that's a CLOSE TYPO of a real tool.
43
+ * Deliberately NOT flagged: any name the vocabulary knows (blocking it is the
44
+ * point — including a withheld one, which is merely redundant), an MCP tool (a
45
+ * legitimate plugin tool to block), or a bare unknown with no near match.
46
+ *
47
+ * This is the ONE place a near match still gates a finding, and it is not the
48
+ * confidence proxy the allow-side check was rightly stripped of. On a block-list
49
+ * the risk inverts: an entry naming nothing is harmless UNLESS you meant a real
50
+ * tool and mistyped it, and "meant a real tool" is precisely what a one-character
51
+ * distance evidences. `disallowedTools: [Zzzz]` blocks nothing and nobody
52
+ * intended otherwise; `disallowedTools: [Bsh]` leaves `Bash` wide open. So the
53
+ * distance here is the actual semantic signal, not a stand-in for one.
50
54
  */
51
55
  function disallowedToolIssues(tools, dialect) {
52
- const never = new Set(dialect.neverAvailableTools);
56
+ const vocab = subagentToolVocabulary(dialect);
53
57
  const issues = [];
54
58
  for (const raw of tools) {
55
59
  const tool = raw.split("(")[0].trim();
56
60
  if (tool === "" || tool === "*")
57
61
  continue;
58
- if (dialect.builtinAgentTools.includes(tool))
59
- continue; // legitimately blocked
60
- if (never.has(tool))
61
- continue; // harmless to list (already unavailable)
62
62
  if (dialect.mcpToolPattern.test(tool))
63
63
  continue; // a real plugin/MCP tool to block
64
- const near = closestTool(tool, dialect);
64
+ if ((0, vocabulary_js_1.classify)(vocab, tool).kind !== "unrecognised")
65
+ continue; // a real name — blocking it is fine
66
+ const near = (0, vocabulary_js_1.suggest)(vocab, tool);
65
67
  if (near === null)
66
68
  continue; // bare unknown → likely a plugin tool, not a typo
67
69
  issues.push({
68
70
  tool,
71
+ verdict: "unrecognised",
69
72
  kind: "unknown",
70
73
  suggestion: near,
74
+ severity: "scored",
71
75
  message: `disallowedTools entry "${tool}" matches no real tool — it blocks nothing. Did you mean "${near}"?`,
72
76
  });
73
77
  }
@@ -80,34 +84,32 @@ function disallowedToolIssues(tools, dialect) {
80
84
  * is stripped to its base tool before checking.
81
85
  */
82
86
  function verifyToolContract(tools, dialect) {
83
- const never = new Set(dialect.neverAvailableTools);
87
+ const vocab = subagentToolVocabulary(dialect);
84
88
  const issues = [];
85
89
  for (const raw of tools) {
86
90
  const tool = raw.split("(")[0].trim(); // strip a Tool(restriction) suffix
87
91
  if (tool === "" || tool === "*")
88
92
  continue; // "" / "*" = wildcard, inherits all
89
- if (never.has(tool)) {
90
- issues.push({
91
- tool,
92
- kind: "never-available",
93
- suggestion: null,
94
- message: `Tool "${tool}" is never available to a subagent — remove it from the tools list.`,
95
- });
96
- continue;
97
- }
98
- if (dialect.builtinAgentTools.includes(tool))
99
- continue;
100
93
  if (dialect.mcpToolPattern.test(tool))
101
94
  continue;
102
- const near = closestTool(tool, dialect);
103
- const hint = near ? ` Did you mean "${near}"?` : "";
95
+ const verdict = (0, vocabulary_js_1.classify)(vocab, tool);
96
+ const issue = (0, vocabulary_js_1.termIssue)(vocab, verdict, "Tool", "the subagent never gets it");
97
+ if (issue === null)
98
+ continue;
104
99
  issues.push({
105
100
  tool,
106
- kind: "unknown",
107
- suggestion: near,
108
- message: `Unknown tool "${tool}" — use a built-in tool (${dialect.builtinAgentTools.join(", ")}) or an MCP tool (mcp__server__tool).${hint}`,
101
+ verdict: verdict.kind,
102
+ kind: TOOL_ISSUE_KIND[verdict.kind],
103
+ suggestion: issue.suggestion,
104
+ ...(issue.condition !== undefined ? { condition: issue.condition } : {}),
105
+ severity: issue.severity,
106
+ message: issue.message,
109
107
  });
110
108
  }
111
109
  return issues;
112
110
  }
111
+ var vocabulary_js_2 = require("./vocabulary.js");
112
+ Object.defineProperty(exports, "scoredIssues", { enumerable: true, get: function () { return vocabulary_js_2.scoredIssues; } });
113
+ Object.defineProperty(exports, "advisoryIssues", { enumerable: true, get: function () { return vocabulary_js_2.advisoryIssues; } });
114
+ Object.defineProperty(exports, "authoringIssues", { enumerable: true, get: function () { return vocabulary_js_2.authoringIssues; } });
113
115
  //# sourceMappingURL=tool-contract.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The invariant that was missing when `Agent` sat in two catalogs at once.
3
+ *
4
+ * A `HarnessDialect` carries several name lists that describe the SAME
5
+ * vocabulary from different angles — `builtinAgentTools` (declarable),
6
+ * `neverAvailableTools` (dead), `sideEffectingTools` (a subset of declarable).
7
+ * Nothing checked that they agreed. So `Agent` could be listed as
8
+ * never-available while its own alias `Task` sat in the built-in catalog, and
9
+ * `dialect-drift.ts` could read `Agent` out of the vendor's shipped
10
+ * `sdk-tools.d.ts` every run, for months, without anything noticing the
11
+ * contradiction. The lists were consistent with nothing, including each other.
12
+ *
13
+ * These checks are cheap, total, and adapter-agnostic, so they run in the
14
+ * adapter conformance kit — every adapter, present and future, third-party
15
+ * included. A dialect that contradicts itself now fails LOUDLY at the point an
16
+ * author would first run the kit, instead of silently producing a confident
17
+ * wrong finding in someone else's repo.
18
+ *
19
+ * Deliberately NOT here: any judgement about whether a name is *correct*. This
20
+ * cannot tell you the platform renamed `Task` to `Agent` — only that you cannot
21
+ * claim both at once. Freshness against the real platform is
22
+ * `dialect-drift.ts`'s job; agreement between our own claims is this one's.
23
+ */
24
+ import type { HarnessDialect } from "./dialect.js";
25
+ import type { HarnessVocabulary } from "./vocabulary.js";
26
+ /** Human-readable violations of the dialect's internal name invariants. */
27
+ export declare function dialectVocabularyProblems(dialect: HarnessDialect): string[];
28
+ /**
29
+ * When a dialect declares a vocabulary, its legacy name lists must be exactly
30
+ * that vocabulary's projections. This is what stops the two from drifting once
31
+ * both exist: a dialect can carry the richer catalog AND the flat arrays other
32
+ * code still reads, but it cannot let them disagree.
33
+ */
34
+ export declare function vocabularyProjectionProblems(vocab: HarnessVocabulary, builtinAgentTools: readonly string[], neverAvailableTools: readonly string[]): string[];
35
+ //# sourceMappingURL=vocabulary-consistency.d.ts.map
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dialectVocabularyProblems = dialectVocabularyProblems;
4
+ exports.vocabularyProjectionProblems = vocabularyProjectionProblems;
5
+ /** Human-readable violations of the dialect's internal name invariants. */
6
+ function dialectVocabularyProblems(dialect) {
7
+ const problems = [];
8
+ const builtin = new Set(dialect.builtinAgentTools);
9
+ const never = new Set(dialect.neverAvailableTools);
10
+ // The exact state that shipped: a name claimed as both declarable and dead.
11
+ for (const tool of never)
12
+ if (builtin.has(tool))
13
+ problems.push(`tool "${tool}" is in BOTH builtinAgentTools and neverAvailableTools — ` +
14
+ `it cannot be both declarable and never available`);
15
+ // A side-effecting tool outside the catalog can never be reached by
16
+ // `classifyToolEffect` (rule 1 only fires for names rule 2 could see), so the
17
+ // entry is dead weight that reads as protection.
18
+ for (const tool of dialect.sideEffectingTools ?? [])
19
+ if (!builtin.has(tool))
20
+ problems.push(`tool "${tool}" is in sideEffectingTools but not in builtinAgentTools — ` +
21
+ `the effect classification can never reach it`);
22
+ // A block-semantics subset that names an event the dialect doesn't fire is a
23
+ // rule about nothing.
24
+ const events = new Set(dialect.hookEvents);
25
+ for (const [field, list] of [
26
+ ["noEffectHookEvents", dialect.noEffectHookEvents ?? []],
27
+ [
28
+ "permissionDecisionHookEvents",
29
+ dialect.permissionDecisionHookEvents ?? [],
30
+ ],
31
+ ])
32
+ for (const event of list)
33
+ if (!events.has(event))
34
+ problems.push(`hook event "${event}" is in ${field} but not in hookEvents — ` +
35
+ `it describes an event this dialect says never fires`);
36
+ return problems;
37
+ }
38
+ /**
39
+ * When a dialect declares a vocabulary, its legacy name lists must be exactly
40
+ * that vocabulary's projections. This is what stops the two from drifting once
41
+ * both exist: a dialect can carry the richer catalog AND the flat arrays other
42
+ * code still reads, but it cannot let them disagree.
43
+ */
44
+ function vocabularyProjectionProblems(vocab, builtinAgentTools, neverAvailableTools) {
45
+ const problems = [];
46
+ const declarable = new Set(vocab.terms.filter((t) => t.status !== "withheld").map((t) => t.name));
47
+ const withheld = new Set(vocab.terms.filter((t) => t.status === "withheld").map((t) => t.name));
48
+ const diff = (label, expected, actual) => {
49
+ const got = new Set(actual);
50
+ for (const n of expected)
51
+ if (!got.has(n))
52
+ problems.push(`${label} is missing "${n}", which the vocabulary declares`);
53
+ for (const n of got)
54
+ if (!expected.has(n))
55
+ problems.push(`${label} has "${n}", which the vocabulary does not declare`);
56
+ };
57
+ diff("builtinAgentTools", declarable, builtinAgentTools);
58
+ diff("neverAvailableTools", withheld, neverAvailableTools);
59
+ // A conditional term with no condition cannot be reported as one — the whole
60
+ // reason the status exists is to quote the platform's qualifier back.
61
+ for (const t of vocab.terms)
62
+ if (t.status === "conditional" && (t.condition ?? "").trim() === "")
63
+ problems.push(`term "${t.name}" is conditional but states no condition — ` +
64
+ `a condition we cannot quote is one we cannot report`);
65
+ // An alias pointing at a name the vocabulary doesn't hold sends the reader
66
+ // somewhere that doesn't exist.
67
+ for (const t of vocab.terms)
68
+ if (t.aliasOf !== undefined &&
69
+ !vocab.terms.some((o) => o.name === t.aliasOf))
70
+ problems.push(`term "${t.name}" is an alias of "${t.aliasOf}", which this vocabulary ` +
71
+ `does not contain`);
72
+ // Two entries for one name make `classify` order-dependent.
73
+ const seen = new Set();
74
+ for (const t of vocab.terms) {
75
+ if (seen.has(t.name))
76
+ problems.push(`term "${t.name}" appears more than once in the vocabulary`);
77
+ seen.add(t.name);
78
+ }
79
+ return problems;
80
+ }
81
+ //# sourceMappingURL=vocabulary-consistency.js.map
@@ -0,0 +1,138 @@
1
+ /** What the platform does with a term, per the vendor's own documentation. */
2
+ export type TermStatus = "available" | "withheld" | "conditional";
3
+ /** One word of a harness's vocabulary, with what the platform does with it. */
4
+ export interface VocabularyTerm {
5
+ readonly name: string;
6
+ readonly status: TermStatus;
7
+ /**
8
+ * The vendor's stated condition, near-verbatim. REQUIRED when `status` is
9
+ * `"conditional"` — a condition we cannot quote is a condition we cannot
10
+ * report, and reporting it is the whole point of the status.
11
+ */
12
+ readonly condition?: string;
13
+ /**
14
+ * The current name this term is a still-working deprecated alias of (e.g.
15
+ * `Task` → `Agent`, renamed in Claude Code 2.1.63). An alias is NOT a defect:
16
+ * the platform keeps honouring it.
17
+ */
18
+ readonly aliasOf?: string;
19
+ }
20
+ /** A named set of platform terms, tagged with where and when it was captured. */
21
+ export interface HarnessVocabulary {
22
+ /** Which vocabulary this is — used in messages, so it must read as English. */
23
+ readonly kind: string;
24
+ /**
25
+ * The exact vendor artifact + version this catalog was read from, e.g.
26
+ * `"code.claude.com/docs/en/hooks § Hook events (claude-code 2.1.233)"`.
27
+ * Printed with every `unrecognised` advisory, so our staleness is visible to
28
+ * the person who hit it rather than only to us.
29
+ */
30
+ readonly capturedFrom: string;
31
+ readonly terms: readonly VocabularyTerm[];
32
+ }
33
+ /**
34
+ * What the catalog says about one name. Total — there is no absent answer, and
35
+ * deliberately no near-match on the `unrecognised` branch (see the module note:
36
+ * a distance in scope at the decision point is what produced the bugs).
37
+ */
38
+ export type TermVerdict = {
39
+ readonly kind: "available";
40
+ readonly term: VocabularyTerm;
41
+ } | {
42
+ readonly kind: "withheld";
43
+ readonly term: VocabularyTerm;
44
+ } | {
45
+ readonly kind: "conditional";
46
+ readonly term: VocabularyTerm;
47
+ } | {
48
+ readonly kind: "unrecognised";
49
+ readonly name: string;
50
+ };
51
+ /** How much weight a finding carries — the ONLY input to whether it is scored. */
52
+ export type IssueSeverity =
53
+ /** A defect in the audited repo. Enters the grade. */
54
+ "scored"
55
+ /** True but not actionable, or a statement about vigiles. Never scored. */
56
+ | "advisory";
57
+ /** Look the name up. Total: always one of the four verdicts, never null. */
58
+ export declare function classify(vocab: HarnessVocabulary, name: string): TermVerdict;
59
+ /**
60
+ * Closest catalog name within edit distance 2, else null — a MESSAGE decoration
61
+ * only. Never call this to decide whether to report something; the verdict has
62
+ * already decided that. The ≤2 bound stays tight for the reason it always was:
63
+ * a loose bound mis-suggests (`TaskGet → Task?` is a different real tool, not a
64
+ * typo). Only `available` terms are offered — suggesting a name the platform
65
+ * withholds would trade one dead reference for another.
66
+ */
67
+ export declare function suggest(vocab: HarnessVocabulary, name: string): string | null;
68
+ /** A vocabulary finding: the message to show and whether it counts. */
69
+ export interface TermIssue {
70
+ /** Which verdict produced this — the input to every downstream policy. */
71
+ readonly verdict: TermVerdict["kind"];
72
+ readonly severity: IssueSeverity;
73
+ readonly message: string;
74
+ /** Near-match for the message only; null unless the term is unrecognised. */
75
+ readonly suggestion: string | null;
76
+ /**
77
+ * The vendor condition, present only for a `conditional` verdict. Carried so a
78
+ * report can GROUP the tools that share one condition instead of repeating the
79
+ * same sentence per tool — a delegating subagent legitimately declares eight of
80
+ * them, and eight identical paragraphs is noise from a tool that sells itself
81
+ * on not crying wolf.
82
+ */
83
+ readonly condition?: string;
84
+ }
85
+ /**
86
+ * Turn a verdict into the finding to report, or null when there is nothing to
87
+ * say. The severity is decided HERE, once, from the verdict — callers never
88
+ * invent their own policy, which is what let `scan` and `lint` drift apart from
89
+ * `compileAgent` before.
90
+ *
91
+ * `noun` names the thing in the message ("hook event" / "tool"); `subject`
92
+ * describes what listing it does, e.g. "a hook here never fires".
93
+ */
94
+ export declare function termIssue(vocab: HarnessVocabulary, verdict: TermVerdict, noun: string, deadConsequence: string): TermIssue | null;
95
+ /**
96
+ * The issues that count toward a grade. Replaces the per-check
97
+ * `confidentToolIssues` / `confidentHookEventIssues` helpers, which asked "is
98
+ * there a near match?" — a question about spelling, answered by a helper each
99
+ * caller had to remember to apply and which `compileAgent` did not, so `scan`,
100
+ * `lint` and authoring could disagree about which issues were real. Severity now
101
+ * travels ON the issue, decided once in {@link termIssue}, so the split is the
102
+ * same wherever it is taken.
103
+ */
104
+ export declare function scoredIssues<T extends {
105
+ readonly severity: IssueSeverity;
106
+ }>(issues: readonly T[]): T[];
107
+ /**
108
+ * The issues that are surfaced but never scored — `conditional` tools and any
109
+ * name newer than our capture. Kept out of the grade on purpose: vigiles's own
110
+ * staleness must not cost someone a letter.
111
+ */
112
+ export declare function advisoryIssues<T extends {
113
+ readonly severity: IssueSeverity;
114
+ }>(issues: readonly T[]): T[];
115
+ /**
116
+ * The issues an AUTHORING path treats as errors — everything except
117
+ * `conditional`. Authoring is a CLOSED world: you are writing this spec now,
118
+ * against the vigiles you have, so an unrecognised name is a typo worth stopping
119
+ * for. Auditing is an OPEN world: someone else wrote the file, possibly against
120
+ * a newer platform, so there the same verdict is only an advisory.
121
+ *
122
+ * `conditional` is an error in NEITHER. The tool is real and declaring it is
123
+ * correct; erroring on it is exactly what told delegating subagents to drop
124
+ * `Agent`, and what made `tools: Agent, Read, Bash` — a worked example in the
125
+ * vendor's own docs — fail to compile.
126
+ */
127
+ export declare function authoringIssues<T extends {
128
+ readonly verdict: TermVerdict["kind"];
129
+ }>(issues: readonly T[]): T[];
130
+ /**
131
+ * Build a vocabulary from a dialect that predates this module — `available` from
132
+ * its built-in catalog, `withheld` from its never-available list. A dialect on
133
+ * the legacy shape keeps working and its unknowns become `unrecognised`
134
+ * ADVISORIES rather than silence, which is the honest reading: a catalog with no
135
+ * recorded capture cannot claim a name is invalid.
136
+ */
137
+ export declare function vocabularyFromLists(kind: string, capturedFrom: string, available: readonly string[], withheld?: readonly string[]): HarnessVocabulary;
138
+ //# sourceMappingURL=vocabulary.d.ts.map