vigiles 10.0.0 → 11.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.
Files changed (38) hide show
  1. package/README.md +113 -83
  2. package/dist/adapters/claude-code/dialect.js +15 -0
  3. package/dist/audit-report.d.ts +1 -1
  4. package/dist/audit-report.template.html +1 -1
  5. package/dist/audit-score.d.ts +19 -12
  6. package/dist/audit-score.js +65 -11
  7. package/dist/cli.js +249 -0
  8. package/dist/core/CLAUDE.md.spec.d.ts +3 -0
  9. package/dist/core/CLAUDE.md.spec.js +26 -0
  10. package/dist/core/delegation-trifecta.d.ts +64 -0
  11. package/dist/core/delegation-trifecta.js +124 -0
  12. package/dist/core/dialect.d.ts +18 -0
  13. package/dist/core/hook-block-ineffective.d.ts +62 -0
  14. package/dist/core/hook-block-ineffective.js +153 -0
  15. package/dist/core/hook-matcher.d.ts +66 -0
  16. package/dist/core/hook-matcher.js +182 -0
  17. package/dist/core/hook-normalize.d.ts +43 -0
  18. package/dist/core/hook-normalize.js +78 -0
  19. package/dist/core/lethal-trifecta.d.ts +100 -0
  20. package/dist/core/lethal-trifecta.js +197 -0
  21. package/dist/core/plugin-dir-layout.d.ts +30 -0
  22. package/dist/core/plugin-dir-layout.js +73 -0
  23. package/dist/core/rule-meta.d.ts +82 -0
  24. package/dist/core/rule-meta.js +266 -0
  25. package/dist/core/skill-missing-fence.d.ts +47 -0
  26. package/dist/core/skill-missing-fence.js +119 -0
  27. package/dist/core/skill-resources.d.ts +27 -0
  28. package/dist/core/skill-resources.js +167 -0
  29. package/dist/core/types.d.ts +71 -0
  30. package/dist/core/validate.d.ts +1 -0
  31. package/dist/core/validate.js +26 -4
  32. package/dist/leaderboard.d.ts +1 -0
  33. package/dist/leaderboard.js +42 -4
  34. package/dist/scan.d.ts +106 -0
  35. package/dist/scan.js +251 -45
  36. package/dist/setup-plan.d.ts +6 -3
  37. package/dist/setup-plan.js +12 -2
  38. package/package.json +1 -1
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RULE_META = void 0;
4
+ exports.allRuleMeta = allRuleMeta;
5
+ exports.ruleMeta = ruleMeta;
6
+ /**
7
+ * The registry. `Record<RuleName, RuleMeta>` makes completeness a COMPILE-TIME
8
+ * guarantee: add a `RulesConfig` key without a meta here and `tsc` fails. The
9
+ * runtime test additionally binds this to `docs/rules/*.md` (the fs side a type
10
+ * can't see).
11
+ */
12
+ exports.RULE_META = {
13
+ // --- Spec & integrity (adoption / artifact-matches-source) ---------------
14
+ "require-instructions-spec": {
15
+ id: "require-instructions-spec",
16
+ bucket: "external-decidable",
17
+ surface: ["instruction"],
18
+ defaultSeverity: "warn",
19
+ summary: "A .spec.ts exists behind each CLAUDE.md / AGENTS.md.",
20
+ detector: "spec-presence (cli)",
21
+ },
22
+ "require-skill-spec": {
23
+ id: "require-skill-spec",
24
+ bucket: "external-decidable",
25
+ surface: ["skill"],
26
+ defaultSeverity: "off",
27
+ summary: "A .spec.ts exists behind each SKILL.md (opt-in).",
28
+ detector: "spec-presence (cli)",
29
+ },
30
+ integrity: {
31
+ id: "integrity",
32
+ bucket: "external-decidable",
33
+ surface: ["instruction"],
34
+ defaultSeverity: "warn",
35
+ summary: "A compiled instruction file still matches its spec's SHA-256.",
36
+ detector: "checkIntegrity",
37
+ upstreamPrevention: "compile stamps the hash; this catches later hand-edits",
38
+ },
39
+ coverage: {
40
+ id: "coverage",
41
+ bucket: "external-decidable",
42
+ surface: ["instruction"],
43
+ defaultSeverity: "off",
44
+ summary: "The spec covers ≥ threshold of linter rules / npm scripts.",
45
+ detector: "analyzeCoverage",
46
+ },
47
+ // --- Test coverage --------------------------------------------------------
48
+ "untested-skill": {
49
+ id: "untested-skill",
50
+ bucket: "external-decidable",
51
+ surface: ["skill"],
52
+ defaultSeverity: "warn",
53
+ summary: "A SKILL.md ships with a test or eval.",
54
+ detector: "findUntestedSurfaces",
55
+ },
56
+ "untested-subagent": {
57
+ id: "untested-subagent",
58
+ bucket: "external-decidable",
59
+ surface: ["subagent"],
60
+ defaultSeverity: "warn",
61
+ summary: "A subagent (agents/*.md) ships with a test or eval.",
62
+ detector: "findUntestedSurfaces",
63
+ },
64
+ "untested-hook": {
65
+ id: "untested-hook",
66
+ bucket: "external-decidable",
67
+ surface: ["hook"],
68
+ defaultSeverity: "warn",
69
+ summary: "A file-backed hook script ships with a test or eval.",
70
+ detector: "findUntestedSurfaces",
71
+ },
72
+ // --- Reference marking ----------------------------------------------------
73
+ "unmarked-refs": {
74
+ id: "unmarked-refs",
75
+ bucket: "heuristic-behavioral",
76
+ surface: ["instruction"],
77
+ defaultSeverity: "warn",
78
+ summary: "Code-shaped refs in an instruction file are marked (verifiable).",
79
+ detector: "collectRefIssues",
80
+ },
81
+ // --- Subagent contracts ---------------------------------------------------
82
+ "subagent-tool-contract": {
83
+ id: "subagent-tool-contract",
84
+ bucket: "structural-closed",
85
+ surface: ["subagent"],
86
+ defaultSeverity: "warn",
87
+ summary: "A subagent's tools: are all real (no never-available / typo).",
88
+ detector: "confidentToolIssues",
89
+ upstreamPrevention: "typed agent() vocabulary + compileAgent — an unknown tool is a tsc/compile error",
90
+ },
91
+ "disallowed-tools-contract": {
92
+ id: "disallowed-tools-contract",
93
+ bucket: "structural-closed",
94
+ surface: ["subagent"],
95
+ defaultSeverity: "warn",
96
+ summary: "A disallowedTools: entry isn't a typo that blocks nothing.",
97
+ detector: "disallowedToolIssues",
98
+ upstreamPrevention: "typed agent() vocabulary (a typo is a tsc error)",
99
+ },
100
+ "subagent-frontmatter": {
101
+ id: "subagent-frontmatter",
102
+ bucket: "structural-closed",
103
+ surface: ["subagent"],
104
+ defaultSeverity: "warn",
105
+ summary: "A subagent has required name/description + valid model/color.",
106
+ detector: "frontmatterIssuesFor",
107
+ upstreamPrevention: "compileAgent emits the required frontmatter",
108
+ },
109
+ // --- Hooks & MCP ----------------------------------------------------------
110
+ "hook-events": {
111
+ id: "hook-events",
112
+ bucket: "structural-closed",
113
+ surface: ["hook"],
114
+ defaultSeverity: "warn",
115
+ summary: "A hook's event name is one the harness defines (it can fire).",
116
+ detector: "confidentHookEventIssues",
117
+ upstreamPrevention: "compiled hook on: is dialect-validated at compile",
118
+ },
119
+ "hook-script-exists": {
120
+ id: "hook-script-exists",
121
+ bucket: "external-decidable",
122
+ surface: ["hook"],
123
+ defaultSeverity: "warn",
124
+ summary: "A hook command's script file exists on disk (it can run).",
125
+ detector: "scanHooks (status 'missing')",
126
+ upstreamPrevention: "a compiled hook is self-contained (no external script)",
127
+ },
128
+ "hook-block-ineffective": {
129
+ id: "hook-block-ineffective",
130
+ bucket: "structural-closed",
131
+ surface: ["hook"],
132
+ defaultSeverity: "warn",
133
+ summary: "A hook that looks like it blocks actually can (event + field).",
134
+ detector: "hookBlockIssues",
135
+ upstreamPrevention: "compiled hooks — deny compiles to exit 2; the field/exit code is never hand-written",
136
+ },
137
+ "hook-matcher": {
138
+ id: "hook-matcher",
139
+ bucket: "structural-closed",
140
+ surface: ["hook"],
141
+ defaultSeverity: "warn",
142
+ summary: "A hook matcher fires (no tool-name typo / malformed MCP form).",
143
+ detector: "hookMatcherIssues",
144
+ upstreamPrevention: "compiled hook tool()/tools() matcher is typed",
145
+ },
146
+ "prefer-compiled-hooks": {
147
+ id: "prefer-compiled-hooks",
148
+ bucket: "heuristic-behavioral",
149
+ surface: ["hook"],
150
+ defaultSeverity: "off",
151
+ summary: "Recommends compiled hooks over hand-written shell (one nudge).",
152
+ detector: "scanHooks (manualHookCount)",
153
+ },
154
+ "mcp-config": {
155
+ id: "mcp-config",
156
+ bucket: "structural-closed",
157
+ surface: ["mcp"],
158
+ defaultSeverity: "warn",
159
+ summary: "A declared MCP server can start (has a command or url).",
160
+ detector: "verifyMcpServers",
161
+ },
162
+ "mcp-tool-resolves": {
163
+ id: "mcp-tool-resolves",
164
+ bucket: "external-decidable",
165
+ surface: ["subagent", "mcp"],
166
+ defaultSeverity: "warn",
167
+ summary: "A subagent's mcp__server__tool names a declared server.",
168
+ detector: "verifyMcpToolServers",
169
+ },
170
+ "mcp-hook-target-resolves": {
171
+ id: "mcp-hook-target-resolves",
172
+ bucket: "external-decidable",
173
+ surface: ["hook", "mcp"],
174
+ defaultSeverity: "warn",
175
+ summary: "A type:mcp_tool hook action is complete + names a declared server.",
176
+ detector: "verifyMcpHookTargets",
177
+ },
178
+ // --- Skill triggers -------------------------------------------------------
179
+ "skill-frontmatter": {
180
+ id: "skill-frontmatter",
181
+ bucket: "structural-closed",
182
+ surface: ["skill"],
183
+ defaultSeverity: "warn",
184
+ summary: "A SKILL.md declares an explicit name + description (reliable trigger).",
185
+ detector: "skillMetaIssuesFor",
186
+ upstreamPrevention: "skill() compiler emits name + description",
187
+ },
188
+ "skill-missing-fence": {
189
+ id: "skill-missing-fence",
190
+ bucket: "structural-closed",
191
+ surface: ["skill"],
192
+ defaultSeverity: "warn",
193
+ summary: "A SKILL.md opens with a --- fence (else it loads as inert body).",
194
+ detector: "skillMissingFence",
195
+ upstreamPrevention: "skill() compiler always emits the fence",
196
+ },
197
+ "description-overlap": {
198
+ id: "description-overlap",
199
+ bucket: "heuristic-behavioral",
200
+ surface: ["skill"],
201
+ defaultSeverity: "warn",
202
+ summary: "Two model-invocable skills aren't near-identical (wrong one fires).",
203
+ detector: "findDescriptionOverlaps",
204
+ },
205
+ "frontmatter-valid": {
206
+ id: "frontmatter-valid",
207
+ bucket: "heuristic-behavioral",
208
+ surface: ["skill", "subagent"],
209
+ defaultSeverity: "warn",
210
+ summary: "A --- block parses as YAML (js-yaml is stricter than some loaders).",
211
+ detector: "malformedFrontmatterFor",
212
+ upstreamPrevention: "the spec compiler emits valid frontmatter",
213
+ },
214
+ "skill-resource-resolves": {
215
+ id: "skill-resource-resolves",
216
+ bucket: "external-decidable",
217
+ surface: ["skill"],
218
+ defaultSeverity: "warn",
219
+ summary: "A SKILL.md's bundled-file reference exists under the skill dir.",
220
+ detector: "skillResourceIssues",
221
+ },
222
+ // --- Plugin layout & safety ----------------------------------------------
223
+ "plugin-dir-layout": {
224
+ id: "plugin-dir-layout",
225
+ bucket: "external-decidable",
226
+ surface: ["plugin"],
227
+ defaultSeverity: "warn",
228
+ summary: "No functional surface dir is nested inside the manifest dir.",
229
+ detector: "pluginDirLayoutIssues",
230
+ upstreamPrevention: "init scaffolds surfaces at the plugin root",
231
+ },
232
+ "lethal-trifecta": {
233
+ id: "lethal-trifecta",
234
+ bucket: "structural-closed",
235
+ surface: ["skill", "subagent"],
236
+ defaultSeverity: "warn",
237
+ summary: "No unit's tools hold all three lethal-trifecta legs at once.",
238
+ detector: "lethalTrifectaIssues",
239
+ },
240
+ "delegation-trifecta": {
241
+ id: "delegation-trifecta",
242
+ bucket: "structural-closed",
243
+ surface: ["subagent"],
244
+ defaultSeverity: "warn",
245
+ summary: "No subagent's (own ∪ delegated) capability forms a trifecta.",
246
+ detector: "delegationTrifectaIssues",
247
+ },
248
+ // --- Docs hygiene ---------------------------------------------------------
249
+ "orphan-docs": {
250
+ id: "orphan-docs",
251
+ bucket: "external-decidable",
252
+ surface: ["docs"],
253
+ defaultSeverity: "warn",
254
+ summary: "Every docs/ + research/ .md is referenced by another .md.",
255
+ detector: "findOrphanDocs",
256
+ },
257
+ };
258
+ /** All declared rule metas, as a list. */
259
+ function allRuleMeta() {
260
+ return Object.values(exports.RULE_META);
261
+ }
262
+ /** Look up one rule's meta (undefined for an unknown id). */
263
+ function ruleMeta(id) {
264
+ return exports.RULE_META[id];
265
+ }
266
+ //# sourceMappingURL=rule-meta.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * vigiles — SKILL.md missing-fence detector (the cross-reference moat applied
3
+ * to SKILL.md frontmatter structure).
4
+ *
5
+ * A SKILL.md that begins with frontmatter-looking keys (`name:`, `description:`,
6
+ * `allowed-tools:`, etc.) but is MISSING the opening `---` fence is a very
7
+ * common real-world pain: the harness loads the whole file as plain body text —
8
+ * no name, no description, no tool list — so the skill is INVISIBLE and never
9
+ * fires. The model-selector has nothing to match against and the agent can never
10
+ * find the skill at all.
11
+ *
12
+ * HIGH-PRECISION / FP-SAFE, by the same don't-cry-wolf discipline as the rest of
13
+ * vigiles (see `danglingRefs`, `confidentToolIssues`): we flag ONLY when the
14
+ * first non-blank, non-comment line matches a KNOWN frontmatter key at column 0
15
+ * from an explicit whitelist (`name`, `description`, `allowed-tools`, `tools`,
16
+ * `model`, `color`, `disable-model-invocation`, `argument-hint`, `version`,
17
+ * `license`, `metadata`). A random prose line like `Note: something` is never
18
+ * flagged — the whitelist is what keeps the check FP-safe. Prefer MISSING a
19
+ * real finding over emitting a false positive.
20
+ *
21
+ * Pure: no IO. Input is the raw SKILL.md content string.
22
+ */
23
+ /**
24
+ * A finding returned when a SKILL.md is missing its opening `---` fence but
25
+ * begins with what looks like a frontmatter key.
26
+ */
27
+ export interface SkillFenceFinding {
28
+ /** The frontmatter-looking key that appears unfenced (e.g. `"name"`). */
29
+ readonly key: string;
30
+ /** 1-based line number of the first unfenced key in the content string. */
31
+ readonly line: number;
32
+ /** Human-readable explanation and fix. */
33
+ readonly message: string;
34
+ }
35
+ /**
36
+ * Detect a SKILL.md body that begins with a known frontmatter key at column 0
37
+ * but is missing the required opening `---` fence.
38
+ *
39
+ * Returns a {@link SkillFenceFinding} describing the unfenced key, or `null`
40
+ * when no problem is detected (the file is correctly fenced, starts with prose,
41
+ * or is empty).
42
+ *
43
+ * Pure — no IO. The shared detector behind both `vigiles lint`
44
+ * (`skill-missing-fence` rule) and `vigiles audit` — one detector, no drift.
45
+ */
46
+ export declare function skillMissingFence(skillBody: string): SkillFenceFinding | null;
47
+ //# sourceMappingURL=skill-missing-fence.d.ts.map
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles — SKILL.md missing-fence detector (the cross-reference moat applied
4
+ * to SKILL.md frontmatter structure).
5
+ *
6
+ * A SKILL.md that begins with frontmatter-looking keys (`name:`, `description:`,
7
+ * `allowed-tools:`, etc.) but is MISSING the opening `---` fence is a very
8
+ * common real-world pain: the harness loads the whole file as plain body text —
9
+ * no name, no description, no tool list — so the skill is INVISIBLE and never
10
+ * fires. The model-selector has nothing to match against and the agent can never
11
+ * find the skill at all.
12
+ *
13
+ * HIGH-PRECISION / FP-SAFE, by the same don't-cry-wolf discipline as the rest of
14
+ * vigiles (see `danglingRefs`, `confidentToolIssues`): we flag ONLY when the
15
+ * first non-blank, non-comment line matches a KNOWN frontmatter key at column 0
16
+ * from an explicit whitelist (`name`, `description`, `allowed-tools`, `tools`,
17
+ * `model`, `color`, `disable-model-invocation`, `argument-hint`, `version`,
18
+ * `license`, `metadata`). A random prose line like `Note: something` is never
19
+ * flagged — the whitelist is what keeps the check FP-safe. Prefer MISSING a
20
+ * real finding over emitting a false positive.
21
+ *
22
+ * Pure: no IO. Input is the raw SKILL.md content string.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.skillMissingFence = skillMissingFence;
26
+ // ---------------------------------------------------------------------------
27
+ // Constants (FP-safety whitelist)
28
+ // ---------------------------------------------------------------------------
29
+ /**
30
+ * The known SKILL.md / agent-harness frontmatter keys. A line that matches one
31
+ * of these at column 0 is flagged when there is no opening `---` fence. Any
32
+ * key NOT in this list is treated as prose and silently skipped.
33
+ */
34
+ const KNOWN_KEYS = [
35
+ "name",
36
+ "description",
37
+ "allowed-tools",
38
+ "tools",
39
+ "model",
40
+ "color",
41
+ "disable-model-invocation",
42
+ "argument-hint",
43
+ "version",
44
+ "license",
45
+ "metadata",
46
+ ];
47
+ /**
48
+ * Pre-built regex: matches `<known-key>:` at column 0, optionally followed by
49
+ * whitespace or end-of-line. Capturing group 1 holds the matched key name.
50
+ */
51
+ const KNOWN_KEY_RE = new RegExp(`^(${KNOWN_KEYS.join("|")})\\s*:`);
52
+ /**
53
+ * Lines that are unambiguously prose or markdown structure — NOT frontmatter.
54
+ * When the first meaningful line starts with any of these, return null
55
+ * immediately without even testing the key whitelist.
56
+ */
57
+ const PROSE_START_RE = /^(?:#|>|-|\*|`|<|\d+\.|[A-Za-z]{4,}(?:\s|$))/;
58
+ /**
59
+ * A vigiles integrity / meta comment that may legitimately precede frontmatter.
60
+ * We skip it so `name:` on line 2 (after the comment on line 1) is still found.
61
+ */
62
+ const VIGILES_COMMENT_RE = /^<!--\s*vigiles:/;
63
+ // ---------------------------------------------------------------------------
64
+ // Detector
65
+ // ---------------------------------------------------------------------------
66
+ /**
67
+ * Detect a SKILL.md body that begins with a known frontmatter key at column 0
68
+ * but is missing the required opening `---` fence.
69
+ *
70
+ * Returns a {@link SkillFenceFinding} describing the unfenced key, or `null`
71
+ * when no problem is detected (the file is correctly fenced, starts with prose,
72
+ * or is empty).
73
+ *
74
+ * Pure — no IO. The shared detector behind both `vigiles lint`
75
+ * (`skill-missing-fence` rule) and `vigiles audit` — one detector, no drift.
76
+ */
77
+ function skillMissingFence(skillBody) {
78
+ // Strip a leading UTF-8 BOM (U+FEFF) if present.
79
+ const content = skillBody.startsWith("") ? skillBody.slice(1) : skillBody;
80
+ const lines = content.split("\n");
81
+ let firstMeaningfulLine = null;
82
+ let firstMeaningfulLineNo = 0; // 1-based
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const raw = lines[i];
85
+ const trimmed = raw.trimEnd();
86
+ // Skip blank lines.
87
+ if (trimmed.trim() === "")
88
+ continue;
89
+ // Skip a leading vigiles integrity/meta comment (`<!-- vigiles:... -->`).
90
+ if (VIGILES_COMMENT_RE.test(trimmed.trim()))
91
+ continue;
92
+ firstMeaningfulLine = trimmed;
93
+ firstMeaningfulLineNo = i + 1; // convert to 1-based
94
+ break;
95
+ }
96
+ // Empty or all-blank file — nothing to flag.
97
+ if (firstMeaningfulLine === null)
98
+ return null;
99
+ // A proper opening fence — correctly structured, nothing to flag.
100
+ if (firstMeaningfulLine.startsWith("---"))
101
+ return null;
102
+ // Unambiguous prose / markdown constructs — not a frontmatter key.
103
+ if (PROSE_START_RE.test(firstMeaningfulLine))
104
+ return null;
105
+ // Check against the known-key whitelist (the FP-safety gate).
106
+ const match = KNOWN_KEY_RE.exec(firstMeaningfulLine);
107
+ if (!match)
108
+ return null;
109
+ const key = match[1];
110
+ return {
111
+ key,
112
+ line: firstMeaningfulLineNo,
113
+ message: `SKILL.md is missing its opening \`---\` frontmatter fence: ` +
114
+ `\`${key}:\` on line ${firstMeaningfulLineNo} is loaded as body text, ` +
115
+ `so the skill has no name, description, or trigger and will never fire. ` +
116
+ `Wrap the metadata block in \`---\` … \`---\`.`,
117
+ };
118
+ }
119
+ //# sourceMappingURL=skill-missing-fence.js.map
@@ -0,0 +1,27 @@
1
+ /** How a missing bundled-resource reference was found in the body. */
2
+ export type SkillResourceKind = "link" | "path";
3
+ /** A SKILL.md body reference to a local bundled file that doesn't exist on disk. */
4
+ export interface SkillResourceFinding {
5
+ /** The reference text exactly as written in the body (e.g. `scripts/run.sh`). */
6
+ readonly ref: string;
7
+ /** The path normalized relative to the skill dir, used for resolution. */
8
+ readonly resolved: string;
9
+ /** Whether the ref came from a markdown link `[..](..)` or an inline/path mention. */
10
+ readonly kind: SkillResourceKind;
11
+ /** 1-based source line of the reference in the body. */
12
+ readonly line: number;
13
+ }
14
+ export interface SkillResourceOptions {
15
+ /** Injectable existence check (default: node:fs existsSync). */
16
+ readonly existsSync?: (p: string) => boolean;
17
+ }
18
+ /**
19
+ * The bundled-resource references in a SKILL.md body that don't resolve on disk
20
+ * under `skillDir`. Pure + FP-safe (see the module header). `skillDir` is the
21
+ * directory the SKILL.md itself lives in (resources are bundled beside it).
22
+ *
23
+ * The shared detector behind both `vigiles lint` (the `skill-resource-resolves`
24
+ * rule) and `vigiles audit` (the read-only report) — one detector, no drift.
25
+ */
26
+ export declare function skillResourceIssues(skillBody: string, skillDir: string, opts?: SkillResourceOptions): SkillResourceFinding[];
27
+ //# sourceMappingURL=skill-resources.d.ts.map
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.skillResourceIssues = skillResourceIssues;
4
+ /**
5
+ * vigiles — SKILL bundled-resource resolution (the cross-reference moat applied
6
+ * to a SKILL.md body).
7
+ *
8
+ * A SKILL.md body is freeform markdown that routinely points the agent at LOCAL
9
+ * BUNDLED files shipped beside it — `scripts/foo.sh`, `references/api.md`,
10
+ * `[setup](./scripts/run.py)`, an inline `run \`scripts/setup.sh\``. When a
11
+ * referenced file doesn't exist on disk under the skill directory, the agent
12
+ * reads the instruction, gets nothing, and silently continues (a documented top
13
+ * skill pain — one practitioner found 59 broken refs across 192 files). vigiles
14
+ * already verifies file/script refs inside typed specs (core/refs.ts,
15
+ * core/doc-refs.ts) and intra-plugin script refs in the loader
16
+ * (plugin-loader.ts `danglingRefs`); this extends that to the SKILL.md body.
17
+ *
18
+ * HIGH-PRECISION / FP-SAFE, by the same don't-cry-wolf discipline the rest of
19
+ * vigiles holds (see `danglingRefs`/`isPluginRooted`): we flag ONLY references
20
+ * that are UNAMBIGUOUSLY a local bundled resource — a markdown link to a
21
+ * relative path with a file extension, or an explicit `scripts/`/`references/`/
22
+ * `assets/`-prefixed path (the Agent-Skills standard bundle dirs) with an
23
+ * extension. Everything else is skipped: URLs, absolute paths, `${VAR}`/`$VAR`
24
+ * tokens, `../` escapes, bare words with no extension or known prefix. Prefer
25
+ * MISSING a real ref over emitting a false positive — a noisy resource check
26
+ * would teach users to ignore it.
27
+ *
28
+ * Pure: the only IO is an injectable `existsSync` (default node:fs), mirroring
29
+ * core/refs.ts and the loader so the detector is testable with a fake.
30
+ */
31
+ const node_fs_1 = require("node:fs");
32
+ const node_path_1 = require("node:path");
33
+ // ---------------------------------------------------------------------------
34
+ // Shapes we match vs deliberately skip (FP-safety)
35
+ // ---------------------------------------------------------------------------
36
+ const FENCE = /^\s*(?:`{3,}|~{3,})/;
37
+ // The Agent-Skills standard bundle subdirectories. A path PREFIXED by one of
38
+ // these is unambiguously a local bundled resource, even without a `./`.
39
+ const BUNDLE_DIRS = ["scripts", "references", "assets"];
40
+ const BUNDLE_PREFIX = new RegExp(`^(?:${BUNDLE_DIRS.join("|")})/`);
41
+ // A markdown inline link `[text](target)` — we read its target.
42
+ const MD_LINK = /\[[^\]]*\]\(([^)\s]+)\)/g;
43
+ // An inline-code path mention: a backtick span whose whole content is a single
44
+ // path token. We only treat it as a ref when it is a bundle-dir-prefixed path
45
+ // with an extension (the high-confidence shape); a bare `scripts` or a generic
46
+ // `foo.ts` mention is NOT flagged.
47
+ const INLINE_SPAN = /`([^`\n]+)`/g;
48
+ // A path must carry a file extension to be a resource reference. A bare word or
49
+ // a directory name (`scripts/lib`) is undecidable prose — skipped.
50
+ const HAS_EXT = /\.[A-Za-z0-9]+$/;
51
+ /**
52
+ * A reference target is a LOCAL BUNDLED RESOURCE worth resolving iff it is a
53
+ * relative path with a file extension AND is not one of the skip shapes. This is
54
+ * the single gate; both the link path and the inline-path path run through it.
55
+ */
56
+ function localResourceTarget(rawTarget) {
57
+ // Strip a markdown link title / fragment / query if present, and trim.
58
+ const target = rawTarget.trim();
59
+ if (target.length === 0)
60
+ return null;
61
+ // SKIP: URLs (http://, https://, mailto:, any scheme://) — external.
62
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || /^mailto:/i.test(target)) {
63
+ return null;
64
+ }
65
+ // SKIP: a pure anchor / fragment-only link (`#section`).
66
+ if (target.startsWith("#"))
67
+ return null;
68
+ // SKIP: absolute paths (`/etc/x`, Windows `C:\`) — not bundled-relative.
69
+ if (target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target))
70
+ return null;
71
+ // SKIP: variable tokens (`${CLAUDE_PLUGIN_ROOT}/x`, `$VAR/x`) — uncheckable,
72
+ // and almost always a plugin-root or runtime path, not a bundled file.
73
+ if (target.includes("$"))
74
+ return null;
75
+ // Drop a URL fragment / query suffix so `references/api.md#auth` resolves to
76
+ // the file. (Only after the scheme check above, so we never mangle a URL.)
77
+ const path = target.replace(/[?#].*$/, "");
78
+ if (path.length === 0)
79
+ return null;
80
+ // SKIP: a `../` escape OUT of the skill dir — undecidable / not a bundled
81
+ // resource (it points at a sibling skill or the repo). A leading `./` is fine.
82
+ const normalized = path.replace(/^\.\//, "");
83
+ if (normalized.startsWith("../") || normalized.includes("/../"))
84
+ return null;
85
+ // Must look like a file (have an extension), else it's a dir/prose mention.
86
+ if (!HAS_EXT.test(normalized))
87
+ return null;
88
+ return normalized;
89
+ }
90
+ /**
91
+ * Whether an inline-code path token is high-confidence enough to flag on its
92
+ * own (no surrounding `[..](..)` link syntax). We require a BUNDLE-DIR PREFIX
93
+ * (`scripts/`, `references/`, `assets/`) so a generic `` `config.json` `` or a
94
+ * `` `src/foo.ts` `` API mention in prose is never flagged — only the standard
95
+ * bundle layout, which is unambiguously a shipped resource.
96
+ */
97
+ function isInlineBundlePath(token) {
98
+ const t = token.trim();
99
+ // A single token only — a span with spaces is a command/prose, not a path.
100
+ if (/\s/.test(t))
101
+ return false;
102
+ const normalized = t.replace(/^\.\//, "");
103
+ return BUNDLE_PREFIX.test(normalized) && HAS_EXT.test(normalized);
104
+ }
105
+ /** Collect candidate bundled-resource refs from one body line, skipping fences. */
106
+ function candidatesInLine(line, lineNo) {
107
+ const out = [];
108
+ // Markdown links: any relative path target that passes the local-resource gate.
109
+ for (const m of line.matchAll(MD_LINK)) {
110
+ const resolved = localResourceTarget(m[1]);
111
+ if (resolved !== null) {
112
+ out.push({ ref: m[1].trim(), resolved, kind: "link", line: lineNo });
113
+ }
114
+ }
115
+ // Inline-code path mentions: only the high-confidence bundle-dir-prefixed form.
116
+ for (const m of line.matchAll(INLINE_SPAN)) {
117
+ const token = m[1].trim();
118
+ if (!isInlineBundlePath(token))
119
+ continue;
120
+ const resolved = localResourceTarget(token);
121
+ if (resolved !== null) {
122
+ out.push({ ref: token, resolved, kind: "path", line: lineNo });
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+ /**
128
+ * The bundled-resource references in a SKILL.md body that don't resolve on disk
129
+ * under `skillDir`. Pure + FP-safe (see the module header). `skillDir` is the
130
+ * directory the SKILL.md itself lives in (resources are bundled beside it).
131
+ *
132
+ * The shared detector behind both `vigiles lint` (the `skill-resource-resolves`
133
+ * rule) and `vigiles audit` (the read-only report) — one detector, no drift.
134
+ */
135
+ function skillResourceIssues(skillBody, skillDir, opts = {}) {
136
+ const exists = opts.existsSync ?? node_fs_1.existsSync;
137
+ const findings = [];
138
+ const seen = new Set();
139
+ const lines = skillBody.split("\n");
140
+ let inFence = false;
141
+ for (let i = 0; i < lines.length; i++) {
142
+ if (FENCE.test(lines[i])) {
143
+ inFence = !inFence;
144
+ continue;
145
+ }
146
+ if (inFence)
147
+ continue;
148
+ for (const c of candidatesInLine(lines[i], i + 1)) {
149
+ const full = (0, node_path_1.resolve)(skillDir, c.resolved);
150
+ if (exists(full))
151
+ continue;
152
+ // De-dupe the same missing file referenced several times in the body.
153
+ const key = `${c.kind}:${c.resolved}`;
154
+ if (seen.has(key))
155
+ continue;
156
+ seen.add(key);
157
+ findings.push({
158
+ ref: c.ref,
159
+ resolved: c.resolved,
160
+ kind: c.kind,
161
+ line: c.line,
162
+ });
163
+ }
164
+ }
165
+ return findings;
166
+ }
167
+ //# sourceMappingURL=skill-resources.js.map