vigiles 29.1.0 → 30.0.1

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 (122) hide show
  1. package/dist/adapter-conformance.d.ts +1 -1
  2. package/dist/adapter-conformance.js +106 -25
  3. package/dist/adapter-registry.d.ts +61 -14
  4. package/dist/adapter-registry.js +78 -10
  5. package/dist/adapter.d.ts +23 -2
  6. package/dist/adapter.js +13 -1
  7. package/dist/adapters/claude-code/adapter.d.ts +32 -2
  8. package/dist/adapters/claude-code/adapter.js +44 -23
  9. package/dist/adapters/claude-code/agent-runtime.js +3 -1
  10. package/dist/adapters/claude-code/dialect.js +87 -21
  11. package/dist/adapters/claude-code/effect-region.js +3 -1
  12. package/dist/adapters/claude-code/hook-protocol.js +16 -0
  13. package/dist/adapters/claude-code/instruction-chain.d.ts +25 -0
  14. package/dist/adapters/claude-code/instruction-chain.js +626 -0
  15. package/dist/adapters/claude-code/layout.d.ts +2 -2
  16. package/dist/adapters/claude-code/layout.js +42 -8
  17. package/dist/adapters/claude-code/model-access.d.ts +41 -0
  18. package/dist/adapters/claude-code/model-access.js +46 -0
  19. package/dist/adapters/claude-code/skill-reachability.d.ts +125 -0
  20. package/dist/adapters/claude-code/skill-reachability.js +111 -0
  21. package/dist/adapters/claude-code/skill-runtime.js +3 -1
  22. package/dist/adapters/codex/adapter.d.ts +39 -2
  23. package/dist/adapters/codex/adapter.js +29 -29
  24. package/dist/adapters/codex/dialect.js +11 -6
  25. package/dist/adapters/codex/eval.d.ts +10 -0
  26. package/dist/adapters/codex/eval.js +48 -1
  27. package/dist/adapters/codex/hook-protocol.d.ts +2 -1
  28. package/dist/adapters/codex/hook-protocol.js +10 -0
  29. package/dist/adapters/codex/instruction-chain.d.ts +40 -0
  30. package/dist/adapters/codex/instruction-chain.js +105 -0
  31. package/dist/adapters/codex/layout.d.ts +1 -1
  32. package/dist/adapters/codex/layout.js +41 -14
  33. package/dist/adapters/opencode/adapter.d.ts +33 -2
  34. package/dist/adapters/opencode/adapter.js +36 -36
  35. package/dist/adapters/opencode/dialect.js +2 -2
  36. package/dist/adapters/opencode/instruction-chain.d.ts +37 -0
  37. package/dist/adapters/opencode/instruction-chain.js +70 -0
  38. package/dist/adapters/opencode/layout.d.ts +19 -0
  39. package/dist/adapters/opencode/layout.js +34 -15
  40. package/dist/adoptability.d.ts +31 -1
  41. package/dist/adoptability.js +57 -0
  42. package/dist/cli-main.js +185 -102
  43. package/dist/core/adapter.d.ts +213 -61
  44. package/dist/core/compile.d.ts +2 -2
  45. package/dist/core/compile.js +57 -46
  46. package/dist/core/compose.d.ts +5 -3
  47. package/dist/core/compose.js +5 -3
  48. package/dist/core/config-schema.d.ts +14 -2
  49. package/dist/core/config-schema.js +20 -7
  50. package/dist/core/dialect.d.ts +54 -12
  51. package/dist/core/dialect.js +56 -0
  52. package/dist/core/eval-driver.d.ts +194 -0
  53. package/dist/core/eval-driver.js +3 -0
  54. package/dist/core/frontmatter-read.d.ts +10 -0
  55. package/dist/core/frontmatter-read.js +30 -3
  56. package/dist/core/guards.js +3 -1
  57. package/dist/core/hook-program.d.ts +27 -2
  58. package/dist/core/hook-program.js +29 -24
  59. package/dist/core/hook-protocol.d.ts +54 -0
  60. package/dist/core/install-reader.d.ts +18 -0
  61. package/dist/core/install-reader.js +88 -0
  62. package/dist/core/instruction-chain.d.ts +444 -0
  63. package/dist/core/instruction-chain.js +292 -0
  64. package/dist/core/instruction-weight.d.ts +96 -14
  65. package/dist/core/instruction-weight.js +65 -30
  66. package/dist/core/layout.d.ts +220 -33
  67. package/dist/core/layout.js +115 -1
  68. package/dist/core/lethal-trifecta.d.ts +12 -7
  69. package/dist/core/lethal-trifecta.js +13 -13
  70. package/dist/core/live-driver.d.ts +137 -0
  71. package/dist/core/live-driver.js +14 -0
  72. package/dist/core/markdown.d.ts +23 -0
  73. package/dist/core/markdown.js +77 -28
  74. package/dist/core/orphans.js +9 -7
  75. package/dist/core/settings-codec.d.ts +17 -0
  76. package/dist/core/settings-codec.js +56 -0
  77. package/dist/core/surface-discovery.d.ts +2 -2
  78. package/dist/core/surface-discovery.js +24 -12
  79. package/dist/core/surface-scopes.d.ts +26 -6
  80. package/dist/core/surface-scopes.js +52 -11
  81. package/dist/core/validate.js +16 -3
  82. package/dist/coverage-artifact.d.ts +3 -2
  83. package/dist/coverage-artifact.js +6 -5
  84. package/dist/eval-cache.d.ts +6 -1
  85. package/dist/eval-cache.js +11 -1
  86. package/dist/eval.d.ts +16 -108
  87. package/dist/eval.js +36 -2
  88. package/dist/harness-test.d.ts +3 -63
  89. package/dist/hook-install.d.ts +12 -1
  90. package/dist/hook-install.js +12 -1
  91. package/dist/hook-runtime.js +4 -2
  92. package/dist/hook-state-store.js +3 -1
  93. package/dist/local-files-tracked.d.ts +17 -0
  94. package/dist/local-files-tracked.js +70 -0
  95. package/dist/local-files.d.ts +62 -0
  96. package/dist/local-files.js +183 -0
  97. package/dist/observe.d.ts +3 -2
  98. package/dist/observe.js +7 -6
  99. package/dist/plugin-loader.d.ts +1 -1
  100. package/dist/plugin-loader.js +43 -36
  101. package/dist/scan-behavioral.d.ts +34 -25
  102. package/dist/scan-behavioral.js +122 -58
  103. package/dist/scan-core.js +37 -18
  104. package/dist/scan-files.d.ts +1 -1
  105. package/dist/scan-files.js +53 -33
  106. package/dist/scan-trigger-suggest.d.ts +0 -21
  107. package/dist/scan-trigger-suggest.js +0 -23
  108. package/dist/scan.d.ts +4 -4
  109. package/dist/scan.js +120 -84
  110. package/dist/skill-harness.d.ts +21 -5
  111. package/dist/skill-harness.js +29 -11
  112. package/dist/surface-discovery-fs.d.ts +2 -0
  113. package/dist/surface-discovery-fs.js +108 -6
  114. package/dist/test-coverage-files.js +24 -17
  115. package/dist/test-coverage.d.ts +9 -3
  116. package/dist/test-coverage.js +32 -22
  117. package/dist/verify-plugin-guards.js +1 -1
  118. package/package.json +1 -1
  119. package/dist/skill-reachability.d.ts +0 -68
  120. package/dist/skill-reachability.js +0 -205
  121. /package/dist/{dialect-drift.d.ts → adapters/claude-code/dialect-drift.d.ts} +0 -0
  122. /package/dist/{dialect-drift.js → adapters/claude-code/dialect-drift.js} +0 -0
@@ -82,19 +82,25 @@ function matchSurface(files, prefixes, leafRe) {
82
82
  }
83
83
  function discoverSkills(files, layout, repoName) {
84
84
  const out = [];
85
- if (layout.skillDir) {
86
- const prefixes = surfacePrefixes(layout.skillDir, layout.materializeRoot);
87
- for (const path of matchSurface(files, prefixes, "[^/]+/SKILL\\.md")) {
88
- const name = (0, posix_path_js_1.basename)((0, posix_path_js_1.dirname)(path));
89
- const content = files[path];
90
- out.push({
91
- kind: "skill",
92
- path,
93
- name,
94
- tokens: [`${layout.skillDir}/${name}`, `:${name}`],
95
- ignored: content.includes(IGNORE_MARKER),
96
- });
97
- }
85
+ // 🔴 THE GATE MOVES TO THE TOP so it covers the root-`SKILL.md` case below too
86
+ // — the twin of this function on disk had the same hole. A layout declaring no
87
+ // skill surface reported a root `SKILL.md` as an untested skill, with a token
88
+ // reading `undefined/<name>`, lowering Tested for a repo whose harness never
89
+ // loads skills at all. Shape matches `discoverAgents` right below.
90
+ const skillDir = layout.surfaces.skill;
91
+ if (skillDir === undefined)
92
+ return out;
93
+ const prefixes = surfacePrefixes(skillDir, (0, layout_js_1.materializePrefix)(layout));
94
+ for (const path of matchSurface(files, prefixes, "[^/]+/SKILL\\.md")) {
95
+ const name = (0, posix_path_js_1.basename)((0, posix_path_js_1.dirname)(path));
96
+ const content = files[path];
97
+ out.push({
98
+ kind: "skill",
99
+ path,
100
+ name,
101
+ tokens: [`${skillDir}/${name}`, `:${name}`],
102
+ ignored: content.includes(IGNORE_MARKER),
103
+ });
98
104
  }
99
105
  // Single-skill-directory target: a bare `SKILL.md` at the repo root.
100
106
  //
@@ -113,7 +119,7 @@ function discoverSkills(files, layout, repoName) {
113
119
  kind: "skill",
114
120
  path: "SKILL.md",
115
121
  name,
116
- tokens: [`${layout.skillDir}/${name}`, `:${name}`],
122
+ tokens: [`${skillDir}/${name}`, `:${name}`],
117
123
  ignored: content.includes(IGNORE_MARKER),
118
124
  });
119
125
  }
@@ -121,9 +127,10 @@ function discoverSkills(files, layout, repoName) {
121
127
  }
122
128
  function discoverAgents(files, layout) {
123
129
  const out = [];
124
- if (!layout.agentDir)
130
+ const agentDir = layout.surfaces.agent;
131
+ if (agentDir === undefined)
125
132
  return out;
126
- const prefixes = surfacePrefixes(layout.agentDir, layout.materializeRoot);
133
+ const prefixes = surfacePrefixes(agentDir, (0, layout_js_1.materializePrefix)(layout));
127
134
  // Same depth rule as the scan classifier — quoted from AGENT_FILE_LEAF_RE, not
128
135
  // respelled. This discoverer feeds the `Tested` metric; when it disagreed with
129
136
  // the classifier, `audit` printed a subagent count and an untested-surface
@@ -132,7 +139,7 @@ function discoverAgents(files, layout) {
132
139
  if (path.endsWith(".spec.ts"))
133
140
  continue;
134
141
  const content = files[path];
135
- const name = (0, layout_js_1.agentSurfaceName)(path, layout.agentDir) ?? (0, posix_path_js_1.basename)(path, ".md");
142
+ const name = (0, layout_js_1.agentSurfaceName)(path, agentDir) ?? (0, posix_path_js_1.basename)(path, ".md");
136
143
  const dir = (0, posix_path_js_1.dirname)(path);
137
144
  out.push({
138
145
  kind: "agent",
@@ -217,7 +217,13 @@ export interface TestCoverageOptions {
217
217
  * manifest/settings paths. Defaults to Claude Code; a non-CC adapter passes its
218
218
  * own so the surface globs and hook-token expansion aren't hard-coded.
219
219
  */
220
- readonly layout?: PluginLayout;
220
+ /**
221
+ * The layout to discover surfaces under. REQUIRED: it used to default to the
222
+ * Claude Code layout, which meant this harness-agnostic detector imported an
223
+ * adapter, and a caller that forgot to pass one silently graded a Codex repo
224
+ * with Claude Code's directories.
225
+ */
226
+ readonly layout: PluginLayout;
221
227
  }
222
228
  /**
223
229
  * Find harness surfaces (skills / agents / hooks) that no test or eval covers.
@@ -231,7 +237,7 @@ export interface TestCoverageOptions {
231
237
  * deterministic coverage, no evals" from "has neither" — two positions the single
232
238
  * count made indistinguishable.
233
239
  */
234
- export declare function findUntestedSurfaces(options?: TestCoverageOptions): UntestedReport;
240
+ export declare function findUntestedSurfaces(options: TestCoverageOptions): UntestedReport;
235
241
  /** Suggested colocated test path for an untested surface (shown in the warning). */
236
242
  export declare function suggestedTestPath(surface: Surface, ext?: string): string;
237
243
  /** Tally the union tier's coverage decisions by how each was established. */
@@ -260,7 +266,7 @@ export declare function coverageEvidenceCounts(report: UntestedReport): Evidence
260
266
  * Returns `null` when the edited file isn't a skill/agent surface, or when it is
261
267
  * covered on both tiers. Never throws — a nudge must not disrupt an edit.
262
268
  */
263
- export declare function skillTestNudge(filePath: string, options?: TestCoverageOptions): string | null;
269
+ export declare function skillTestNudge(filePath: string, options: TestCoverageOptions): string | null;
264
270
  /**
265
271
  * What the PAID eval tier measures for a surface of this kind — or `null` when
266
272
  * it measures nothing for it.
@@ -76,23 +76,18 @@ const assert_never_js_1 = require("./core/assert-never.js");
76
76
  const ts_runner_caps_js_1 = require("./ts-runner-caps.js");
77
77
  const glob_1 = require("glob");
78
78
  const layout_js_1 = require("./core/layout.js");
79
- // 🔴 Same finding as src/scan.ts: a module listed as a harness-agnostic detector
80
- // importing the Claude Code adapter for a DEFAULT. Invisible to both existing
81
- // fences — unclassified for `boundaries/dependencies`, and `\.claude` does not
82
- // match `/claude-code/`. The default belongs to the caller, not the detector.
83
- // eslint-disable-next-line local/no-harness-names -- see above
84
- const layout_js_2 = require("./adapters/claude-code/layout.js");
85
79
  const coverage_evidence_js_1 = require("./coverage-evidence.js");
86
80
  const coverage_artifact_js_1 = require("./coverage-artifact.js");
87
81
  /**
88
82
  * The two on-disk locations a surface dir can occupy: the plugin-root form
89
83
  * (`skills/…`) and the materialized form (`.claude/skills/…`) — derived from the
90
- * layout so a non-Claude-Code harness (its own `materializeRoot` / surface dir)
91
- * is discovered without hard-coding `.claude`. An empty `dir` (a harness lacking
92
- * the surface, e.g. Codex subagents) yields no globs.
84
+ * layout so a non-Claude-Code harness (its own materialize prefix / surface dir)
85
+ * is discovered without hard-coding `.claude`. An ABSENT `dir` (a harness
86
+ * lacking the surface, e.g. Codex subagents) yields no globs — it used to be an
87
+ * empty string, which was a second spelling of the same fact.
93
88
  */
94
89
  function surfaceGlobs(dir, leaf, materializeRoot) {
95
- if (!dir)
90
+ if (dir === undefined)
96
91
  return [];
97
92
  const matForm = materializeRoot ? `${materializeRoot}/${dir}` : dir;
98
93
  return [...new Set([`${dir}/${leaf}`, `${matForm}/${leaf}`])];
@@ -151,7 +146,17 @@ function read(path) {
151
146
  }
152
147
  function discoverSkills(basePath, ignore, layout) {
153
148
  const out = [];
154
- const found = (0, glob_1.globSync)(surfaceGlobs(layout.skillDir, "*/SKILL.md", layout.materializeRoot), { cwd: basePath, ignore });
149
+ // 🔴 ONE GATE FOR THE WHOLE FUNCTION, and it is structural rather than
150
+ // repeated. A harness whose layout declares no skill surface has no skills to
151
+ // find — `surfaceGlobs` already returned `[]` for the nested case, but the
152
+ // root-`SKILL.md` case below did not ask, so a third-party layout with only
153
+ // agents or commands still reported an untested skill, and its token read
154
+ // `undefined/<name>`. Narrowing here makes that token unwritable instead of
155
+ // merely unwritten: `skillDir` is a `string` from this line on.
156
+ const skillDir = layout.surfaces.skill;
157
+ if (skillDir === undefined)
158
+ return out;
159
+ const found = (0, glob_1.globSync)(surfaceGlobs(skillDir, "*/SKILL.md", (0, layout_js_1.materializePrefix)(layout)), { cwd: basePath, ignore });
155
160
  for (const path of found.sort()) {
156
161
  const name = (0, node_path_1.basename)((0, node_path_1.dirname)(path));
157
162
  const content = read((0, node_path_1.join)(basePath, path));
@@ -159,7 +164,7 @@ function discoverSkills(basePath, ignore, layout) {
159
164
  kind: "skill",
160
165
  path,
161
166
  name,
162
- tokens: [`${layout.skillDir}/${name}`, `:${name}`],
167
+ tokens: [`${skillDir}/${name}`, `:${name}`],
163
168
  ignored: content.includes(IGNORE_MARKER),
164
169
  });
165
170
  }
@@ -181,7 +186,7 @@ function discoverSkills(basePath, ignore, layout) {
181
186
  kind: "skill",
182
187
  path: "SKILL.md",
183
188
  name,
184
- tokens: [`${layout.skillDir}/${name}`, `:${name}`],
189
+ tokens: [`${skillDir}/${name}`, `:${name}`],
185
190
  ignored: content.includes(IGNORE_MARKER),
186
191
  });
187
192
  }
@@ -204,11 +209,15 @@ function discoverAgents(basePath, ignore, layout) {
204
209
  // dialect is exactly how these two discoverers drifted from the scan
205
210
  // classifier in the first place, so only one dialect is authoritative and the
206
211
  // other is allowed to over-match.
207
- const found = (0, glob_1.globSync)(surfaceGlobs(layout.agentDir, "**/*.md", layout.materializeRoot), { cwd: basePath, ignore });
208
- const prefixes = layout.materializeRoot
209
- ? [layout.agentDir, `${layout.materializeRoot}/${layout.agentDir}`]
210
- : [layout.agentDir];
211
- const isAgentFile = layout.agentDir
212
+ const found = (0, glob_1.globSync)(surfaceGlobs(layout.surfaces.agent, "**/*.md", (0, layout_js_1.materializePrefix)(layout)), { cwd: basePath, ignore });
213
+ const agentDir = layout.surfaces.agent;
214
+ const matPrefix = (0, layout_js_1.materializePrefix)(layout);
215
+ const prefixes = agentDir === undefined
216
+ ? []
217
+ : matPrefix
218
+ ? [agentDir, `${matPrefix}/${agentDir}`]
219
+ : [agentDir];
220
+ const isAgentFile = agentDir
212
221
  ? new RegExp(`^(?:${[...new Set(prefixes)]
213
222
  .map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
214
223
  .join("|")})/${layout_js_1.AGENT_FILE_LEAF_RE}$`)
@@ -225,7 +234,8 @@ function discoverAgents(basePath, ignore, layout) {
225
234
  if (isAgentFile && !isAgentFile.test(rel))
226
235
  continue;
227
236
  const content = read((0, node_path_1.join)(basePath, path));
228
- const name = (0, layout_js_1.agentSurfaceName)(rel, layout.agentDir) ?? (0, node_path_1.basename)(rel, ".md");
237
+ const name = (agentDir === undefined ? null : (0, layout_js_1.agentSurfaceName)(rel, agentDir)) ??
238
+ (0, node_path_1.basename)(rel, ".md");
229
239
  const dir = (0, node_path_1.dirname)(path);
230
240
  out.push({
231
241
  kind: "agent",
@@ -415,9 +425,9 @@ function tierOf(considered, tests, index, tier, globs) {
415
425
  * deterministic coverage, no evals" from "has neither" — two positions the single
416
426
  * count made indistinguishable.
417
427
  */
418
- function findUntestedSurfaces(options = {}) {
428
+ function findUntestedSurfaces(options) {
419
429
  const basePath = options.basePath ?? process.cwd();
420
- const layout = options.layout ?? layout_js_2.claudeCodeLayout;
430
+ const { layout } = options;
421
431
  const ignore = [...DEFAULT_IGNORE, ...(options.exclude ?? [])];
422
432
  const globs = options.include ?? DEFAULT_TEST_GLOBS;
423
433
  const surfaces = [];
@@ -531,7 +541,7 @@ function coverageEvidenceCounts(report) {
531
541
  * Returns `null` when the edited file isn't a skill/agent surface, or when it is
532
542
  * covered on both tiers. Never throws — a nudge must not disrupt an edit.
533
543
  */
534
- function skillTestNudge(filePath, options = {}) {
544
+ function skillTestNudge(filePath, options) {
535
545
  let report;
536
546
  try {
537
547
  report = findUntestedSurfaces({ ...options, hooks: false });
@@ -573,7 +573,7 @@ function experimental_verifyPluginGuards(dir, opts = {}) {
573
573
  // A harness whose hooks are not shell processes has nothing this tier can
574
574
  // drive. Saying so is the `no-silent-skips` half — an empty `hooks` list with
575
575
  // no note is indistinguishable from "we looked and it was fine".
576
- if (!adapter.capabilities.shellHooks || !adapter.hookProtocol)
576
+ if (!adapter.shellHooks || !adapter.hookProtocol)
577
577
  return {
578
578
  ...base,
579
579
  hooks: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "29.1.0",
3
+ "version": "30.0.1",
4
4
  "description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,68 +0,0 @@
1
- /** The plugin id `claude plugin install` records — `<plugin>@<marketplace>`. */
2
- export declare const VIGILES_PLUGIN_ID = "vigiles@vigiles";
3
- /**
4
- * Where a reachable install was found. Deliberately does NOT include a project
5
- * `enabledPlugins` entry: that declares the plugin, it does not install it (see
6
- * the header). It is reported as {@link SkillReachability.declaredNotInstalled}.
7
- */
8
- export type ReachabilitySource =
9
- /** `~/.claude/plugins/installed_plugins.json` — what `claude plugin install` writes. */
10
- "global-plugin"
11
- /** The skills vendored into the repo's own `.claude/skills/` (standalone config, always loaded). */
12
- | "repo-skills";
13
- /** The advisory: can the agent see vigiles's skills from this repo? */
14
- export interface SkillReachability {
15
- /** True when at least one {@link ReachabilitySource} was found. */
16
- readonly reachable: boolean;
17
- /** Every source that resolved, in check order. Empty when un-wired. */
18
- readonly sources: readonly ReachabilitySource[];
19
- /**
20
- * The repo's `.claude/settings.json` enables the plugin, but no install was
21
- * found. Claude Code will prompt this collaborator to install it; until they
22
- * do, it does not load. A distinct state from "nothing configured at all",
23
- * because the fix belongs to the person, not the repo.
24
- */
25
- readonly declaredNotInstalled: boolean;
26
- /**
27
- * Shipped skills found under `node_modules/vigiles/skills/` while UNREACHABLE
28
- * — present on disk, invisible to the agent. Empty when reachable (there is
29
- * nothing stranded if they are wired) or when the package isn't installed yet.
30
- */
31
- readonly strandedSkills: readonly string[];
32
- }
33
- /**
34
- * Is `vigiles` a declared dependency of this package.json text? Pure. Any of the
35
- * four dependency fields counts — the question is "did this repo take vigiles
36
- * on", not how.
37
- */
38
- export declare function declaresVigilesDependency(pkgJson: string): boolean;
39
- /**
40
- * Does the global registry record a live install of the vigiles plugin? Pure over
41
- * the raw `installed_plugins.json` text. An entry with an EMPTY array is a
42
- * leftover record, not an install, so it does not count.
43
- */
44
- export declare function hasGlobalPluginInstall(installedPluginsJson: string): boolean;
45
- /**
46
- * Does the repo's `.claude/settings.json` explicitly enable the vigiles plugin?
47
- * Pure. An explicit `false` is a deliberate disable and does NOT count.
48
- */
49
- export declare function hasEnabledPlugin(settingsJson: string): boolean;
50
- /**
51
- * Best-effort, read-local reachability check for `vigiles audit`. Returns null
52
- * when the question does not apply — the repo has no package.json, or does not
53
- * depend on vigiles, or IS vigiles — so a non-consumer is never nagged. NEVER
54
- * throws: every read degrades to "not found".
55
- *
56
- * `opts.home` overrides `$HOME` (tests, and any caller with a relocated config).
57
- */
58
- export declare function checkSkillReachability(dir: string, opts?: {
59
- readonly home?: string;
60
- }): SkillReachability | null;
61
- /**
62
- * The warning for an un-wired repo, or null when there is nothing to say
63
- * (reachable, or the check did not apply). Names the skill the user most likely
64
- * went looking for, says where the copies are stranded when they exist, and ends
65
- * on a command that fixes it.
66
- */
67
- export declare function formatSkillReachability(r: SkillReachability | null): string | null;
68
- //# sourceMappingURL=skill-reachability.d.ts.map
@@ -1,205 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VIGILES_PLUGIN_ID = void 0;
4
- exports.declaresVigilesDependency = declaresVigilesDependency;
5
- exports.hasGlobalPluginInstall = hasGlobalPluginInstall;
6
- exports.hasEnabledPlugin = hasEnabledPlugin;
7
- exports.checkSkillReachability = checkSkillReachability;
8
- exports.formatSkillReachability = formatSkillReachability;
9
- /**
10
- * Are vigiles's SHIPPED SKILLS actually reachable by the agent in this repo?
11
- *
12
- * vigiles publishes six user-facing skills (`SHIPPED_SKILLS`) — the teaching
13
- * surface. `test-harness` alone answers "which testing tier do I want?", the
14
- * question this project's docs are otherwise organized around. They reach the
15
- * agent through the GLOBAL plugin install (`vigiles init`, or
16
- * `claude plugin marketplace add zernie/vigiles` + `claude plugin install
17
- * vigiles@vigiles`), which lands in `~/.claude/plugins/` — deliberately never
18
- * vendored into the repo (`docs/agent-setup.md`).
19
- *
20
- * The failure this module makes loud: `npm install vigiles` ALSO puts those
21
- * skills on disk, at `node_modules/vigiles/skills/` (they are in package.json's
22
- * `files`, because the npm tarball doubles as the plugin payload). Claude Code
23
- * never scans `node_modules`. So a repo that took the dependency but never ran
24
- * the plugin install has all six skills present, unreachable, and **silent** —
25
- * observed in a real consumer repo, where a full day went into re-deriving what
26
- * `test-harness` teaches while it sat three directories away.
27
- *
28
- * ⚠️ The part that is easy to get wrong, and did get wrong TWICE, in opposite
29
- * directions:
30
- *
31
- * 1. The authoritative record of a `claude plugin install` is the GLOBAL
32
- * `~/.claude/plugins/installed_plugins.json`. A repo's `.claude/settings.json`
33
- * carries PROJECT-level `enabledPlugins`, which a correctly-installed
34
- * user-scope plugin does not appear in. Judging "is it wired?" from
35
- * `settings.json` alone reports a working install as broken — the misread
36
- * that made this look like an npm packaging bug.
37
- * 2. The converse is ALSO false: a project `enabledPlugins` entry does not make
38
- * the plugin load. Per the Claude Code docs (Discover plugins → "Configure
39
- * team marketplaces"), as of CC v2.1.195 — "A plugin that only the project's
40
- * `.claude/settings.json` enables, and that comes from an external source
41
- * such as a GitHub repository or npm package, doesn't load until the team
42
- * member installs it." vigiles ships from a GitHub marketplace, so that is
43
- * exactly our case. Committing `extraKnownMarketplaces` + `enabledPlugins`
44
- * makes Claude Code PROMPT each collaborator to install; it does not install.
45
- * Confirmed empirically: in a repo whose committed settings.json declares an
46
- * external plugin project-level, the global registry had no marketplace
47
- * entry, no cache dir, and no install record for it, while a plugin installed
48
- * the normal way on the same machine had all three.
49
- *
50
- * So a project declaration is a THIRD state — declared, not installed — that
51
- * still warrants a warning, with a different fix line: the collaborator runs the
52
- * install, the repo cannot run it for them.
53
- *
54
- * Shape follows `src/dialect-drift.ts`: pure parsers + a best-effort local read
55
- * that NEVER throws + a formatter that returns null when there is nothing to
56
- * say, so `vigiles audit` can print it without a new verb, flag, or failure mode.
57
- * It is ADVISORY — it never touches the audit score, because reachability is a
58
- * property of the machine (is the plugin installed?), not of the repo, and a
59
- * score that moved between a laptop and CI for identical source would be a lie.
60
- */
61
- const node_fs_1 = require("node:fs");
62
- const node_os_1 = require("node:os");
63
- const node_path_1 = require("node:path");
64
- const setup_plan_js_1 = require("./setup-plan.js");
65
- /** The plugin id `claude plugin install` records — `<plugin>@<marketplace>`. */
66
- exports.VIGILES_PLUGIN_ID = "vigiles@vigiles";
67
- /**
68
- * Is `vigiles` a declared dependency of this package.json text? Pure. Any of the
69
- * four dependency fields counts — the question is "did this repo take vigiles
70
- * on", not how.
71
- */
72
- function declaresVigilesDependency(pkgJson) {
73
- let pkg;
74
- try {
75
- pkg = JSON.parse(pkgJson);
76
- }
77
- catch {
78
- return false;
79
- }
80
- // The vigiles repo itself is not a consumer — it IS the plugin.
81
- if (pkg.name === "vigiles")
82
- return false;
83
- const fields = [
84
- "dependencies",
85
- "devDependencies",
86
- "peerDependencies",
87
- "optionalDependencies",
88
- ];
89
- return fields.some((f) => {
90
- const deps = pkg[f];
91
- return typeof deps === "object" && deps !== null && "vigiles" in deps;
92
- });
93
- }
94
- /**
95
- * Does the global registry record a live install of the vigiles plugin? Pure over
96
- * the raw `installed_plugins.json` text. An entry with an EMPTY array is a
97
- * leftover record, not an install, so it does not count.
98
- */
99
- function hasGlobalPluginInstall(installedPluginsJson) {
100
- try {
101
- const parsed = JSON.parse(installedPluginsJson);
102
- const entry = parsed.plugins?.[exports.VIGILES_PLUGIN_ID];
103
- return Array.isArray(entry) && entry.length > 0;
104
- }
105
- catch {
106
- return false;
107
- }
108
- }
109
- /**
110
- * Does the repo's `.claude/settings.json` explicitly enable the vigiles plugin?
111
- * Pure. An explicit `false` is a deliberate disable and does NOT count.
112
- */
113
- function hasEnabledPlugin(settingsJson) {
114
- try {
115
- const parsed = JSON.parse(settingsJson);
116
- return parsed.enabledPlugins?.[exports.VIGILES_PLUGIN_ID] === true;
117
- }
118
- catch {
119
- return false;
120
- }
121
- }
122
- /** Directory names directly under `dir`, or [] when it isn't readable. */
123
- function dirNames(dir) {
124
- try {
125
- return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })
126
- .filter((e) => e.isDirectory())
127
- .map((e) => e.name);
128
- }
129
- catch {
130
- return [];
131
- }
132
- }
133
- /** Read a file, or null when it isn't there / isn't readable. Never throws. */
134
- function readOrNull(path) {
135
- try {
136
- return (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, "utf-8") : null;
137
- }
138
- catch {
139
- return null;
140
- }
141
- }
142
- /**
143
- * Best-effort, read-local reachability check for `vigiles audit`. Returns null
144
- * when the question does not apply — the repo has no package.json, or does not
145
- * depend on vigiles, or IS vigiles — so a non-consumer is never nagged. NEVER
146
- * throws: every read degrades to "not found".
147
- *
148
- * `opts.home` overrides `$HOME` (tests, and any caller with a relocated config).
149
- */
150
- function checkSkillReachability(dir, opts = {}) {
151
- const pkgJson = readOrNull((0, node_path_1.join)(dir, "package.json"));
152
- if (pkgJson === null || !declaresVigilesDependency(pkgJson))
153
- return null;
154
- const home = opts.home ?? (0, node_os_1.homedir)();
155
- const sources = [];
156
- const installed = readOrNull((0, node_path_1.join)(home, ".claude", "plugins", "installed_plugins.json"));
157
- if (installed !== null && hasGlobalPluginInstall(installed))
158
- sources.push("global-plugin");
159
- // Vendored copies: only vigiles's OWN skill names count. A repo with 38
160
- // unrelated skills in `.claude/skills/` is still un-wired.
161
- const repoSkills = new Set(dirNames((0, node_path_1.join)(dir, ".claude", "skills")));
162
- if (setup_plan_js_1.SHIPPED_SKILLS.some((s) => repoSkills.has(s)))
163
- sources.push("repo-skills");
164
- const reachable = sources.length > 0;
165
- // A project declaration is NOT a source — it makes Claude Code prompt for an
166
- // install, it does not perform one. Only meaningful while unreachable.
167
- const settings = readOrNull((0, node_path_1.join)(dir, ".claude", "settings.json"));
168
- const declaredNotInstalled = !reachable && settings !== null && hasEnabledPlugin(settings);
169
- const vendored = new Set(dirNames((0, node_path_1.join)(dir, "node_modules", "vigiles", "skills")));
170
- return {
171
- reachable,
172
- sources,
173
- declaredNotInstalled,
174
- strandedSkills: reachable
175
- ? []
176
- : setup_plan_js_1.SHIPPED_SKILLS.filter((s) => vendored.has(s)),
177
- };
178
- }
179
- /**
180
- * The warning for an un-wired repo, or null when there is nothing to say
181
- * (reachable, or the check did not apply). Names the skill the user most likely
182
- * went looking for, says where the copies are stranded when they exist, and ends
183
- * on a command that fixes it.
184
- */
185
- function formatSkillReachability(r) {
186
- if (!r || r.reachable)
187
- return null;
188
- const stranded = r.strandedSkills.length > 0
189
- ? ` ${String(r.strandedSkills.length)} of them are sitting in ` +
190
- `node_modules/vigiles/skills/, which the agent never scans.`
191
- : "";
192
- const why = r.declaredNotInstalled
193
- ? `This repo DECLARES the vigiles plugin in .claude/settings.json, but a ` +
194
- `project declaration doesn't install it — Claude Code loads an ` +
195
- `external-source plugin only once each collaborator installs it on their ` +
196
- `own machine.`
197
- : `This repo depends on vigiles, but its plugin isn't installed.`;
198
- return (`⚠ vigiles's skills are NOT reachable by your agent here. ${why} So the ` +
199
- `shipped skills (${setup_plan_js_1.SHIPPED_SKILLS.join(", ")}) can't be selected — ` +
200
- `including test-harness, which picks the testing tier for you.${stranded}\n` +
201
- ` Fix (per machine): claude plugin marketplace add zernie/vigiles && ` +
202
- `claude plugin install ${exports.VIGILES_PLUGIN_ID}\n` +
203
- ` (or run \`vigiles init\`, which does both)`);
204
- }
205
- //# sourceMappingURL=skill-reachability.js.map