vigiles 16.1.2 → 16.1.3

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.
@@ -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
@@ -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
- * blockedcorrect), 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
+ * pointincluding 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
- * blockedcorrect), 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
+ * pointincluding 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