vigiles 9.1.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 (47) hide show
  1. package/README.md +126 -112
  2. package/dist/adapters/claude-code/dialect.js +15 -0
  3. package/dist/audit-html.d.ts +15 -4
  4. package/dist/audit-html.js +15 -6
  5. package/dist/audit-report.d.ts +58 -2
  6. package/dist/audit-report.js +29 -0
  7. package/dist/audit-report.template.html +34 -24
  8. package/dist/audit-score.d.ts +19 -12
  9. package/dist/audit-score.js +79 -15
  10. package/dist/audit-serve.d.ts +109 -0
  11. package/dist/audit-serve.js +257 -0
  12. package/dist/cli.js +435 -20
  13. package/dist/core/CLAUDE.md.spec.d.ts +3 -0
  14. package/dist/core/CLAUDE.md.spec.js +26 -0
  15. package/dist/core/compile.d.ts +5 -1
  16. package/dist/core/compile.js +19 -10
  17. package/dist/core/delegation-trifecta.d.ts +64 -0
  18. package/dist/core/delegation-trifecta.js +124 -0
  19. package/dist/core/dialect.d.ts +18 -0
  20. package/dist/core/hook-block-ineffective.d.ts +62 -0
  21. package/dist/core/hook-block-ineffective.js +153 -0
  22. package/dist/core/hook-matcher.d.ts +66 -0
  23. package/dist/core/hook-matcher.js +182 -0
  24. package/dist/core/hook-normalize.d.ts +43 -0
  25. package/dist/core/hook-normalize.js +78 -0
  26. package/dist/core/lethal-trifecta.d.ts +100 -0
  27. package/dist/core/lethal-trifecta.js +197 -0
  28. package/dist/core/plugin-dir-layout.d.ts +30 -0
  29. package/dist/core/plugin-dir-layout.js +73 -0
  30. package/dist/core/rule-meta.d.ts +82 -0
  31. package/dist/core/rule-meta.js +266 -0
  32. package/dist/core/skill-missing-fence.d.ts +47 -0
  33. package/dist/core/skill-missing-fence.js +119 -0
  34. package/dist/core/skill-resources.d.ts +27 -0
  35. package/dist/core/skill-resources.js +167 -0
  36. package/dist/core/types.d.ts +71 -0
  37. package/dist/core/validate.d.ts +1 -0
  38. package/dist/core/validate.js +26 -4
  39. package/dist/leaderboard.d.ts +1 -0
  40. package/dist/leaderboard.js +64 -15
  41. package/dist/scan-behavioral.d.ts +85 -0
  42. package/dist/scan-behavioral.js +225 -0
  43. package/dist/scan.d.ts +106 -0
  44. package/dist/scan.js +269 -53
  45. package/dist/setup-plan.d.ts +6 -3
  46. package/dist/setup-plan.js +12 -2
  47. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pluginDirLayoutIssues = pluginDirLayoutIssues;
4
+ /**
5
+ * vigiles — plugin manifest directory layout verification.
6
+ *
7
+ * The #1 plugin-author mistake (OSS pain #E1): placing functional surface
8
+ * directories (skills/, agents/, commands/, hooks/) INSIDE the `.claude-plugin/`
9
+ * manifest directory instead of at the plugin root. The harness resolves surface
10
+ * dirs relative to the PLUGIN ROOT (where the agent is launched), not relative to
11
+ * the manifest directory — so a `skills/` nested inside `.claude-plugin/` is
12
+ * completely invisible to the harness. Only `plugin.json` belongs inside the
13
+ * manifest directory; everything else must live at the root.
14
+ *
15
+ * Pure + FP-safe: the only IO is an injectable `existsSync` and `isDirectory`
16
+ * (defaults: node:fs), mirroring the pattern in core/skill-resources.ts and the
17
+ * loader, so the detector is fully testable with fakes and never touches the
18
+ * filesystem in tests.
19
+ *
20
+ * Harness-agnostic: the surface directory names are INJECTED from the layout
21
+ * (PluginLayout.skillDir / agentDir / commandDir / hookDir, or equivalent), never
22
+ * hard-coded here. ONE detector reused by both `vigiles lint` (the
23
+ * `plugin-dir-layout` rule) and `vigiles audit` (the read-only report) — one
24
+ * detector, no drift.
25
+ */
26
+ const node_fs_1 = require("node:fs");
27
+ const node_path_1 = require("node:path");
28
+ // ---------------------------------------------------------------------------
29
+ // Detector
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Default `isDirectory` implementation — wraps statSync so that a missing or
33
+ * unreadable path returns `false` instead of throwing.
34
+ */
35
+ function defaultIsDirectory(p) {
36
+ try {
37
+ return (0, node_fs_1.statSync)(p).isDirectory();
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ /**
44
+ * Surface directories found nested INSIDE the manifest directory, where they are
45
+ * invisible to the harness.
46
+ *
47
+ * `manifestDir` is the absolute path to the manifest directory (e.g. the repo's
48
+ * `.claude-plugin/`). `surfaceDirNames` are the harness's functional surface
49
+ * directory names, injected from the layout (e.g. `["skills","agents","commands",
50
+ * "hooks"]`) so the detector stays harness-agnostic — NEVER hard-code them.
51
+ *
52
+ * Returns `[]` when the manifest dir doesn't exist or holds no misplaced surface
53
+ * dirs.
54
+ */
55
+ function pluginDirLayoutIssues(manifestDir, surfaceDirNames, opts) {
56
+ const exists = opts?.existsSync ?? node_fs_1.existsSync;
57
+ const isDir = opts?.isDirectory ?? defaultIsDirectory;
58
+ const manifestBase = (0, node_path_1.basename)(manifestDir);
59
+ const findings = [];
60
+ for (const name of surfaceDirNames) {
61
+ const candidate = (0, node_path_1.join)(manifestDir, name);
62
+ if (exists(candidate) && isDir(candidate)) {
63
+ findings.push({
64
+ dir: name,
65
+ message: `\`${name}/\` lives inside the \`${manifestBase}/\` manifest directory ` +
66
+ `where the harness can't see it — only \`plugin.json\` belongs there; ` +
67
+ `move \`${name}/\` to the plugin root.`,
68
+ });
69
+ }
70
+ }
71
+ return findings;
72
+ }
73
+ //# sourceMappingURL=plugin-dir-layout.js.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * vigiles — the RULE METADATA registry (the ESLint-`meta` pattern, adapted).
3
+ *
4
+ * Every deterministic check vigiles ships is DECLARED here with the one fact that
5
+ * dissolves the "why is this a warning / can't this be a type?" confusion: its
6
+ * DECIDABILITY BUCKET, which sets the strongest enforcement the defect can ever
7
+ * reach. See `research/enforcement-model.md` for the full model and
8
+ * `lint-rule-calibration` (root CLAUDE.md) for the governance rule.
9
+ *
10
+ * WHY A CENTRAL REGISTRY, NOT CO-LOCATED `export const meta`. ESLint co-locates
11
+ * meta with each rule because there 1 rule = 1 module. vigiles deliberately
12
+ * SHARES detectors (one-detector-no-drift: a single pure function feeds both
13
+ * `lint` and `audit`, and `scan.ts` computes many rules at once), so co-locating
14
+ * would scatter metas across files that each own several rules — the very
15
+ * fragmentation we're removing. ONE registry keyed by rule name is the honest
16
+ * single source (mirrors `cli-commands.ts` for verbs); the `detector` field names
17
+ * the pure function so traceability survives.
18
+ *
19
+ * The bucket is the CEILING; `defaultSeverity` is where the rule sits TODAY. A
20
+ * gap between them is meaningful: a bucket-A/B rule at `warn` is a CANDIDATE for
21
+ * promotion to `error` (deterministic, just rolling out), whereas a bucket-C rule
22
+ * at `warn` is permanent (forcing it to `error` would cry wolf).
23
+ */
24
+ import type { RulesConfig } from "./types.js";
25
+ /**
26
+ * The decidability class of a defect — the single fact that sets its ceiling.
27
+ *
28
+ * - `structural-closed` — decidable from the artifact's own content over a CLOSED
29
+ * vocabulary; a TYPE could make it impossible for authors who route through the
30
+ * typed constructor. Ceiling: unrepresentable / won't-typecheck.
31
+ * - `external-decidable` — decidable, but needs the EXTERNAL world (the
32
+ * filesystem, a linter catalog, another file/server). No type can read those,
33
+ * so the ceiling is a hard ERROR at compile-cross-ref or lint — never a type.
34
+ * - `heuristic-behavioral` — undecidable or a fuzzy proxy ("are these too
35
+ * similar?", "does this fire?"). Ceiling: a WARNING or a model-MEASUREMENT. By
36
+ * the math, not by laziness; an `error` here would cry wolf.
37
+ */
38
+ export type RuleBucket = "structural-closed" | "external-decidable" | "heuristic-behavioral";
39
+ /** The artifact surface a rule reads (coarse; a rule may span several). */
40
+ export type RuleSurface = "instruction" | "skill" | "subagent" | "hook" | "mcp" | "plugin" | "docs";
41
+ /** Where a rule sits by default — `"off"` is the normalized form of `false`. */
42
+ export type RuleDefaultSeverity = "error" | "warn" | "off";
43
+ /** Every named rule: the `RulesConfig` keys plus the built-in `orphan-docs`. */
44
+ export type RuleName = keyof RulesConfig | "orphan-docs";
45
+ /**
46
+ * The declared shape of one rule — co-located metadata in the ESLint `meta`
47
+ * sense, gathered into one registry because vigiles shares detectors.
48
+ */
49
+ export interface RuleMeta {
50
+ /** Canonical rule id (a `RulesConfig` key or `orphan-docs`). */
51
+ readonly id: RuleName;
52
+ /** The decidability class → the rule's strongest possible ceiling. */
53
+ readonly bucket: RuleBucket;
54
+ /** The artifact surface(s) the rule reads (non-empty). */
55
+ readonly surface: readonly RuleSurface[];
56
+ /** Where the rule sits by default today (≤ the bucket's ceiling). */
57
+ readonly defaultSeverity: RuleDefaultSeverity;
58
+ /** One-line "what it checks" (tracks the docs matrix row). */
59
+ readonly summary: string;
60
+ /** The shared pure detector function (one-detector-no-drift traceability). */
61
+ readonly detector: string;
62
+ /**
63
+ * The upstream construct that makes this SAME defect impossible for authors who
64
+ * route through it — a TYPE (structural-closed) or a COMPILE cross-ref. Absent
65
+ * when no construct path exists for the surface, the prevention isn't shipped
66
+ * yet, or the defect is undecidable (heuristic-behavioral). The lint rule is
67
+ * always the artifact-time floor regardless.
68
+ */
69
+ readonly upstreamPrevention?: string;
70
+ }
71
+ /**
72
+ * The registry. `Record<RuleName, RuleMeta>` makes completeness a COMPILE-TIME
73
+ * guarantee: add a `RulesConfig` key without a meta here and `tsc` fails. The
74
+ * runtime test additionally binds this to `docs/rules/*.md` (the fs side a type
75
+ * can't see).
76
+ */
77
+ export declare const RULE_META: Record<RuleName, RuleMeta>;
78
+ /** All declared rule metas, as a list. */
79
+ export declare function allRuleMeta(): RuleMeta[];
80
+ /** Look up one rule's meta (undefined for an unknown id). */
81
+ export declare function ruleMeta(id: string): RuleMeta | undefined;
82
+ //# sourceMappingURL=rule-meta.d.ts.map
@@ -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