vigiles 5.0.1 → 5.2.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 (72) hide show
  1. package/README.md +15 -9
  2. package/dist/adapters/claude-code/adapter.js +1 -0
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +30 -6
  4. package/dist/adapters/claude-code/agent-runtime.js +66 -37
  5. package/dist/adapters/claude-code/dialect.js +37 -2
  6. package/dist/adapters/claude-code/effect-region.d.ts +9 -0
  7. package/dist/adapters/claude-code/effect-region.js +45 -0
  8. package/dist/adapters/claude-code/layout.js +3 -0
  9. package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
  10. package/dist/adapters/claude-code/skill-runtime.js +48 -0
  11. package/dist/adapters/codex/adapter.js +3 -0
  12. package/dist/adapters/codex/eval.d.ts +94 -0
  13. package/dist/adapters/codex/eval.js +227 -0
  14. package/dist/adapters/codex/layout.js +3 -0
  15. package/dist/adapters/opencode/adapter.js +1 -0
  16. package/dist/adapters/opencode/layout.js +3 -0
  17. package/dist/check.d.ts +8 -0
  18. package/dist/check.js +27 -3
  19. package/dist/cli.js +712 -21
  20. package/dist/codex.d.ts +1 -0
  21. package/dist/codex.js +3 -0
  22. package/dist/core/adapter.d.ts +10 -0
  23. package/dist/core/bash-effects.d.ts +41 -0
  24. package/dist/core/bash-effects.js +405 -0
  25. package/dist/core/compile.d.ts +3 -1
  26. package/dist/core/compile.js +169 -74
  27. package/dist/core/description-overlap.d.ts +27 -0
  28. package/dist/core/description-overlap.js +53 -0
  29. package/dist/core/dialect.d.ts +18 -0
  30. package/dist/core/effects.d.ts +172 -0
  31. package/dist/core/effects.js +245 -0
  32. package/dist/core/frontmatter-read.d.ts +25 -0
  33. package/dist/core/frontmatter-read.js +138 -0
  34. package/dist/core/hook-events.d.ts +34 -0
  35. package/dist/core/hook-events.js +48 -0
  36. package/dist/core/layout.d.ts +6 -0
  37. package/dist/core/mcp-config.d.ts +20 -0
  38. package/dist/core/mcp-config.js +40 -0
  39. package/dist/core/mcp-hook.d.ts +35 -0
  40. package/dist/core/mcp-hook.js +70 -0
  41. package/dist/core/mcp-tool.d.ts +50 -0
  42. package/dist/core/mcp-tool.js +61 -0
  43. package/dist/core/orphans.js +21 -0
  44. package/dist/core/spec.d.ts +142 -3
  45. package/dist/core/spec.js +48 -0
  46. package/dist/core/tool-contract.d.ts +68 -0
  47. package/dist/core/tool-contract.js +113 -0
  48. package/dist/core/types.d.ts +91 -2
  49. package/dist/core/validate.js +23 -1
  50. package/dist/eval.d.ts +69 -13
  51. package/dist/eval.js +106 -51
  52. package/dist/harness-test.d.ts +7 -0
  53. package/dist/harness-test.js +19 -7
  54. package/dist/leaderboard.d.ts +2 -0
  55. package/dist/leaderboard.js +63 -3
  56. package/dist/optimize.d.ts +74 -0
  57. package/dist/optimize.js +94 -0
  58. package/dist/plugin-loader.d.ts +1 -0
  59. package/dist/plugin-loader.js +71 -18
  60. package/dist/scaffold-test.d.ts +30 -0
  61. package/dist/scaffold-test.js +158 -0
  62. package/dist/scan-behavioral.d.ts +73 -0
  63. package/dist/scan-behavioral.js +150 -0
  64. package/dist/scan.d.ts +166 -1
  65. package/dist/scan.js +622 -55
  66. package/dist/score-explainer.d.ts +69 -0
  67. package/dist/score-explainer.js +169 -0
  68. package/dist/test-coverage.d.ts +7 -0
  69. package/dist/test-coverage.js +39 -24
  70. package/package.json +2 -1
  71. package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -6
  72. package/skills/edit-spec/SKILL.md +1 -1
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Score-explainer — the deterministic WHY behind a low measured score (C4 of the
3
+ * measurement-authority pivot; the strongest pairing in it).
4
+ *
5
+ * The MEASUREMENT layer finds a behavioral SYMPTOM: "this skill underperforms",
6
+ * "the wrong skill fires", "this agent fails its task". A behavioral symptom alone
7
+ * is not actionable — you can drop the skill, but you don't know WHY it lost. The
8
+ * cross-reference engine (the linting layer) already detects the deterministic
9
+ * CAUSES: two skills with near-identical descriptions can't be told apart by the
10
+ * selector; a never-available / typo'd tool is silently dropped from a subagent; a
11
+ * hook on a misspelled event never fires. This module BRIDGES the two: given the
12
+ * `ScanReport` the linter already computes, it surfaces — per affected surface —
13
+ * the deterministic cause of a behavioral symptom and the one-line fix.
14
+ *
15
+ * Measurement says "caveman underperforms"
16
+ * the explainer says "...BECAUSE its description overlaps `compress` (0.86) —
17
+ * differentiate them so the selector can disambiguate."
18
+ *
19
+ * It REUSES the scan findings (one-detector-no-drift) — it never re-detects. So
20
+ * it's pure over a `ScanReport`, free, model-less, and consistent with `vigiles
21
+ * lint`/`scan`. It's the diagnostic the per-repo optimizer (`vigiles optimize`,
22
+ * A2) prints next to each "drop / swap" recommendation. See
23
+ * `research/measurement-authority.md` ("what becomes of the linting", role 2).
24
+ */
25
+ import type { ScanReport } from "./scan.js";
26
+ /**
27
+ * The behavioral failure a MEASUREMENT would observe — the symptom an explanation
28
+ * accounts for. Discriminated so a consumer can group/filter by symptom and so an
29
+ * explanation can never carry a symptom it has no cause for.
30
+ */
31
+ export type BehavioralSymptom = "wrong-skill-fires" | "skill-never-fires" | "agent-underperforms" | "hook-never-runs" | "subagent-never-dispatches";
32
+ /**
33
+ * How firmly the deterministic finding EXPLAINS the symptom:
34
+ * - `"likely"` — a hard structural dead-end (a missing script can't run, a
35
+ * never-available tool can't be called); the cause is near-certain.
36
+ * - `"possible"` — a high-precision PROXY for a behavioral risk (a description
37
+ * overlap / a foreign-script description); deterministic to detect, but whether
38
+ * it actually moved behaviour is confirmed by `scan --trigger`.
39
+ */
40
+ export type ExplanationConfidence = "likely" | "possible";
41
+ export interface ScoreExplanation {
42
+ /** The affected surface (a skill/agent/hook name or path) the symptom attaches to. */
43
+ readonly surface: string;
44
+ /** What a measurement would SEE. */
45
+ readonly symptom: BehavioralSymptom;
46
+ /** The deterministic finding (the scan/lint detector's own message — no drift). */
47
+ readonly cause: string;
48
+ /** The lint rule that found it, so a reader can open `docs/rules/<detector>.md`. */
49
+ readonly detector: string;
50
+ /** A single, actionable fix. */
51
+ readonly fix: string;
52
+ readonly confidence: ExplanationConfidence;
53
+ }
54
+ /**
55
+ * Explain every behavioral symptom the report's deterministic findings account
56
+ * for. Returns one `ScoreExplanation` per finding, `"likely"` causes first (a hard
57
+ * dead-end is more certain than a proxy). Pure over the report.
58
+ */
59
+ export declare function explainScore(report: ScanReport): ScoreExplanation[];
60
+ /**
61
+ * The explanations that attach to ONE underperforming surface — the call the
62
+ * benchmark/optimizer makes when a measurement flags a single skill/agent. Matches
63
+ * a surface name case-insensitively, including the `"a ↔ b"` overlap pairs (so
64
+ * explaining "caveman" surfaces an overlap with another skill).
65
+ */
66
+ export declare function explainSurface(report: ScanReport, surface: string): ScoreExplanation[];
67
+ /** Render explanations for a CLI/report — grouped under the symptom, fix called out. */
68
+ export declare function formatExplanations(exps: readonly ScoreExplanation[]): string;
69
+ //# sourceMappingURL=score-explainer.d.ts.map
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * Score-explainer — the deterministic WHY behind a low measured score (C4 of the
4
+ * measurement-authority pivot; the strongest pairing in it).
5
+ *
6
+ * The MEASUREMENT layer finds a behavioral SYMPTOM: "this skill underperforms",
7
+ * "the wrong skill fires", "this agent fails its task". A behavioral symptom alone
8
+ * is not actionable — you can drop the skill, but you don't know WHY it lost. The
9
+ * cross-reference engine (the linting layer) already detects the deterministic
10
+ * CAUSES: two skills with near-identical descriptions can't be told apart by the
11
+ * selector; a never-available / typo'd tool is silently dropped from a subagent; a
12
+ * hook on a misspelled event never fires. This module BRIDGES the two: given the
13
+ * `ScanReport` the linter already computes, it surfaces — per affected surface —
14
+ * the deterministic cause of a behavioral symptom and the one-line fix.
15
+ *
16
+ * Measurement says "caveman underperforms"
17
+ * the explainer says "...BECAUSE its description overlaps `compress` (0.86) —
18
+ * differentiate them so the selector can disambiguate."
19
+ *
20
+ * It REUSES the scan findings (one-detector-no-drift) — it never re-detects. So
21
+ * it's pure over a `ScanReport`, free, model-less, and consistent with `vigiles
22
+ * lint`/`scan`. It's the diagnostic the per-repo optimizer (`vigiles optimize`,
23
+ * A2) prints next to each "drop / swap" recommendation. See
24
+ * `research/measurement-authority.md` ("what becomes of the linting", role 2).
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.explainScore = explainScore;
28
+ exports.explainSurface = explainSurface;
29
+ exports.formatExplanations = formatExplanations;
30
+ const SYMPTOM_LABEL = {
31
+ "wrong-skill-fires": "the selector fires the wrong skill",
32
+ "skill-never-fires": "the skill never fires",
33
+ "agent-underperforms": "the subagent loses a declared tool",
34
+ "hook-never-runs": "the hook never runs",
35
+ "subagent-never-dispatches": "the subagent won't register",
36
+ };
37
+ // 1. Description overlap → the selector can't disambiguate (precision collision).
38
+ function overlapExplanations(report) {
39
+ return report.descriptionOverlaps.map((o) => ({
40
+ surface: `${o.a} ↔ ${o.b}`,
41
+ symptom: "wrong-skill-fires",
42
+ cause: o.message,
43
+ detector: "description-overlap",
44
+ fix: `Differentiate the descriptions of "${o.a}" and "${o.b}" (${o.similarity} similar) — the selector picks by description, so near-identical text makes it fire the wrong one.`,
45
+ confidence: "possible",
46
+ }));
47
+ }
48
+ // 2. Skill with no usable description → nothing for the selector to match on.
49
+ function skillExplanations(report) {
50
+ return report.skills
51
+ .filter((s) => !s.hasDescription)
52
+ .map((s) => ({
53
+ surface: s.name,
54
+ symptom: "skill-never-fires",
55
+ cause: `"${s.name}" has no usable description`,
56
+ detector: "skill-frontmatter",
57
+ fix: `Add a "description:" to "${s.name}" — the selector matches on it; without one the skill has no trigger surface.`,
58
+ confidence: "likely",
59
+ }));
60
+ }
61
+ // 3. Subagent tool-contract dead entries → the tool is silently dropped.
62
+ function agentExplanations(report) {
63
+ const out = [];
64
+ for (const a of report.agents) {
65
+ for (const t of a.toolIssues) {
66
+ out.push({
67
+ surface: a.name,
68
+ symptom: "agent-underperforms",
69
+ cause: t.message,
70
+ detector: "subagent-tool-contract",
71
+ fix: t.suggestion
72
+ ? `In "${a.name}", change the tool "${t.tool}" to "${t.suggestion}" — as written it isn't a real tool, so it's dropped and the agent can't use it.`
73
+ : `In "${a.name}", remove or correct the tool "${t.tool}" — it isn't an available tool, so it's silently dropped from the contract.`,
74
+ confidence: "likely",
75
+ });
76
+ }
77
+ for (const m of a.mcpToolIssues) {
78
+ out.push({
79
+ surface: a.name,
80
+ symptom: "agent-underperforms",
81
+ cause: m.message,
82
+ detector: "mcp-tool-resolves",
83
+ fix: `"${a.name}" lists the MCP tool "${m.tool}" but its server "${m.server}" isn't declared in the plugin's mcpServers — declare the server or drop the tool, else the call can't resolve.`,
84
+ confidence: "likely",
85
+ });
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ // 4. Hook on an unknown event, or 5. a missing hook script → the hook never runs.
91
+ function hookExplanations(report) {
92
+ const out = report.hookEventIssues.map((h) => ({
93
+ surface: h.event,
94
+ symptom: "hook-never-runs",
95
+ cause: h.message,
96
+ detector: "hook-events",
97
+ fix: h.suggestion
98
+ ? `Change the hook event "${h.event}" to "${h.suggestion}" — the harness doesn't define "${h.event}", so the hook never fires.`
99
+ : `Fix the hook event "${h.event}" — the harness doesn't define it, so the hook never fires.`,
100
+ confidence: "likely",
101
+ }));
102
+ for (const h of report.hooks) {
103
+ if (h.status === "missing") {
104
+ out.push({
105
+ surface: h.script,
106
+ symptom: "hook-never-runs",
107
+ cause: `hook script "${h.script}" does not exist on disk`,
108
+ detector: "hook-script-exists",
109
+ fix: `Create "${h.script}" or fix its path — the hook references a script that isn't on disk, so it silently never runs.`,
110
+ confidence: "likely",
111
+ });
112
+ }
113
+ }
114
+ return out;
115
+ }
116
+ // 6. Subagent frontmatter missing a required field → it won't register at all.
117
+ function frontmatterExplanations(report) {
118
+ return report.frontmatterIssues
119
+ .filter((f) => f.kind === "agent")
120
+ .map((f) => ({
121
+ surface: f.path,
122
+ symptom: "subagent-never-dispatches",
123
+ cause: f.message,
124
+ detector: "subagent-frontmatter",
125
+ fix: `Add the missing ${f.missing.join(" + ")} to "${f.path}" — a subagent without it won't register, so it can never be dispatched.`,
126
+ confidence: "likely",
127
+ }));
128
+ }
129
+ const confidenceRank = (c) => c === "likely" ? 0 : 1;
130
+ /**
131
+ * Explain every behavioral symptom the report's deterministic findings account
132
+ * for. Returns one `ScoreExplanation` per finding, `"likely"` causes first (a hard
133
+ * dead-end is more certain than a proxy). Pure over the report.
134
+ */
135
+ function explainScore(report) {
136
+ return [
137
+ ...overlapExplanations(report),
138
+ ...skillExplanations(report),
139
+ ...agentExplanations(report),
140
+ ...hookExplanations(report),
141
+ ...frontmatterExplanations(report),
142
+ // `likely` before `possible` — surface the certain dead-ends first.
143
+ ].sort((x, y) => confidenceRank(x.confidence) - confidenceRank(y.confidence));
144
+ }
145
+ /**
146
+ * The explanations that attach to ONE underperforming surface — the call the
147
+ * benchmark/optimizer makes when a measurement flags a single skill/agent. Matches
148
+ * a surface name case-insensitively, including the `"a ↔ b"` overlap pairs (so
149
+ * explaining "caveman" surfaces an overlap with another skill).
150
+ */
151
+ function explainSurface(report, surface) {
152
+ const needle = surface.toLowerCase();
153
+ return explainScore(report).filter((e) => e.surface.toLowerCase().includes(needle));
154
+ }
155
+ /** Render explanations for a CLI/report — grouped under the symptom, fix called out. */
156
+ function formatExplanations(exps) {
157
+ if (exps.length === 0) {
158
+ return "No deterministic cause found — the cause is likely behavioral (measure with `scan --trigger` / an eval).";
159
+ }
160
+ const lines = [];
161
+ for (const e of exps) {
162
+ const mark = e.confidence === "likely" ? "✗" : "⚠";
163
+ lines.push(`${mark} ${e.surface} — ${SYMPTOM_LABEL[e.symptom]}`);
164
+ lines.push(` cause: ${e.cause} [${e.detector}]`);
165
+ lines.push(` fix: ${e.fix}`);
166
+ }
167
+ return lines.join("\n");
168
+ }
169
+ //# sourceMappingURL=score-explainer.js.map
@@ -21,6 +21,7 @@
21
21
  * The only opt-out is explicit: a `vigiles:ignore-test` marker in the surface
22
22
  * file, which is reported as `exempt` so the skip is visible, never silent.
23
23
  */
24
+ import type { PluginLayout } from "./core/layout.js";
24
25
  export type SurfaceKind = "skill" | "agent" | "hook";
25
26
  export interface Surface {
26
27
  readonly kind: SurfaceKind;
@@ -54,6 +55,12 @@ export interface TestCoverageOptions {
54
55
  readonly testGlobs?: readonly string[];
55
56
  /** Extra ignore globs (added to node_modules/dist/.git/.vigiles). */
56
57
  readonly exclude?: readonly string[];
58
+ /**
59
+ * Harness layout — where skills/agents live, the plugin-root token, the
60
+ * manifest/settings paths. Defaults to Claude Code; a non-CC adapter passes its
61
+ * own so the surface globs and hook-token expansion aren't hard-coded.
62
+ */
63
+ readonly layout?: PluginLayout;
57
64
  }
58
65
  /**
59
66
  * Find harness surfaces (skills / agents / hooks) that no test or eval covers.
@@ -29,6 +29,20 @@ exports.formatUntestedReport = formatUntestedReport;
29
29
  const node_fs_1 = require("node:fs");
30
30
  const node_path_1 = require("node:path");
31
31
  const glob_1 = require("glob");
32
+ const layout_js_1 = require("./adapters/claude-code/layout.js");
33
+ /**
34
+ * The two on-disk locations a surface dir can occupy: the plugin-root form
35
+ * (`skills/…`) and the materialized form (`.claude/skills/…`) — derived from the
36
+ * layout so a non-Claude-Code harness (its own `materializeRoot` / surface dir)
37
+ * is discovered without hard-coding `.claude`. An empty `dir` (a harness lacking
38
+ * the surface, e.g. Codex subagents) yields no globs.
39
+ */
40
+ function surfaceGlobs(dir, leaf, materializeRoot) {
41
+ if (!dir)
42
+ return [];
43
+ const matForm = materializeRoot ? `${materializeRoot}/${dir}` : dir;
44
+ return [...new Set([`${dir}/${leaf}`, `${matForm}/${leaf}`])];
45
+ }
32
46
  // ---------------------------------------------------------------------------
33
47
  // Internals
34
48
  // ---------------------------------------------------------------------------
@@ -58,12 +72,9 @@ function read(path) {
58
72
  return "";
59
73
  }
60
74
  }
61
- function discoverSkills(basePath, ignore) {
75
+ function discoverSkills(basePath, ignore, layout) {
62
76
  const out = [];
63
- const found = (0, glob_1.globSync)(["skills/*/SKILL.md", ".claude/skills/*/SKILL.md"], {
64
- cwd: basePath,
65
- ignore,
66
- });
77
+ const found = (0, glob_1.globSync)(surfaceGlobs(layout.skillDir, "*/SKILL.md", layout.materializeRoot), { cwd: basePath, ignore });
67
78
  for (const path of found.sort()) {
68
79
  const name = (0, node_path_1.basename)((0, node_path_1.dirname)(path));
69
80
  const content = read((0, node_path_1.join)(basePath, path));
@@ -71,18 +82,15 @@ function discoverSkills(basePath, ignore) {
71
82
  kind: "skill",
72
83
  path,
73
84
  name,
74
- tokens: [`skills/${name}`, `:${name}`],
85
+ tokens: [`${layout.skillDir}/${name}`, `:${name}`],
75
86
  ignored: content.includes(IGNORE_MARKER),
76
87
  });
77
88
  }
78
89
  return out;
79
90
  }
80
- function discoverAgents(basePath, ignore) {
91
+ function discoverAgents(basePath, ignore, layout) {
81
92
  const out = [];
82
- const found = (0, glob_1.globSync)(["agents/*.md", ".claude/agents/*.md"], {
83
- cwd: basePath,
84
- ignore,
85
- });
93
+ const found = (0, glob_1.globSync)(surfaceGlobs(layout.agentDir, "*.md", layout.materializeRoot), { cwd: basePath, ignore });
86
94
  for (const path of found.sort()) {
87
95
  if (path.endsWith(".spec.ts"))
88
96
  continue;
@@ -100,7 +108,7 @@ function discoverAgents(basePath, ignore) {
100
108
  return out;
101
109
  }
102
110
  /** Hook-script paths referenced from a manifest's `hooks` block (file hooks only). */
103
- function hookScripts(basePath, manifest) {
111
+ function hookScripts(basePath, manifest, pluginRootToken) {
104
112
  if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(basePath, manifest)))
105
113
  return [];
106
114
  let hooks;
@@ -114,25 +122,31 @@ function hookScripts(basePath, manifest) {
114
122
  if (hooks === undefined)
115
123
  return [];
116
124
  const text = JSON.stringify(hooks);
125
+ // "${CLAUDE_PLUGIN_ROOT}" → unbraced "$CLAUDE_PLUGIN_ROOT" — strip the harness's
126
+ // own token (both forms) so the path is checkable relative to the plugin root.
127
+ const unbraced = pluginRootToken.replace(/^\$\{(.+)\}$/, "$$$1");
117
128
  const scripts = new Set();
118
129
  for (const m of text.matchAll(SCRIPT_RE)) {
119
130
  const rel = m[0]
120
- .replace("${CLAUDE_PLUGIN_ROOT}/", "")
121
- .replace(/^\$\{CLAUDE_PLUGIN_ROOT\}/, "")
131
+ .replaceAll(pluginRootToken, "")
132
+ .replaceAll(unbraced, "")
133
+ .replace(/^\/+/, "")
122
134
  .replace(/^\.\//, "");
123
135
  if ((0, node_fs_1.existsSync)((0, node_path_1.join)(basePath, rel)))
124
136
  scripts.add(rel);
125
137
  }
126
138
  return [...scripts];
127
139
  }
128
- function discoverHooks(basePath) {
140
+ function discoverHooks(basePath, layout) {
129
141
  const scripts = new Set();
130
- for (const m of [
131
- ".claude-plugin/plugin.json",
132
- ".claude/settings.json",
133
- ".claude/settings.local.json",
134
- ]) {
135
- for (const s of hookScripts(basePath, m))
142
+ // The harness's manifest + settings (and a `.local` settings sibling, a CC
143
+ // convention that's harmless to probe elsewhere).
144
+ const localSettings = layout.settingsPath.replace(/(\.[^./]+)$/, ".local$1");
145
+ const manifests = [
146
+ ...new Set([layout.manifestPath, layout.settingsPath, localSettings]),
147
+ ];
148
+ for (const m of manifests) {
149
+ for (const s of hookScripts(basePath, m, layout.pluginRootToken))
136
150
  scripts.add(s);
137
151
  }
138
152
  return [...scripts].sort().map((path) => ({
@@ -185,15 +199,16 @@ function isCovered(surface, tests) {
185
199
  */
186
200
  function findUntestedSurfaces(options = {}) {
187
201
  const basePath = options.basePath ?? process.cwd();
202
+ const layout = options.layout ?? layout_js_1.claudeCodeLayout;
188
203
  const ignore = [...DEFAULT_IGNORE, ...(options.exclude ?? [])];
189
204
  const globs = options.testGlobs ?? DEFAULT_TEST_GLOBS;
190
205
  const surfaces = [];
191
206
  if (options.skills !== false)
192
- surfaces.push(...discoverSkills(basePath, ignore));
207
+ surfaces.push(...discoverSkills(basePath, ignore, layout));
193
208
  if (options.agents !== false)
194
- surfaces.push(...discoverAgents(basePath, ignore));
209
+ surfaces.push(...discoverAgents(basePath, ignore, layout));
195
210
  if (options.hooks !== false)
196
- surfaces.push(...discoverHooks(basePath));
211
+ surfaces.push(...discoverHooks(basePath, layout));
197
212
  // Every skill/agent/hook is held to the requirement — only an explicit
198
213
  // `vigiles:ignore-test` marker exempts a surface (a visible, deliberate skip).
199
214
  const considered = surfaces.filter((s) => !s.ignored);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "5.0.1",
3
+ "version": "5.2.0",
4
4
  "description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -128,6 +128,7 @@
128
128
  "glob": "^13.0.6",
129
129
  "js-yaml": "^4.1.0",
130
130
  "minimatch": "^10.0.1",
131
+ "mvdan-sh": "^0.10.1",
131
132
  "ts-essentials": "^10.2.1",
132
133
  "typescript": "^5.9.3"
133
134
  }
@@ -1,13 +1,13 @@
1
1
  ---
2
- name: migrate-to-spec
3
- description: Convert an existing hand-written CLAUDE.md into a typed .spec.ts file for incremental adoption
2
+ name: adopt-spec
3
+ description: Adopt a typed .spec.ts for an existing hand-written CLAUDE.md start from the file you already have, non-destructively
4
4
  disable-model-invocation: true
5
5
  argument-hint: <path to CLAUDE.md, defaults to CLAUDE.md>
6
6
  ---
7
7
 
8
- Convert an existing hand-written CLAUDE.md (or AGENTS.md) into a typed `CLAUDE.md.spec.ts` file. This is the incremental adoption path — you keep your existing instruction file as the starting point and get type safety going forward.
8
+ Start a typed `CLAUDE.md.spec.ts` from an existing hand-written CLAUDE.md (or AGENTS.md). This is the non-destructive adoption path — you keep your existing instruction file as the starting point and get type safety going forward.
9
9
 
10
- > **Don't need full TypeScript?** A typed spec is the deepest commitment level. If the user only wants verified rules without a build step, point them at markdown mode first: inline `<!-- vigiles:enforce ... -->` comments (Level 0) or a `vigiles:` YAML frontmatter block with `vigiles generate-schema` for editor autocomplete (Level 1). Both are verified by `vigiles lint` with the same engine as a spec. See `docs/markdown-mode.md`. Migrate to a spec only when they want compiler-grade guarantees.
10
+ > **Don't need full TypeScript?** A typed spec is the deepest commitment level. If the user only wants verified rules without a build step, point them at markdown mode first: inline `<!-- vigiles:enforce ... -->` comments (Level 0) or a `vigiles:` YAML frontmatter block with `vigiles generate-schema` for editor autocomplete (Level 1). Both are verified by `vigiles lint` with the same engine as a spec. See `docs/markdown-mode.md`. Adopt a spec only when they want compiler-grade guarantees.
11
11
 
12
12
  ## Instructions
13
13
 
@@ -46,8 +46,6 @@ import {
46
46
  claude,
47
47
  enforce,
48
48
  guidance,
49
- check,
50
- every,
51
49
  file,
52
50
  cmd,
53
51
  ref,
@@ -27,7 +27,7 @@ Look for spec files in the repo root:
27
27
  - Any `*.spec.ts` matching instruction files
28
28
 
29
29
  If no spec exists: if there's a hand-written `CLAUDE.md`, suggest the
30
- `migrate-to-spec` skill; otherwise suggest `npx vigiles init` to scaffold one.
30
+ `adopt-spec` skill; otherwise suggest `npx vigiles init` to scaffold one.
31
31
 
32
32
  ### Step 2: Read and Understand the Spec
33
33