vigiles 16.1.1 → 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/bash-effects.d.ts +57 -0
- package/dist/core/bash-effects.js +147 -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/markdown.d.ts +53 -0
- package/dist/core/markdown.js +99 -0
- package/dist/core/rule-meta.js +2 -2
- package/dist/core/skill-resources.js +58 -57
- package/dist/core/source-refs.d.ts +118 -0
- package/dist/core/source-refs.js +206 -0
- 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/coverage-evidence.js +15 -3
- package/dist/plugin-loader.js +22 -28
- package/dist/scan-core.d.ts +8 -1
- package/dist/scan-core.js +105 -20
- package/dist/scan-files.js +17 -20
- package/dist/scan.d.ts +24 -0
- package/dist/scan.js +8 -1
- package/package.json +1 -1
|
@@ -37,6 +37,15 @@ exports.skillResourceIssues = skillResourceIssues;
|
|
|
37
37
|
* `MD_HEADING`) and carries no illustrative cue (example / e.g. / such as /
|
|
38
38
|
* would be / template / →). See `inlinePathIsUsed`.
|
|
39
39
|
*
|
|
40
|
+
* CANDIDATES COME FROM THE PARSE, THE GATE READS THE PROSE. Every candidate is
|
|
41
|
+
* a `MarkdownRef` from `core/markdown.ts` — a link's DESTINATION or a code span
|
|
42
|
+
* that is not inside a link's text. The line is then consulted only to decide
|
|
43
|
+
* whether the surrounding prose DIRECTS the agent at the file. Before that
|
|
44
|
+
* split, a second regex scanned each line for backtick spans without knowing it
|
|
45
|
+
* was standing inside a link, and reported the link's LABEL as a missing
|
|
46
|
+
* resource while its destination resolved (measured on
|
|
47
|
+
* `microsoft/power-platform-skills` and `rohitg00/pro-workflow`, 2026-08-17).
|
|
48
|
+
*
|
|
40
49
|
* ESCAPE HATCH: a SKILL.md carrying `<!-- vigiles-disable skill-resource-resolves -->`
|
|
41
50
|
* anywhere in its body opts OUT of this check entirely (mirrors `orphans.ts`'s
|
|
42
51
|
* `vigiles-disable orphan-docs`) — for a skill whose body is inherently full of
|
|
@@ -57,13 +66,6 @@ const markdown_js_1 = require("./markdown.js");
|
|
|
57
66
|
// these is unambiguously a local bundled resource, even without a `./`.
|
|
58
67
|
const BUNDLE_DIRS = ["scripts", "references", "assets"];
|
|
59
68
|
const BUNDLE_PREFIX = new RegExp(`^(?:${BUNDLE_DIRS.join("|")})/`);
|
|
60
|
-
// A markdown inline link `[text](target)` — we read its target.
|
|
61
|
-
const MD_LINK = /\[[^\]]*\]\(([^)\s]+)\)/g;
|
|
62
|
-
// An inline-code path mention: a backtick span whose whole content is a single
|
|
63
|
-
// path token. We only treat it as a ref when it is a bundle-dir-prefixed path
|
|
64
|
-
// with an extension (the high-confidence shape); a bare `scripts` or a generic
|
|
65
|
-
// `foo.ts` mention is NOT flagged.
|
|
66
|
-
const INLINE_SPAN = /`([^`\n]+)`/g;
|
|
67
69
|
// A path must carry a file extension to be a resource reference. A bare word or
|
|
68
70
|
// a directory name (`scripts/lib`) is undecidable prose — skipped.
|
|
69
71
|
const HAS_EXT = /\.[A-Za-z0-9]+$/;
|
|
@@ -224,39 +226,40 @@ function inlinePathIsUsed(line) {
|
|
|
224
226
|
return false;
|
|
225
227
|
return USE_DIRECTIVE.test(line) || MD_HEADING.test(line);
|
|
226
228
|
}
|
|
227
|
-
/**
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (inlinePathIsUsed(line)) {
|
|
249
|
-
for (const m of line.matchAll(INLINE_SPAN)) {
|
|
250
|
-
const token = m[1].trim();
|
|
251
|
-
if (!isInlineBundlePath(token))
|
|
252
|
-
continue;
|
|
253
|
-
const resolved = localResourceTarget(token);
|
|
254
|
-
if (resolved !== null) {
|
|
255
|
-
out.push({ ref: token, resolved, kind: "path", line: lineNo });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
229
|
+
/**
|
|
230
|
+
* Turn one structural markdown reference into a candidate, or `null`.
|
|
231
|
+
*
|
|
232
|
+
* `line` is the raw source line, used ONLY by the prose gate — the candidate
|
|
233
|
+
* itself comes from {@link markdownRefs}, never from the characters. That split
|
|
234
|
+
* is load-bearing: the gate reads prose because prose is what it judges, while
|
|
235
|
+
* a REFERENCE is a thing the parser found.
|
|
236
|
+
*/
|
|
237
|
+
function candidateFor(ref, line) {
|
|
238
|
+
if (ref.kind === "link") {
|
|
239
|
+
// A markdown link is EXPLICIT follow-me syntax — the DESTINATION is the
|
|
240
|
+
// reference — so it's real UNLESS the line is an illustrative example.
|
|
241
|
+
// Suppress it ONLY on an illustrative cue; do NOT also require a use
|
|
242
|
+
// directive, or a plain `Resources: [API](references/api.md)` (no verb)
|
|
243
|
+
// goes unchecked (Codex review — that under-detection).
|
|
244
|
+
if (ILLUSTRATIVE_CUE.test(line))
|
|
245
|
+
return null;
|
|
246
|
+
const resolved = localResourceTarget(ref.value);
|
|
247
|
+
if (resolved === null)
|
|
248
|
+
return null;
|
|
249
|
+
return { ref: ref.value.trim(), resolved, kind: "link", line: ref.line };
|
|
258
250
|
}
|
|
259
|
-
|
|
251
|
+
// A bare inline backtick path is noisier (often just a mention), so it needs
|
|
252
|
+
// BOTH a use directive AND no cue, and only in the high-confidence
|
|
253
|
+
// bundle-dir-prefixed form.
|
|
254
|
+
if (!inlinePathIsUsed(line))
|
|
255
|
+
return null;
|
|
256
|
+
const token = ref.value.trim();
|
|
257
|
+
if (!isInlineBundlePath(token))
|
|
258
|
+
return null;
|
|
259
|
+
const resolved = localResourceTarget(token);
|
|
260
|
+
if (resolved === null)
|
|
261
|
+
return null;
|
|
262
|
+
return { ref: token, resolved, kind: "path", line: ref.line };
|
|
260
263
|
}
|
|
261
264
|
/**
|
|
262
265
|
* A SKILL.md body carrying this marker opts OUT of skill-resource checking
|
|
@@ -293,25 +296,23 @@ function skillResourceIssues(skillBody, skillDir, opts) {
|
|
|
293
296
|
const findings = [];
|
|
294
297
|
const seen = new Set();
|
|
295
298
|
const lines = skillBody.split("\n");
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
+
for (const ref of (0, markdown_js_1.markdownRefs)(skillBody)) {
|
|
300
|
+
const c = candidateFor(ref, lines[ref.line - 1] ?? "");
|
|
301
|
+
if (c === null)
|
|
302
|
+
continue;
|
|
303
|
+
if (resolvesAnywhere(c.resolved))
|
|
304
|
+
continue;
|
|
305
|
+
// De-dupe the same missing file referenced several times in the body.
|
|
306
|
+
const key = `${c.kind}:${c.resolved}`;
|
|
307
|
+
if (seen.has(key))
|
|
299
308
|
continue;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
seen.add(key);
|
|
308
|
-
findings.push({
|
|
309
|
-
ref: c.ref,
|
|
310
|
-
resolved: c.resolved,
|
|
311
|
-
kind: c.kind,
|
|
312
|
-
line: c.line,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
309
|
+
seen.add(key);
|
|
310
|
+
findings.push({
|
|
311
|
+
ref: c.ref,
|
|
312
|
+
resolved: c.resolved,
|
|
313
|
+
kind: c.kind,
|
|
314
|
+
line: c.line,
|
|
315
|
+
});
|
|
315
316
|
}
|
|
316
317
|
return findings;
|
|
317
318
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — recovering a FILE REFERENCE from PLAIN SOURCE TEXT.
|
|
3
|
+
*
|
|
4
|
+
* The third of three reference grammars, and the only one with no parser behind
|
|
5
|
+
* it. A markdown body is read by markdown-it (`core/markdown.ts` →
|
|
6
|
+
* `markdownRefs`), a hook `command` is read by mvdan-sh (`core/bash-effects.ts`
|
|
7
|
+
* → `commandWords`), and what is left — the body of a hook script, a helper
|
|
8
|
+
* `.js` / `.py` / `.rb` — is scanned for path-shaped character runs, because a
|
|
9
|
+
* general-purpose "find every path this program touches" analysis is not a
|
|
10
|
+
* thing this tool can be.
|
|
11
|
+
*
|
|
12
|
+
* Since that scan IS a character run, the only thing standing between it and a
|
|
13
|
+
* false accusation is where the run may START and STOP. This module owns both
|
|
14
|
+
* boundaries and the extension vocabularies, so no caller can build a pattern
|
|
15
|
+
* without them.
|
|
16
|
+
*
|
|
17
|
+
* ## The two boundaries, and the two live defects that came from omitting them
|
|
18
|
+
*
|
|
19
|
+
* RIGHT — the extension must END the token. `INTRA_REF_EXTS` used to be a bare
|
|
20
|
+
* alternation with no trailing assertion, and `js` sits ahead of `json` in it,
|
|
21
|
+
* so `hooks/hooks.json` matched as `hooks/hooks.js`. Measured 2026-08-17 on
|
|
22
|
+
* `microsoft/power-platform-skills`: a `//` comment naming `hooks/hooks.json`,
|
|
23
|
+
* with that exact file sitting beside it on disk, was reported as
|
|
24
|
+
* "hooks/hooks.js (referenced but MISSING)" — a maintainer told a file they
|
|
25
|
+
* have is missing, under a name they never wrote.
|
|
26
|
+
*
|
|
27
|
+
* LEFT — the surface dir must START the token. `(?:agents|hooks|skills)/` with
|
|
28
|
+
* nothing before it also matches the tail of `claude-agents/`. Measured the
|
|
29
|
+
* same day on `fcakyon/claude-codex-settings`: a
|
|
30
|
+
* `new URL("../../../claude-agents/fable-advisor.md", …)` — a correct,
|
|
31
|
+
* resolving reference — was reported as a broken `agents/fable-advisor.md`.
|
|
32
|
+
* That boundary is a predicate ({@link startsAtSeparator}) rather than a regex
|
|
33
|
+
* lookbehind, because the browser twin (`scan-files.ts`) compiles this into the
|
|
34
|
+
* demo engine, and because the caller already inspects `m.index` for its
|
|
35
|
+
* plugin-rooted test.
|
|
36
|
+
*
|
|
37
|
+
* ## Comments are prose in every language, not only in shell
|
|
38
|
+
*
|
|
39
|
+
* `stripShellComments` had the right idea and too narrow a domain: a full-line
|
|
40
|
+
* `#` in a `.sh` file is prose, and so is a full-line `//` or a JSDoc `*` in a
|
|
41
|
+
* `.js` file. Both remaining corpus false accusations in this detector came out
|
|
42
|
+
* of JSDoc — `* It is deliberately not registered in hooks/hooks.json:` and
|
|
43
|
+
* "`git log --grep=.env -- hooks/x.mjs` is refused …". Neither is a file
|
|
44
|
+
* operation; both were reported as broken references.
|
|
45
|
+
*
|
|
46
|
+
* FULL-LINE ONLY, the same rule the shell version already held. A trailing
|
|
47
|
+
* comment on a real code line is left alone, so a genuine reference sharing a
|
|
48
|
+
* line with code is never dropped. The cost is stated rather than hidden: a
|
|
49
|
+
* path mentioned ONLY in a trailing comment still counts as a reference.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Whether a match at `idx` begins at a path boundary rather than inside a
|
|
53
|
+
* longer name. Position 0 counts as a boundary.
|
|
54
|
+
*
|
|
55
|
+
* `/` is a boundary on purpose: `${CLAUDE_PLUGIN_ROOT}/hooks/x.sh` is the
|
|
56
|
+
* standard spelling, and the caller's plugin-rooted test decides whether the
|
|
57
|
+
* segment before that slash roots the path inside the plugin or outside it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function startsAtSeparator(content: string, idx: number): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Extensions an intra-plugin reference may carry — the file kinds a plugin's
|
|
62
|
+
* own hook / helper source legitimately points at.
|
|
63
|
+
*/
|
|
64
|
+
export declare const INTRA_REF_EXTENSIONS: readonly ["md", "sh", "cmd", "mjs", "cjs", "js", "ts", "py", "rb", "txt", "json"];
|
|
65
|
+
/**
|
|
66
|
+
* Extensions a RUNNABLE script carries. Shared by the hook scanner and the
|
|
67
|
+
* coverage twins, which each declared their own copy before.
|
|
68
|
+
*/
|
|
69
|
+
export declare const SCRIPT_REF_EXTENSIONS: readonly ["sh", "mjs", "cjs", "js", "ts", "py", "rb"];
|
|
70
|
+
/**
|
|
71
|
+
* A plugin-relative path under one of `dirs`, carrying a known extension — the
|
|
72
|
+
* pattern for scanning a plugin's own SOURCE TEXT.
|
|
73
|
+
*
|
|
74
|
+
* The left boundary is not baked into the returned regex (see
|
|
75
|
+
* {@link startsAtSeparator}): a caller scanning raw text must apply that
|
|
76
|
+
* predicate at `m.index`, which both the disk detector and its browser twin do
|
|
77
|
+
* inside their plugin-rooted test.
|
|
78
|
+
*/
|
|
79
|
+
export declare function intraRefPattern(dirs: readonly string[]): RegExp;
|
|
80
|
+
/**
|
|
81
|
+
* A script path occupying a WHOLE shell WORD.
|
|
82
|
+
*
|
|
83
|
+
* ⚠️ ANCHORING IS NOT WHAT FIXES THE `node -e` DEFECT, and saying so would be
|
|
84
|
+
* wrong twice over — the payload
|
|
85
|
+
* `import(require(node:url).pathToFileURL(require(node:path).join(root,hooks,always-on.mjs`
|
|
86
|
+
* contains no whitespace and ends in `.mjs`, so it satisfies this pattern
|
|
87
|
+
* perfectly (asserted in source-refs.test.ts, so the claim cannot drift back).
|
|
88
|
+
* That defect is fixed one level up, by `commandWords` refusing to hand the
|
|
89
|
+
* argument of `-e` to anyone.
|
|
90
|
+
*
|
|
91
|
+
* What anchoring buys is narrower and worth stating exactly: the reported name
|
|
92
|
+
* is always a word a shell could hand to `execve`, never a fragment cut out of
|
|
93
|
+
* a longer one. `echo "see hooks/x.sh"` is one word containing a path; before,
|
|
94
|
+
* `hooks/x.sh` was lifted out of it and checked as though the hook ran it.
|
|
95
|
+
*
|
|
96
|
+
* The cost, stated rather than discovered later: a path bundled into a flag
|
|
97
|
+
* (`--require=hooks/x.js`) is no longer seen. Measured across the 32-repo
|
|
98
|
+
* dogfood corpus, that costs zero findings.
|
|
99
|
+
*/
|
|
100
|
+
export declare function scriptWordPattern(): RegExp;
|
|
101
|
+
/**
|
|
102
|
+
* A script path appearing anywhere inside a string — for the one caller with no
|
|
103
|
+
* shell parse to hand (the coverage twins scan a serialized settings blob).
|
|
104
|
+
* Carries the right boundary; it cannot carry the left one, and its callers
|
|
105
|
+
* gate every hit on the file existing, so a stray match is dropped rather than
|
|
106
|
+
* reported.
|
|
107
|
+
*/
|
|
108
|
+
export declare function scriptRefPattern(): RegExp;
|
|
109
|
+
/**
|
|
110
|
+
* Drop FULL-LINE comments (including a shebang, which also starts with `#`)
|
|
111
|
+
* from an executable source before it is scanned for path references.
|
|
112
|
+
*
|
|
113
|
+
* A file of unknown kind is returned unchanged: guessing a comment syntax is
|
|
114
|
+
* how a real reference gets deleted, and this detector's contract is that it
|
|
115
|
+
* under-reports rather than accuses.
|
|
116
|
+
*/
|
|
117
|
+
export declare function stripFullLineComments(path: string, content: string): string;
|
|
118
|
+
//# sourceMappingURL=source-refs.d.ts.map
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles — recovering a FILE REFERENCE from PLAIN SOURCE TEXT.
|
|
4
|
+
*
|
|
5
|
+
* The third of three reference grammars, and the only one with no parser behind
|
|
6
|
+
* it. A markdown body is read by markdown-it (`core/markdown.ts` →
|
|
7
|
+
* `markdownRefs`), a hook `command` is read by mvdan-sh (`core/bash-effects.ts`
|
|
8
|
+
* → `commandWords`), and what is left — the body of a hook script, a helper
|
|
9
|
+
* `.js` / `.py` / `.rb` — is scanned for path-shaped character runs, because a
|
|
10
|
+
* general-purpose "find every path this program touches" analysis is not a
|
|
11
|
+
* thing this tool can be.
|
|
12
|
+
*
|
|
13
|
+
* Since that scan IS a character run, the only thing standing between it and a
|
|
14
|
+
* false accusation is where the run may START and STOP. This module owns both
|
|
15
|
+
* boundaries and the extension vocabularies, so no caller can build a pattern
|
|
16
|
+
* without them.
|
|
17
|
+
*
|
|
18
|
+
* ## The two boundaries, and the two live defects that came from omitting them
|
|
19
|
+
*
|
|
20
|
+
* RIGHT — the extension must END the token. `INTRA_REF_EXTS` used to be a bare
|
|
21
|
+
* alternation with no trailing assertion, and `js` sits ahead of `json` in it,
|
|
22
|
+
* so `hooks/hooks.json` matched as `hooks/hooks.js`. Measured 2026-08-17 on
|
|
23
|
+
* `microsoft/power-platform-skills`: a `//` comment naming `hooks/hooks.json`,
|
|
24
|
+
* with that exact file sitting beside it on disk, was reported as
|
|
25
|
+
* "hooks/hooks.js (referenced but MISSING)" — a maintainer told a file they
|
|
26
|
+
* have is missing, under a name they never wrote.
|
|
27
|
+
*
|
|
28
|
+
* LEFT — the surface dir must START the token. `(?:agents|hooks|skills)/` with
|
|
29
|
+
* nothing before it also matches the tail of `claude-agents/`. Measured the
|
|
30
|
+
* same day on `fcakyon/claude-codex-settings`: a
|
|
31
|
+
* `new URL("../../../claude-agents/fable-advisor.md", …)` — a correct,
|
|
32
|
+
* resolving reference — was reported as a broken `agents/fable-advisor.md`.
|
|
33
|
+
* That boundary is a predicate ({@link startsAtSeparator}) rather than a regex
|
|
34
|
+
* lookbehind, because the browser twin (`scan-files.ts`) compiles this into the
|
|
35
|
+
* demo engine, and because the caller already inspects `m.index` for its
|
|
36
|
+
* plugin-rooted test.
|
|
37
|
+
*
|
|
38
|
+
* ## Comments are prose in every language, not only in shell
|
|
39
|
+
*
|
|
40
|
+
* `stripShellComments` had the right idea and too narrow a domain: a full-line
|
|
41
|
+
* `#` in a `.sh` file is prose, and so is a full-line `//` or a JSDoc `*` in a
|
|
42
|
+
* `.js` file. Both remaining corpus false accusations in this detector came out
|
|
43
|
+
* of JSDoc — `* It is deliberately not registered in hooks/hooks.json:` and
|
|
44
|
+
* "`git log --grep=.env -- hooks/x.mjs` is refused …". Neither is a file
|
|
45
|
+
* operation; both were reported as broken references.
|
|
46
|
+
*
|
|
47
|
+
* FULL-LINE ONLY, the same rule the shell version already held. A trailing
|
|
48
|
+
* comment on a real code line is left alone, so a genuine reference sharing a
|
|
49
|
+
* line with code is never dropped. The cost is stated rather than hidden: a
|
|
50
|
+
* path mentioned ONLY in a trailing comment still counts as a reference.
|
|
51
|
+
*/
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.SCRIPT_REF_EXTENSIONS = exports.INTRA_REF_EXTENSIONS = void 0;
|
|
54
|
+
exports.startsAtSeparator = startsAtSeparator;
|
|
55
|
+
exports.intraRefPattern = intraRefPattern;
|
|
56
|
+
exports.scriptWordPattern = scriptWordPattern;
|
|
57
|
+
exports.scriptRefPattern = scriptRefPattern;
|
|
58
|
+
exports.stripFullLineComments = stripFullLineComments;
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Boundaries — the two assertions no pattern here may be built without
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
/**
|
|
63
|
+
* The right boundary: the matched extension must be the END of the token.
|
|
64
|
+
* A negative lookahead over the characters an extension is drawn from, so `js`
|
|
65
|
+
* cannot claim the head of `json` / `jsonl` and `py` cannot claim `pyc`. `.` is
|
|
66
|
+
* deliberately NOT in the class, which preserves the prior `\b` behaviour on
|
|
67
|
+
* `bundle.js.map`.
|
|
68
|
+
*/
|
|
69
|
+
const ENDS_TOKEN = "(?![A-Za-z0-9_])";
|
|
70
|
+
/**
|
|
71
|
+
* Characters a path SEGMENT is made of. A match preceded by one of these landed
|
|
72
|
+
* in the middle of a longer name (`claude-agents/`), which is not a reference
|
|
73
|
+
* to the surface dir it happens to end with.
|
|
74
|
+
*/
|
|
75
|
+
const SEGMENT_CHAR = /[A-Za-z0-9._-]/;
|
|
76
|
+
/**
|
|
77
|
+
* Whether a match at `idx` begins at a path boundary rather than inside a
|
|
78
|
+
* longer name. Position 0 counts as a boundary.
|
|
79
|
+
*
|
|
80
|
+
* `/` is a boundary on purpose: `${CLAUDE_PLUGIN_ROOT}/hooks/x.sh` is the
|
|
81
|
+
* standard spelling, and the caller's plugin-rooted test decides whether the
|
|
82
|
+
* segment before that slash roots the path inside the plugin or outside it.
|
|
83
|
+
*/
|
|
84
|
+
function startsAtSeparator(content, idx) {
|
|
85
|
+
if (idx <= 0)
|
|
86
|
+
return true;
|
|
87
|
+
return !SEGMENT_CHAR.test(content[idx - 1] ?? "");
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Extension vocabularies
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* Extensions an intra-plugin reference may carry — the file kinds a plugin's
|
|
94
|
+
* own hook / helper source legitimately points at.
|
|
95
|
+
*/
|
|
96
|
+
exports.INTRA_REF_EXTENSIONS = [
|
|
97
|
+
"md",
|
|
98
|
+
"sh",
|
|
99
|
+
"cmd",
|
|
100
|
+
"mjs",
|
|
101
|
+
"cjs",
|
|
102
|
+
"js",
|
|
103
|
+
"ts",
|
|
104
|
+
"py",
|
|
105
|
+
"rb",
|
|
106
|
+
"txt",
|
|
107
|
+
"json",
|
|
108
|
+
];
|
|
109
|
+
/**
|
|
110
|
+
* Extensions a RUNNABLE script carries. Shared by the hook scanner and the
|
|
111
|
+
* coverage twins, which each declared their own copy before.
|
|
112
|
+
*/
|
|
113
|
+
exports.SCRIPT_REF_EXTENSIONS = [
|
|
114
|
+
"sh",
|
|
115
|
+
"mjs",
|
|
116
|
+
"cjs",
|
|
117
|
+
"js",
|
|
118
|
+
"ts",
|
|
119
|
+
"py",
|
|
120
|
+
"rb",
|
|
121
|
+
];
|
|
122
|
+
/**
|
|
123
|
+
* A plugin-relative path under one of `dirs`, carrying a known extension — the
|
|
124
|
+
* pattern for scanning a plugin's own SOURCE TEXT.
|
|
125
|
+
*
|
|
126
|
+
* The left boundary is not baked into the returned regex (see
|
|
127
|
+
* {@link startsAtSeparator}): a caller scanning raw text must apply that
|
|
128
|
+
* predicate at `m.index`, which both the disk detector and its browser twin do
|
|
129
|
+
* inside their plugin-rooted test.
|
|
130
|
+
*/
|
|
131
|
+
function intraRefPattern(dirs) {
|
|
132
|
+
const exts = exports.INTRA_REF_EXTENSIONS.join("|");
|
|
133
|
+
return new RegExp(`(?:${dirs.join("|")})/[A-Za-z0-9._/-]+\\.(?:${exts})${ENDS_TOKEN}`, "g");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A script path occupying a WHOLE shell WORD.
|
|
137
|
+
*
|
|
138
|
+
* ⚠️ ANCHORING IS NOT WHAT FIXES THE `node -e` DEFECT, and saying so would be
|
|
139
|
+
* wrong twice over — the payload
|
|
140
|
+
* `import(require(node:url).pathToFileURL(require(node:path).join(root,hooks,always-on.mjs`
|
|
141
|
+
* contains no whitespace and ends in `.mjs`, so it satisfies this pattern
|
|
142
|
+
* perfectly (asserted in source-refs.test.ts, so the claim cannot drift back).
|
|
143
|
+
* That defect is fixed one level up, by `commandWords` refusing to hand the
|
|
144
|
+
* argument of `-e` to anyone.
|
|
145
|
+
*
|
|
146
|
+
* What anchoring buys is narrower and worth stating exactly: the reported name
|
|
147
|
+
* is always a word a shell could hand to `execve`, never a fragment cut out of
|
|
148
|
+
* a longer one. `echo "see hooks/x.sh"` is one word containing a path; before,
|
|
149
|
+
* `hooks/x.sh` was lifted out of it and checked as though the hook ran it.
|
|
150
|
+
*
|
|
151
|
+
* The cost, stated rather than discovered later: a path bundled into a flag
|
|
152
|
+
* (`--require=hooks/x.js`) is no longer seen. Measured across the 32-repo
|
|
153
|
+
* dogfood corpus, that costs zero findings.
|
|
154
|
+
*/
|
|
155
|
+
function scriptWordPattern() {
|
|
156
|
+
const exts = exports.SCRIPT_REF_EXTENSIONS.join("|");
|
|
157
|
+
return new RegExp(`^[^\\s*?]+\\.(?:${exts})$`);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A script path appearing anywhere inside a string — for the one caller with no
|
|
161
|
+
* shell parse to hand (the coverage twins scan a serialized settings blob).
|
|
162
|
+
* Carries the right boundary; it cannot carry the left one, and its callers
|
|
163
|
+
* gate every hit on the file existing, so a stray match is dropped rather than
|
|
164
|
+
* reported.
|
|
165
|
+
*/
|
|
166
|
+
function scriptRefPattern() {
|
|
167
|
+
const exts = exports.SCRIPT_REF_EXTENSIONS.join("|");
|
|
168
|
+
return new RegExp("[\\w./$" + "{}@-]+\\." + `(?:${exts})${ENDS_TOKEN}`, "g");
|
|
169
|
+
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Comments (prose inside an executable source)
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/** Source kinds whose full-line comments this module knows how to drop. */
|
|
174
|
+
const HASH_COMMENT = /\.(?:sh|bash|zsh|cmd|py|rb|pl)$/i;
|
|
175
|
+
const SLASH_COMMENT = /\.(?:m|c)?[jt]s$/i;
|
|
176
|
+
/**
|
|
177
|
+
* A line that is ENTIRELY a comment in a `//`-style language: a `//` line, a
|
|
178
|
+
* block-comment delimiter, or a JSDoc continuation `*` followed by whitespace
|
|
179
|
+
* or end of line.
|
|
180
|
+
*
|
|
181
|
+
* A bare `*name` does NOT count — `*run() {}` is a generator method, and
|
|
182
|
+
* dropping that line would silently delete a real reference from code.
|
|
183
|
+
*/
|
|
184
|
+
const SLASH_COMMENT_LINE = /^(?:\/\/|\/\*|\*\/|\*(?:\s|$))/;
|
|
185
|
+
/**
|
|
186
|
+
* Drop FULL-LINE comments (including a shebang, which also starts with `#`)
|
|
187
|
+
* from an executable source before it is scanned for path references.
|
|
188
|
+
*
|
|
189
|
+
* A file of unknown kind is returned unchanged: guessing a comment syntax is
|
|
190
|
+
* how a real reference gets deleted, and this detector's contract is that it
|
|
191
|
+
* under-reports rather than accuses.
|
|
192
|
+
*/
|
|
193
|
+
function stripFullLineComments(path, content) {
|
|
194
|
+
const isHash = HASH_COMMENT.test(path);
|
|
195
|
+
const isSlash = SLASH_COMMENT.test(path);
|
|
196
|
+
if (!isHash && !isSlash)
|
|
197
|
+
return content;
|
|
198
|
+
return content
|
|
199
|
+
.split("\n")
|
|
200
|
+
.filter((line) => {
|
|
201
|
+
const t = line.trimStart();
|
|
202
|
+
return isHash ? !t.startsWith("#") : !SLASH_COMMENT_LINE.test(t);
|
|
203
|
+
})
|
|
204
|
+
.join("\n");
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=source-refs.js.map
|
|
@@ -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
|