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.
@@ -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
- /** Collect candidate bundled-resource refs from one body line, skipping fences. */
228
- function candidatesInLine(line, lineNo) {
229
- const out = [];
230
- // A markdown link is EXPLICIT follow-me syntax (`[text](target)`) — the link IS
231
- // the reference — so it's real UNLESS the line is an illustrative example.
232
- // Suppress it ONLY on an illustrative cue; do NOT also require a use directive,
233
- // or a plain `Resources: [API](references/api.md)` (no verb) goes unchecked
234
- // (Codex review — that under-detection). A BARE inline backtick path is noisier
235
- // (often just a mention), so it still needs BOTH a use directive AND no cue.
236
- const illustrative = ILLUSTRATIVE_CUE.test(line);
237
- // Markdown links: a relative path target that passes the local-resource gate.
238
- if (!illustrative) {
239
- for (const m of line.matchAll(MD_LINK)) {
240
- const resolved = localResourceTarget(m[1]);
241
- if (resolved !== null) {
242
- out.push({ ref: m[1].trim(), resolved, kind: "link", line: lineNo });
243
- }
244
- }
245
- }
246
- // Inline-code path mentions: only the high-confidence bundle-dir-prefixed form,
247
- // and only when the prose directs the agent to use it (the noisier shape).
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
- return out;
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 fenced = (0, markdown_js_1.fencedLineFlags)(skillBody);
297
- for (let i = 0; i < lines.length; i++) {
298
- if (fenced[i])
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
- for (const c of candidatesInLine(lines[i], i + 1)) {
301
- if (resolvesAnywhere(c.resolved))
302
- continue;
303
- // De-dupe the same missing file referenced several times in the body.
304
- const key = `${c.kind}:${c.resolved}`;
305
- if (seen.has(key))
306
- continue;
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 / never-available tool — a guaranteed-dead reference
6
- * a compiler catches, not a runtime surprise.
5
+ * is a typo or a nonexistent tool — a guaranteed-dead reference a compiler
6
+ * catches, not a runtime surprise.
7
7
  *
8
8
  * ONE pure detector (`one-detector-no-drift`), reused by THREE callers so they
9
9
  * can't disagree: `compileAgent` (spec authoring), `scan` (read-only audit of a
@@ -11,51 +11,89 @@
11
11
  * commit gate). The dialect is injected (core ⊄ adapter) — the composition root
12
12
  * passes `claudeCodeDialect` / `codexDialect`.
13
13
  *
14
- * Scope note: this validates a SUBAGENT contract against the SUBAGENT catalog
15
- * (`builtinAgentTools` / `neverAvailableTools`). A skill's `allowed-tools` is a
16
- * DIFFERENT namespace (skills legitimately use `AskUserQuestion`, `TaskCreate`,
17
- * … which are never-available to a subagent), so it is deliberately NOT validated
18
- * here — doing so against the agent catalog would be a false-positive factory.
14
+ * WHAT CHANGED, 2026-08-17. This used to split names two ways — in
15
+ * `builtinAgentTools` (fine) or in `neverAvailableTools` (dead) — and decide
16
+ * what to say about a name in neither by its edit distance to the first list.
17
+ * Two failures came out of that shape:
18
+ *
19
+ * - `Agent` was in the DENYLIST while its own deprecated alias `Task` was in the
20
+ * catalog, so vigiles rejected the platform's current name, accepted the old
21
+ * one, and told orchestrator subagents to remove the tool they exist to use.
22
+ * Nothing could notice, because the two lists were never compared.
23
+ * - Real tools vigiles didn't know (`EndConversation`, `TaskOutput`,
24
+ * `Workflow`) and outright invented ones passed in silence, while typos of
25
+ * known names were caught — so the more wrong a name was, the likelier it
26
+ * went unreported.
27
+ *
28
+ * Names are now CLASSIFIED against the dialect's vocabulary
29
+ * (`core/vocabulary.ts`), which has a third status for what the two-way split
30
+ * could not express: the vendor removes `Agent` only at the spawn depth limit,
31
+ * `ExitPlanMode` only outside plan mode, and most built-ins only from a
32
+ * background subagent. Those are `conditional` — reported as a note with the
33
+ * condition quoted, never as "remove it". Severity travels on the issue, so
34
+ * `scan`, `lint` and `compileAgent` cannot drift apart on which issues count.
35
+ *
36
+ * Scope note: this validates a SUBAGENT contract against the SUBAGENT catalog. A
37
+ * skill's `allowed-tools` is a DIFFERENT namespace (skills legitimately use
38
+ * `AskUserQuestion`, `TaskCreate`, … which a subagent doesn't get), so it is
39
+ * deliberately NOT validated here — doing so against the agent catalog would be
40
+ * a false-positive factory.
19
41
  */
20
42
  import type { HarnessDialect } from "./dialect.js";
21
- export type ToolIssueKind = "never-available" | "unknown";
43
+ import { type HarnessVocabulary, type IssueSeverity, type TermVerdict } from "./vocabulary.js";
44
+ export type ToolIssueKind =
45
+ /** The platform removes it unconditionally — a real, scored defect. */
46
+ "never-available"
47
+ /** Not in vigiles's catalog — advisory; may be newer than our capture. */
48
+ | "unknown"
49
+ /** Real, but removed under a condition vigiles can't see — advisory. */
50
+ | "conditional";
22
51
  export interface ToolIssue {
23
52
  readonly tool: string;
24
53
  readonly kind: ToolIssueKind;
25
- /** Closest known built-in tool (did-you-mean), or null. */
54
+ /** Which vocabulary verdict produced this — the input to every policy. */
55
+ readonly verdict: TermVerdict["kind"];
56
+ /** Closest known built-in tool (did-you-mean), or null. Message only. */
26
57
  readonly suggestion: string | null;
58
+ /**
59
+ * The vendor condition — present ONLY for a `conditional` verdict. Carried so
60
+ * a report can group the tools sharing one condition rather than repeat the
61
+ * same sentence per tool.
62
+ */
63
+ readonly condition?: string;
64
+ /** `"scored"` counts toward the grade; `"advisory"` never does. */
65
+ readonly severity: IssueSeverity;
27
66
  /** A ready-to-show, actionable message. */
28
67
  readonly message: string;
29
68
  }
30
69
  /**
31
- * Closest known built-in tool by edit distance (≤ 2), for a "did you mean" hint.
32
- * The ≤ 2 bound is deliberately tight: a suggestion is a CONFIDENCE signal (this
33
- * `unknown` is really a typo of a real tool), and a loose bound mis-suggests —
34
- * `TaskGet → Task?` (distance 3) is a real tool set, not a typo of `Task`.
70
+ * The tool vocabulary this dialect verifies against — its declared one, else a
71
+ * synthesised one from the flat lists so a legacy adapter keeps working.
35
72
  */
36
- export declare function closestTool(tool: string, dialect: HarnessDialect): string | null;
73
+ export declare function subagentToolVocabulary(dialect: HarnessDialect): HarnessVocabulary;
37
74
  /**
38
- * The HIGH-CONFIDENCE subset of a contract's issues — the ones safe to flag when
39
- * AUDITING a third-party plugin (scan / lint), where the catalog can't know
40
- * every tool (plugin-/MCP-provided, newer platform tools). Only two are confident:
41
- * a `never-available` tool (a curated denylist) and an `unknown` with a close
42
- * typo suggestion (`Edt → Edit`). A bare `unknown` with no near match is NOT
43
- * flagged here — it is more likely a tool vigiles doesn't know than a defect
44
- * (sweeping real plugins surfaced a 280★ plugin using `TaskCreate/TaskGet/…`
45
- * consistently; flagging those would be crying wolf). `compileAgent` stays strict
46
- * — when you author your OWN spec, every unrecognized tool is worth an error.
75
+ * Closest known built-in tool by edit distance (≤ 2), for a "did you mean" hint.
76
+ * A MESSAGE DECORATION, never a gate: whether to report is already settled by
77
+ * the verdict before this is called. The ≤ 2 bound stays tight because a loose
78
+ * bound mis-suggests — `TaskGet → Task?` is a different real tool, not a typo.
47
79
  */
48
- export declare function confidentToolIssues(issues: readonly ToolIssue[]): ToolIssue[];
80
+ export declare function closestTool(tool: string, dialect: HarnessDialect): string | null;
49
81
  /**
50
82
  * Verify a subagent's `disallowedTools:` BLOCK-list — the mirror of the allow
51
83
  * contract. A typo here is dangerous: you meant to block `Bash` but wrote `Bsh`,
52
84
  * so nothing is blocked and the dangerous tool stays available, silently. Returns
53
- * one {@link ToolIssue} per entry that's a CLOSE TYPO of a real built-in (the
54
- * high-confidence signal). Deliberately NOT flagged: a real built-in (it IS being
55
- * blocked — correct), a never-available tool (harmless to block), an MCP tool (a
56
- * legitimate plugin tool to block), or a bare unknown with no near match (likely
57
- * a plugin/MCP tool, not a typo — the cry-wolf trap). The block-list inverts the
58
- * allow check: never-available is fine to list, a typo is the actual defect.
85
+ * one {@link ToolIssue} per entry that's a CLOSE TYPO of a real tool.
86
+ * Deliberately NOT flagged: any name the vocabulary knows (blocking it is the
87
+ * point — including a withheld one, which is merely redundant), an MCP tool (a
88
+ * legitimate plugin tool to block), or a bare unknown with no near match.
89
+ *
90
+ * This is the ONE place a near match still gates a finding, and it is not the
91
+ * confidence proxy the allow-side check was rightly stripped of. On a block-list
92
+ * the risk inverts: an entry naming nothing is harmless UNLESS you meant a real
93
+ * tool and mistyped it, and "meant a real tool" is precisely what a one-character
94
+ * distance evidences. `disallowedTools: [Zzzz]` blocks nothing and nobody
95
+ * intended otherwise; `disallowedTools: [Bsh]` leaves `Bash` wide open. So the
96
+ * distance here is the actual semantic signal, not a stand-in for one.
59
97
  */
60
98
  export declare function disallowedToolIssues(tools: readonly string[], dialect: HarnessDialect): ToolIssue[];
61
99
  /**
@@ -65,4 +103,5 @@ export declare function disallowedToolIssues(tools: readonly string[], dialect:
65
103
  * is stripped to its base tool before checking.
66
104
  */
67
105
  export declare function verifyToolContract(tools: readonly string[], dialect: HarnessDialect): ToolIssue[];
106
+ export { scoredIssues, advisoryIssues, authoringIssues } from "./vocabulary.js";
68
107
  //# sourceMappingURL=tool-contract.d.ts.map