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.
- 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/run-scripts.d.ts +19 -3
- package/dist/adapters/claude-code/run-scripts.js +17 -7
- package/dist/adapters/claude-code/vocabulary.d.ts +133 -0
- package/dist/adapters/claude-code/vocabulary.js +208 -0
- package/dist/adapters/codex/eval.js +2 -0
- package/dist/audit-score.js +1 -1
- package/dist/cli.js +7 -2
- package/dist/core/compile.js +6 -1
- package/dist/core/dialect.d.ts +27 -0
- package/dist/core/eval-load-phase.d.ts +78 -0
- package/dist/core/eval-load-phase.js +104 -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/eval-define.d.ts +166 -0
- package/dist/eval-define.js +182 -0
- package/dist/eval-entry.d.ts +41 -0
- package/dist/eval-entry.js +203 -0
- package/dist/eval.js +2 -0
- package/dist/judge.js +2 -0
- package/dist/scan-behavioral.js +2 -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 +45 -0
- package/dist/scan.js +13 -1
- package/dist/test-coverage.d.ts +45 -0
- package/dist/test-coverage.js +91 -3
- package/dist/test.d.ts +2 -0
- package/dist/test.js +8 -1
- package/package.json +1 -1
- 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
|
|
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
|
|
@@ -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
|