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.
- package/dist/adapter-conformance.js +17 -0
- package/dist/adapters/claude-code/dialect.d.ts +19 -13
- package/dist/adapters/claude-code/dialect.js +40 -62
- package/dist/adapters/claude-code/vocabulary.d.ts +133 -0
- package/dist/adapters/claude-code/vocabulary.js +208 -0
- package/dist/core/compile.js +6 -1
- package/dist/core/dialect.d.ts +27 -0
- package/dist/core/hook-events.d.ts +32 -15
- package/dist/core/hook-events.js +23 -29
- package/dist/core/hook-program.js +12 -4
- package/dist/core/rule-meta.js +2 -2
- package/dist/core/tool-contract.d.ts +69 -30
- package/dist/core/tool-contract.js +59 -57
- package/dist/core/vocabulary-consistency.d.ts +35 -0
- package/dist/core/vocabulary-consistency.js +81 -0
- package/dist/core/vocabulary.d.ts +138 -0
- package/dist/core/vocabulary.js +262 -0
- package/dist/scan-core.d.ts +8 -1
- package/dist/scan-core.js +64 -6
- package/dist/scan-files.js +4 -1
- package/dist/scan.d.ts +24 -0
- package/dist/scan.js +8 -1
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `WorktreeRemove`, …
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
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
|
-
/**
|
|
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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
43
|
+
export declare function hookEventVocabulary(dialect: HarnessDialect): HarnessVocabulary;
|
|
28
44
|
/**
|
|
29
|
-
* Verify hook-event names against the dialect
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
package/dist/core/hook-events.js
CHANGED
|
@@ -1,48 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.authoringIssues = exports.advisoryIssues = exports.scoredIssues = void 0;
|
|
4
|
+
exports.hookEventVocabulary = hookEventVocabulary;
|
|
4
5
|
exports.verifyHookEvents = verifyHookEvents;
|
|
5
|
-
const
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
25
|
-
return
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
22
|
+
const vocab = hookEventVocabulary(dialect);
|
|
34
23
|
const issues = [];
|
|
35
24
|
for (const event of events) {
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
685
|
-
if (
|
|
686
|
-
throw new HookCompileError(
|
|
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
|
package/dist/core/rule-meta.js
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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
|
|
6
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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
|
|
73
|
+
export declare function subagentToolVocabulary(dialect: HarnessDialect): HarnessVocabulary;
|
|
37
74
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
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
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* legitimate plugin tool to block), or a bare unknown with no near match
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
|
8
|
+
const vocabulary_js_1 = require("./vocabulary.js");
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
38
|
-
return
|
|
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
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* legitimate plugin tool to block), or a bare unknown with no near match
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
103
|
-
const
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|