vigiles 2.6.0 → 4.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 (205) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +11 -2
  3. package/README.md +76 -129
  4. package/action.yml +144 -8
  5. package/dist/action-gate.d.ts +1 -1
  6. package/dist/action-gate.js +1 -1
  7. package/dist/adapter-conformance.d.ts +30 -0
  8. package/dist/adapter-conformance.js +153 -0
  9. package/dist/adapter-registry.d.ts +42 -0
  10. package/dist/adapter-registry.js +55 -0
  11. package/dist/adapter.d.ts +26 -0
  12. package/dist/adapter.js +16 -0
  13. package/dist/adapters/claude-code/adapter.d.ts +3 -0
  14. package/dist/adapters/claude-code/adapter.js +46 -0
  15. package/dist/{agent-result.d.ts → adapters/claude-code/agent-result.d.ts} +1 -1
  16. package/dist/adapters/claude-code/dialect.d.ts +13 -0
  17. package/dist/adapters/claude-code/dialect.js +51 -0
  18. package/dist/adapters/claude-code/egress-entry.d.ts +2 -0
  19. package/dist/adapters/claude-code/egress-entry.js +115 -0
  20. package/dist/adapters/claude-code/egress.d.ts +114 -0
  21. package/dist/adapters/claude-code/egress.js +276 -0
  22. package/dist/{eval-cache.d.ts → adapters/claude-code/eval-cache.d.ts} +1 -1
  23. package/dist/{eval-cache.js → adapters/claude-code/eval-cache.js} +1 -1
  24. package/dist/{eval.d.ts → adapters/claude-code/eval.d.ts} +28 -4
  25. package/dist/{eval.js → adapters/claude-code/eval.js} +61 -25
  26. package/dist/{harness-test.d.ts → adapters/claude-code/harness-test.d.ts} +33 -37
  27. package/dist/{harness-test.js → adapters/claude-code/harness-test.js} +124 -43
  28. package/dist/adapters/claude-code/hook-protocol.d.ts +10 -0
  29. package/dist/adapters/claude-code/hook-protocol.js +10 -0
  30. package/dist/adapters/claude-code/layout.d.ts +8 -0
  31. package/dist/adapters/claude-code/layout.js +18 -0
  32. package/dist/{mock-model.d.ts → adapters/claude-code/mock-model.d.ts} +2 -24
  33. package/dist/adapters/claude-code/model-mock.d.ts +11 -0
  34. package/dist/adapters/claude-code/model-mock.js +10 -0
  35. package/dist/adapters/claude-code/plugin-loader.d.ts +25 -0
  36. package/dist/adapters/claude-code/plugin-loader.js +19 -0
  37. package/dist/{run-hook.d.ts → adapters/claude-code/run-hook.d.ts} +50 -3
  38. package/dist/{run-hook.js → adapters/claude-code/run-hook.js} +178 -18
  39. package/dist/adapters/claude-code/run-scripts.d.ts +52 -0
  40. package/dist/adapters/claude-code/run-scripts.js +150 -0
  41. package/dist/adapters/claude-code/runtime.d.ts +16 -0
  42. package/dist/adapters/claude-code/runtime.js +39 -0
  43. package/dist/{sandbox.d.ts → adapters/claude-code/sandbox.d.ts} +10 -0
  44. package/dist/{sandbox.js → adapters/claude-code/sandbox.js} +5 -3
  45. package/dist/{skill-driver.d.ts → adapters/claude-code/skill-driver.d.ts} +1 -1
  46. package/dist/adapters/codex/adapter.d.ts +3 -0
  47. package/dist/adapters/codex/adapter.js +49 -0
  48. package/dist/adapters/codex/dialect.d.ts +10 -0
  49. package/dist/adapters/codex/dialect.js +30 -0
  50. package/dist/adapters/codex/driver.d.ts +20 -0
  51. package/dist/adapters/codex/driver.js +89 -0
  52. package/dist/adapters/codex/hook-protocol.d.ts +10 -0
  53. package/dist/adapters/codex/hook-protocol.js +18 -0
  54. package/dist/adapters/codex/layout.d.ts +16 -0
  55. package/dist/adapters/codex/layout.js +18 -0
  56. package/dist/adapters/codex/mock-model.d.ts +52 -0
  57. package/dist/adapters/codex/mock-model.js +210 -0
  58. package/dist/adapters/codex/model-mock.d.ts +11 -0
  59. package/dist/adapters/codex/model-mock.js +10 -0
  60. package/dist/adapters/codex/runtime.d.ts +38 -0
  61. package/dist/adapters/codex/runtime.js +62 -0
  62. package/dist/adapters/opencode/adapter.d.ts +3 -0
  63. package/dist/adapters/opencode/adapter.js +48 -0
  64. package/dist/adapters/opencode/dialect.d.ts +10 -0
  65. package/dist/adapters/opencode/dialect.js +36 -0
  66. package/dist/adapters/opencode/layout.d.ts +9 -0
  67. package/dist/adapters/opencode/layout.js +25 -0
  68. package/dist/adapters/opencode/model-mock.d.ts +10 -0
  69. package/dist/adapters/opencode/model-mock.js +10 -0
  70. package/dist/adapters/opencode/runtime.d.ts +9 -0
  71. package/dist/adapters/opencode/runtime.js +21 -0
  72. package/dist/claude-code.d.ts +8 -2
  73. package/dist/claude-code.js +8 -2
  74. package/dist/cli-flags.d.ts +22 -0
  75. package/dist/cli-flags.js +38 -0
  76. package/dist/cli.d.ts +1 -1
  77. package/dist/cli.js +670 -245
  78. package/dist/codex.d.ts +20 -0
  79. package/dist/codex.js +36 -0
  80. package/dist/community-skills.d.ts +1 -1
  81. package/dist/community-skills.js +2 -2
  82. package/dist/core/adapter.d.ts +88 -0
  83. package/dist/core/adapter.js +3 -0
  84. package/dist/{compile.d.ts → core/compile.d.ts} +11 -2
  85. package/dist/{compile.js → core/compile.js} +68 -53
  86. package/dist/core/compose.d.ts +79 -0
  87. package/dist/core/compose.js +145 -0
  88. package/dist/core/dialect.d.ts +51 -0
  89. package/dist/core/dialect.js +3 -0
  90. package/dist/core/harness-driver.d.ts +134 -0
  91. package/dist/core/harness-driver.js +3 -0
  92. package/dist/core/hook-protocol.d.ts +28 -0
  93. package/dist/core/hook-protocol.js +3 -0
  94. package/dist/core/layout.d.ts +43 -0
  95. package/dist/core/layout.js +3 -0
  96. package/dist/core/model-mock.d.ts +27 -0
  97. package/dist/core/model-mock.js +3 -0
  98. package/dist/core/refs.d.ts +62 -0
  99. package/dist/{refs.js → core/refs.js} +49 -23
  100. package/dist/core/runtime.d.ts +38 -0
  101. package/dist/core/runtime.js +3 -0
  102. package/dist/{types.d.ts → core/types.d.ts} +29 -0
  103. package/dist/{validate.d.ts → core/validate.d.ts} +1 -1
  104. package/dist/{validate.js → core/validate.js} +41 -4
  105. package/dist/e2e.d.ts +19 -0
  106. package/dist/e2e.js +39 -0
  107. package/dist/harness-assert.d.ts +29 -14
  108. package/dist/harness-assert.js +37 -10
  109. package/dist/integration.d.ts +16 -0
  110. package/dist/integration.js +32 -0
  111. package/dist/leaderboard.d.ts +33 -0
  112. package/dist/leaderboard.js +107 -0
  113. package/dist/linting.d.ts +2 -2
  114. package/dist/linting.js +2 -2
  115. package/dist/plugin-loader.d.ts +6 -3
  116. package/dist/plugin-loader.js +101 -49
  117. package/dist/scan.d.ts +49 -0
  118. package/dist/scan.js +177 -0
  119. package/dist/setup-plan.d.ts +91 -0
  120. package/dist/setup-plan.js +160 -0
  121. package/dist/skill-test.d.ts +1 -1
  122. package/dist/skill-test.js +1 -1
  123. package/dist/test-coverage.d.ts +71 -0
  124. package/dist/test-coverage.js +228 -0
  125. package/dist/testing.d.ts +3 -3
  126. package/dist/testing.js +3 -3
  127. package/dist/unit.d.ts +17 -0
  128. package/dist/unit.js +36 -0
  129. package/hooks/refs-nudge.sh +24 -0
  130. package/package.json +30 -15
  131. package/skills/edit-spec/SKILL.md +131 -0
  132. package/skills/generate-rule/SKILL.md +64 -0
  133. package/skills/linter-docs/clippy.md +241 -0
  134. package/skills/linter-docs/eslint.md +384 -0
  135. package/skills/linter-docs/pylint.md +288 -0
  136. package/skills/linter-docs/rubocop.md +277 -0
  137. package/skills/linter-docs/ruff.md +187 -0
  138. package/skills/linter-docs/stylelint.md +247 -0
  139. package/skills/migrate-to-spec/SKILL.md +126 -0
  140. package/skills/strengthen/SKILL.md +168 -0
  141. package/skills/test-harness/SKILL.md +157 -0
  142. package/dist/action.d.ts +0 -7
  143. package/dist/action.js +0 -180
  144. package/dist/refs.d.ts +0 -44
  145. package/dist/run-scripts.d.ts +0 -20
  146. package/dist/run-scripts.js +0 -70
  147. /package/dist/{agent-result.js → adapters/claude-code/agent-result.js} +0 -0
  148. /package/dist/{agent-runtime.d.ts → adapters/claude-code/agent-runtime.d.ts} +0 -0
  149. /package/dist/{agent-runtime.js → adapters/claude-code/agent-runtime.js} +0 -0
  150. /package/dist/{egress-proxy.d.ts → adapters/claude-code/egress-proxy.d.ts} +0 -0
  151. /package/dist/{egress-proxy.js → adapters/claude-code/egress-proxy.js} +0 -0
  152. /package/dist/{eval-baseline.d.ts → adapters/claude-code/eval-baseline.d.ts} +0 -0
  153. /package/dist/{eval-baseline.js → adapters/claude-code/eval-baseline.js} +0 -0
  154. /package/dist/{judge.d.ts → adapters/claude-code/judge.d.ts} +0 -0
  155. /package/dist/{judge.js → adapters/claude-code/judge.js} +0 -0
  156. /package/dist/{mock-entry.d.ts → adapters/claude-code/mock-entry.d.ts} +0 -0
  157. /package/dist/{mock-entry.js → adapters/claude-code/mock-entry.js} +0 -0
  158. /package/dist/{mock-model.js → adapters/claude-code/mock-model.js} +0 -0
  159. /package/dist/{skill-driver.js → adapters/claude-code/skill-driver.js} +0 -0
  160. /package/dist/{skill-runtime.d.ts → adapters/claude-code/skill-runtime.d.ts} +0 -0
  161. /package/dist/{skill-runtime.js → adapters/claude-code/skill-runtime.js} +0 -0
  162. /package/dist/{stats.d.ts → adapters/claude-code/stats.d.ts} +0 -0
  163. /package/dist/{stats.js → adapters/claude-code/stats.js} +0 -0
  164. /package/dist/{compile-generator.d.ts → core/compile-generator.d.ts} +0 -0
  165. /package/dist/{compile-generator.js → core/compile-generator.js} +0 -0
  166. /package/dist/{coverage.d.ts → core/coverage.d.ts} +0 -0
  167. /package/dist/{coverage.js → core/coverage.js} +0 -0
  168. /package/dist/{doc-refs.d.ts → core/doc-refs.d.ts} +0 -0
  169. /package/dist/{doc-refs.js → core/doc-refs.js} +0 -0
  170. /package/dist/{evolve.d.ts → core/evolve.d.ts} +0 -0
  171. /package/dist/{evolve.js → core/evolve.js} +0 -0
  172. /package/dist/{frontmatter.d.ts → core/frontmatter.d.ts} +0 -0
  173. /package/dist/{frontmatter.js → core/frontmatter.js} +0 -0
  174. /package/dist/{generate-schema.d.ts → core/generate-schema.d.ts} +0 -0
  175. /package/dist/{generate-schema.js → core/generate-schema.js} +0 -0
  176. /package/dist/{generate-types.d.ts → core/generate-types.d.ts} +0 -0
  177. /package/dist/{generate-types.js → core/generate-types.js} +0 -0
  178. /package/dist/{hash.d.ts → core/hash.d.ts} +0 -0
  179. /package/dist/{hash.js → core/hash.js} +0 -0
  180. /package/dist/{inline.d.ts → core/inline.d.ts} +0 -0
  181. /package/dist/{inline.js → core/inline.js} +0 -0
  182. /package/dist/{integrity.d.ts → core/integrity.d.ts} +0 -0
  183. /package/dist/{integrity.js → core/integrity.js} +0 -0
  184. /package/dist/{linters.d.ts → core/linters.d.ts} +0 -0
  185. /package/dist/{linters.js → core/linters.js} +0 -0
  186. /package/dist/{mcp.d.ts → core/mcp.d.ts} +0 -0
  187. /package/dist/{mcp.js → core/mcp.js} +0 -0
  188. /package/dist/{orphans.d.ts → core/orphans.d.ts} +0 -0
  189. /package/dist/{orphans.js → core/orphans.js} +0 -0
  190. /package/dist/{proofs.d.ts → core/proofs.d.ts} +0 -0
  191. /package/dist/{proofs.js → core/proofs.js} +0 -0
  192. /package/dist/{session.d.ts → core/session.d.ts} +0 -0
  193. /package/dist/{session.js → core/session.js} +0 -0
  194. /package/dist/{sidecar.d.ts → core/sidecar.d.ts} +0 -0
  195. /package/dist/{sidecar.js → core/sidecar.js} +0 -0
  196. /package/dist/{spec.d.ts → core/spec.d.ts} +0 -0
  197. /package/dist/{spec.js → core/spec.js} +0 -0
  198. /package/dist/{symbols.d.ts → core/symbols.d.ts} +0 -0
  199. /package/dist/{symbols.js → core/symbols.js} +0 -0
  200. /package/dist/{test-utils.d.ts → core/test-utils.d.ts} +0 -0
  201. /package/dist/{test-utils.js → core/test-utils.js} +0 -0
  202. /package/dist/{types.js → core/types.js} +0 -0
  203. /package/{.claude-plugin/hooks → hooks}/post-edit.sh +0 -0
  204. /package/{.claude-plugin/hooks → hooks}/pre-edit.sh +0 -0
  205. /package/{.claude-plugin/hooks → hooks}/session-start.sh +0 -0
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ /**
3
+ * Pure decision logic for `vigiles init`.
4
+ *
5
+ * Turns CLI args + whether a human's at a TTY into a concrete setup PLAN, so the
6
+ * IO-heavy `setup()` in cli.ts stays a thin shell and every choice is unit-tested.
7
+ * Best-practice onboarding: **interactive** when a human runs it in a terminal,
8
+ * **non-interactive** (sensible defaults) for agents / CI / piped input — so
9
+ * "set up vigiles" from a Claude Code or Codex prompt Just Works without hanging
10
+ * on a prompt. See docs/agent-setup.md.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.parseSetupArgs = parseSetupArgs;
14
+ exports.defaultPlan = defaultPlan;
15
+ exports.shouldPrompt = shouldPrompt;
16
+ exports.planPluginInstall = planPluginInstall;
17
+ exports.resolvePlan = resolvePlan;
18
+ function flagValue(args, prefix) {
19
+ return args.find((a) => a.startsWith(prefix))?.slice(prefix.length);
20
+ }
21
+ /** `--name` → true, `--no-name` → false, neither present → undefined. */
22
+ function boolFlag(args, name) {
23
+ if (args.includes(`--${name}`))
24
+ return true;
25
+ if (args.includes(`--no-${name}`))
26
+ return false;
27
+ return undefined;
28
+ }
29
+ /** Parse `init` args into the choices the user pinned. The two pillars are
30
+ * selected with `--lint` / `--test`. */
31
+ function parseSetupArgs(args) {
32
+ return {
33
+ target: flagValue(args, "--target="),
34
+ strict: args.includes("--strict"),
35
+ yes: args.includes("--yes") || args.includes("-y"),
36
+ lint: boolFlag(args, "lint"),
37
+ test: boolFlag(args, "test"),
38
+ harness: flagValue(args, "--harness="),
39
+ gha: args.includes("--no-gha") ? false : undefined,
40
+ plugin: args.includes("--no-plugin") ? false : undefined,
41
+ };
42
+ }
43
+ /** The non-interactive defaults: both pillars, CI, and the plugin. */
44
+ function defaultPlan(strict = false) {
45
+ return { lint: true, test: true, gha: true, plugin: true, strict };
46
+ }
47
+ /**
48
+ * Whether to drop into interactive prompts: a human at a TTY who passed neither
49
+ * `--yes` nor an explicit `--target`, and who hasn't already pinned every choice
50
+ * via flags. Agents / CI / piped input (no TTY) never prompt.
51
+ */
52
+ function shouldPrompt(parsed, isTTY) {
53
+ if (!isTTY || parsed.yes || parsed.target)
54
+ return false;
55
+ const pillarsPinned = parsed.lint !== undefined || parsed.test !== undefined;
56
+ const allPinned = pillarsPinned && parsed.gha !== undefined && parsed.plugin !== undefined;
57
+ return !allPinned;
58
+ }
59
+ /**
60
+ * Apply the pillar flags. A positive flag (`--lint` and/or `--test`) is an
61
+ * explicit SELECTION — enable exactly the named pillars. Otherwise default to
62
+ * both and let a `--no-*` flag drop one.
63
+ */
64
+ function applyPillarFlags(plan, parsed) {
65
+ if (parsed.lint === true || parsed.test === true) {
66
+ plan.lint = parsed.lint === true;
67
+ plan.test = parsed.test === true;
68
+ return;
69
+ }
70
+ if (parsed.lint === false)
71
+ plan.lint = false;
72
+ if (parsed.test === false)
73
+ plan.test = false;
74
+ }
75
+ function applyAnswers(plan, answers) {
76
+ if (answers.lint !== undefined)
77
+ plan.lint = answers.lint;
78
+ if (answers.test !== undefined)
79
+ plan.test = answers.test;
80
+ if (answers.gha !== undefined)
81
+ plan.gha = answers.gha;
82
+ if (answers.plugin !== undefined)
83
+ plan.plugin = answers.plugin;
84
+ }
85
+ /** Per-harness install plan. `hasClaude` gates the auto-run `claude plugin` CLI
86
+ * (else the same two steps are printed as `/plugin` slash commands).
87
+ *
88
+ * Both methods install GLOBALLY, never into the repo: Claude through its plugin
89
+ * marketplace (~/.claude/plugins/), Codex through the cross-agent `skills` CLI
90
+ * with `-g -y` (the global store ~/.agents/skills/, which Codex reads). Codex
91
+ * gets the skills but NOT hooks — Codex hook wiring (.codex/config.toml [hooks])
92
+ * is not automated yet. */
93
+ function planPluginInstall(harnesses, opts) {
94
+ return harnesses.map((harness) => {
95
+ if (harness === "claude") {
96
+ return {
97
+ harness,
98
+ commands: opts.hasClaude
99
+ ? [
100
+ "claude plugin marketplace add zernie/vigiles",
101
+ "claude plugin install vigiles@vigiles",
102
+ ]
103
+ : [],
104
+ successMessage: "✓ Installed the vigiles plugin (hooks + skills) into ~/.claude/plugins/",
105
+ manualSteps: [
106
+ "/plugin marketplace add zernie/vigiles",
107
+ "/plugin install vigiles@vigiles",
108
+ ],
109
+ notes: [
110
+ "Installs globally to ~/.claude/plugins/ — nothing is added to your repo.",
111
+ ],
112
+ vendors: false,
113
+ };
114
+ }
115
+ if (harness === "codex") {
116
+ // The cross-agent `skills` CLI with `-g -y` installs to the global store
117
+ // ~/.agents/skills/ (NOT the repo, and NOT ~/.codex/ — verified against
118
+ // the real CLI). Skills only; Codex hooks (.codex/config.toml [hooks])
119
+ // are not wired automatically.
120
+ return {
121
+ harness,
122
+ commands: ["npx --yes skills add zernie/vigiles -a codex -g -y"],
123
+ successMessage: "✓ Installed the vigiles skills into ~/.agents/skills/ (global, not vendored)",
124
+ manualSteps: ["npx skills add zernie/vigiles -a codex -g -y"],
125
+ notes: [
126
+ "Codex reads AGENTS.md directly; the skills install globally to ~/.agents/skills/ (not the repo).",
127
+ "Codex hooks (.codex/config.toml [hooks]) are not auto-wired yet — add them manually for compile-on-edit.",
128
+ ],
129
+ vendors: false,
130
+ };
131
+ }
132
+ return {
133
+ harness,
134
+ commands: [],
135
+ successMessage: "",
136
+ manualSteps: [],
137
+ notes: [`No plugin install path for harness '${harness}'.`],
138
+ vendors: false,
139
+ };
140
+ });
141
+ }
142
+ /**
143
+ * Resolve the final plan: defaults, then flags, then interactive answers (each
144
+ * layer overrides the previous only where it has an opinion). `--target` pins a
145
+ * bare lint-pillar spec (no harness scaffold).
146
+ */
147
+ function resolvePlan(parsed, answers) {
148
+ const plan = defaultPlan(parsed.strict);
149
+ applyPillarFlags(plan, parsed);
150
+ if (parsed.gha === false)
151
+ plan.gha = false;
152
+ if (parsed.plugin === false)
153
+ plan.plugin = false;
154
+ if (parsed.target)
155
+ plan.test = false;
156
+ if (answers)
157
+ applyAnswers(plan, answers);
158
+ return plan;
159
+ }
160
+ //# sourceMappingURL=setup-plan.js.map
@@ -8,7 +8,7 @@
8
8
  * gate blocked. The model's prose quality is never asserted — that's the
9
9
  * probabilistic boundary; everything else is deterministic and checkable.
10
10
  */
11
- import { type SkillProgram, type GeneratorSkill } from "./skill-driver.js";
11
+ import { type SkillProgram, type GeneratorSkill } from "./adapters/claude-code/skill-driver.js";
12
12
  export type ModelFn = (prose: string) => string;
13
13
  /**
14
14
  * Script the mocked model. Pass:
@@ -12,7 +12,7 @@
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.scriptModel = scriptModel;
14
14
  exports.runSkill = runSkill;
15
- const skill_driver_js_1 = require("./skill-driver.js");
15
+ const skill_driver_js_1 = require("./adapters/claude-code/skill-driver.js");
16
16
  function gateLabel(g) {
17
17
  if (g._ref === "cmd")
18
18
  return g.command;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Untested-surface detection: the third gap detector, alongside orphan-docs.
3
+ *
4
+ * Stale-ref detection catches specs pointing at files that vanished.
5
+ * Orphan-docs catches docs nothing references. This catches harness *surfaces*
6
+ * — skills, subagents, and hooks — that ship without a test or eval. A surface
7
+ * with no test is a probabilistic-compliance gap hiding in the deterministic
8
+ * layer: nothing measures whether it still does what it claims.
9
+ *
10
+ * Two detectors decide "tested", OR'd, so a test placed ANYWHERE counts:
11
+ * 1. colocation — a `*.{harness,eval}.mjs` next to the surface (the zero-config
12
+ * convention the warning suggests): `skills/foo/*.eval.mjs`,
13
+ * `agents/bar.harness.mjs`, `hooks/pre-edit.harness.mjs`.
14
+ * 2. content-reference — any discovered test (incl. `*.test.ts`) that names the
15
+ * surface by PATH (`skills/foo`, `hooks/pre-edit.sh`) or NAMESPACE
16
+ * (`vigiles:foo`). Not bare-name — too fuzzy.
17
+ *
18
+ * Warning-by-default (a nudge, not a gate). User-invoked skills
19
+ * (`disable-model-invocation: true`) can't auto-trigger, so they're exempt by
20
+ * default; flip `includeUserInvokedSkills` to demand an outcome test for them.
21
+ * Per-surface opt-out: a `vigiles:ignore-test` marker in the surface file.
22
+ */
23
+ export type SurfaceKind = "skill" | "agent" | "hook";
24
+ export interface Surface {
25
+ readonly kind: SurfaceKind;
26
+ /** Repo-relative path to the surface file (SKILL.md / agent .md / hook script). */
27
+ readonly path: string;
28
+ /** Stable name: skill dir, agent basename, or hook script basename. */
29
+ readonly name: string;
30
+ /** Substrings a test may reference to "cover" this surface (path / namespace). */
31
+ readonly tokens: readonly string[];
32
+ /** True for skills with `disable-model-invocation: true`. */
33
+ readonly userInvoked: boolean;
34
+ }
35
+ export interface UntestedReport {
36
+ /** Total surfaces considered (after exemptions). */
37
+ readonly total: number;
38
+ readonly covered: readonly Surface[];
39
+ readonly untested: readonly Surface[];
40
+ /** Surfaces skipped (user-invoked skills, `vigiles:ignore-test`). */
41
+ readonly exempt: number;
42
+ }
43
+ export interface TestCoverageOptions {
44
+ /** Repository root. Defaults to `process.cwd()`. */
45
+ readonly basePath?: string;
46
+ /** Scan skills under `skills/` and `.claude/skills/`. Default true. */
47
+ readonly skills?: boolean;
48
+ /** Scan subagents under `agents/` and `.claude/agents/`. Default true. */
49
+ readonly agents?: boolean;
50
+ /** Scan hook scripts referenced from plugin.json / settings.json. Default true. */
51
+ readonly hooks?: boolean;
52
+ /** Require a test for user-invoked (disable-model-invocation) skills. Default false. */
53
+ readonly includeUserInvokedSkills?: boolean;
54
+ /** Globs of test files that count as coverage. */
55
+ readonly testGlobs?: readonly string[];
56
+ /** Extra ignore globs (added to node_modules/dist/.git/.vigiles). */
57
+ readonly exclude?: readonly string[];
58
+ }
59
+ /**
60
+ * Find harness surfaces (skills / agents / hooks) that no test or eval covers.
61
+ * A surface is covered by a colocated `*.{harness,eval}.mjs` OR any discovered
62
+ * test that references its path/namespace. User-invoked skills are exempt unless
63
+ * `includeUserInvokedSkills` is set; any surface with a `vigiles:ignore-test`
64
+ * marker is exempt.
65
+ */
66
+ export declare function findUntestedSurfaces(options?: TestCoverageOptions): UntestedReport;
67
+ /** Suggested colocated test path for an untested surface (shown in the warning). */
68
+ export declare function suggestedTestPath(surface: Surface): string;
69
+ /** Format an untested-surface report as human-readable text. */
70
+ export declare function formatUntestedReport(report: UntestedReport): string;
71
+ //# sourceMappingURL=test-coverage.d.ts.map
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ /**
3
+ * Untested-surface detection: the third gap detector, alongside orphan-docs.
4
+ *
5
+ * Stale-ref detection catches specs pointing at files that vanished.
6
+ * Orphan-docs catches docs nothing references. This catches harness *surfaces*
7
+ * — skills, subagents, and hooks — that ship without a test or eval. A surface
8
+ * with no test is a probabilistic-compliance gap hiding in the deterministic
9
+ * layer: nothing measures whether it still does what it claims.
10
+ *
11
+ * Two detectors decide "tested", OR'd, so a test placed ANYWHERE counts:
12
+ * 1. colocation — a `*.{harness,eval}.mjs` next to the surface (the zero-config
13
+ * convention the warning suggests): `skills/foo/*.eval.mjs`,
14
+ * `agents/bar.harness.mjs`, `hooks/pre-edit.harness.mjs`.
15
+ * 2. content-reference — any discovered test (incl. `*.test.ts`) that names the
16
+ * surface by PATH (`skills/foo`, `hooks/pre-edit.sh`) or NAMESPACE
17
+ * (`vigiles:foo`). Not bare-name — too fuzzy.
18
+ *
19
+ * Warning-by-default (a nudge, not a gate). User-invoked skills
20
+ * (`disable-model-invocation: true`) can't auto-trigger, so they're exempt by
21
+ * default; flip `includeUserInvokedSkills` to demand an outcome test for them.
22
+ * Per-surface opt-out: a `vigiles:ignore-test` marker in the surface file.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.findUntestedSurfaces = findUntestedSurfaces;
26
+ exports.suggestedTestPath = suggestedTestPath;
27
+ exports.formatUntestedReport = formatUntestedReport;
28
+ const node_fs_1 = require("node:fs");
29
+ const node_path_1 = require("node:path");
30
+ const glob_1 = require("glob");
31
+ // ---------------------------------------------------------------------------
32
+ // Internals
33
+ // ---------------------------------------------------------------------------
34
+ const DEFAULT_TEST_GLOBS = [
35
+ "**/*.harness.mjs",
36
+ "**/*.eval.mjs",
37
+ "**/*.test.ts",
38
+ "**/*.test.mts",
39
+ "**/*.test.cts",
40
+ "**/*.test.js",
41
+ "**/*.test.mjs",
42
+ "**/*.test.cjs",
43
+ ];
44
+ const DEFAULT_IGNORE = [
45
+ "node_modules/**",
46
+ "dist/**",
47
+ ".vigiles/**",
48
+ ".git/**",
49
+ ];
50
+ const IGNORE_MARKER = "vigiles:ignore-test";
51
+ const SCRIPT_RE = /[\w./${}@-]+\.(?:sh|mjs|cjs|js|ts|py|rb)/g;
52
+ function read(path) {
53
+ try {
54
+ return (0, node_fs_1.readFileSync)(path, "utf-8");
55
+ }
56
+ catch {
57
+ return "";
58
+ }
59
+ }
60
+ /** A SKILL.md is user-invoked when its frontmatter sets disable-model-invocation. */
61
+ function isUserInvoked(content) {
62
+ return /^\s*disable-model-invocation:\s*true\s*$/m.test(content);
63
+ }
64
+ function discoverSkills(basePath, ignore) {
65
+ const out = [];
66
+ const found = (0, glob_1.globSync)(["skills/*/SKILL.md", ".claude/skills/*/SKILL.md"], {
67
+ cwd: basePath,
68
+ ignore,
69
+ });
70
+ for (const path of found.sort()) {
71
+ const name = (0, node_path_1.basename)((0, node_path_1.dirname)(path));
72
+ const content = read((0, node_path_1.join)(basePath, path));
73
+ if (content.includes(IGNORE_MARKER))
74
+ continue;
75
+ out.push({
76
+ kind: "skill",
77
+ path,
78
+ name,
79
+ tokens: [`skills/${name}`, `:${name}`],
80
+ userInvoked: isUserInvoked(content),
81
+ });
82
+ }
83
+ return out;
84
+ }
85
+ function discoverAgents(basePath, ignore) {
86
+ const out = [];
87
+ const found = (0, glob_1.globSync)(["agents/*.md", ".claude/agents/*.md"], {
88
+ cwd: basePath,
89
+ ignore,
90
+ });
91
+ for (const path of found.sort()) {
92
+ if (path.endsWith(".spec.ts"))
93
+ continue;
94
+ const content = read((0, node_path_1.join)(basePath, path));
95
+ if (content.includes(IGNORE_MARKER))
96
+ continue;
97
+ const name = (0, node_path_1.basename)(path, ".md");
98
+ const dir = (0, node_path_1.dirname)(path);
99
+ out.push({
100
+ kind: "agent",
101
+ path,
102
+ name,
103
+ tokens: [`${dir}/${name}`],
104
+ userInvoked: false,
105
+ });
106
+ }
107
+ return out;
108
+ }
109
+ /** Hook-script paths referenced from a manifest's `hooks` block (file hooks only). */
110
+ function hookScripts(basePath, manifest) {
111
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(basePath, manifest)))
112
+ return [];
113
+ let hooks;
114
+ try {
115
+ hooks = JSON.parse(read((0, node_path_1.join)(basePath, manifest)))
116
+ .hooks;
117
+ }
118
+ catch {
119
+ return [];
120
+ }
121
+ if (hooks === undefined)
122
+ return [];
123
+ const text = JSON.stringify(hooks);
124
+ const scripts = new Set();
125
+ for (const m of text.matchAll(SCRIPT_RE)) {
126
+ const rel = m[0]
127
+ .replace("${CLAUDE_PLUGIN_ROOT}/", "")
128
+ .replace(/^\$\{CLAUDE_PLUGIN_ROOT\}/, "")
129
+ .replace(/^\.\//, "");
130
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(basePath, rel)))
131
+ scripts.add(rel);
132
+ }
133
+ return [...scripts];
134
+ }
135
+ function discoverHooks(basePath) {
136
+ const scripts = new Set();
137
+ for (const m of [
138
+ ".claude-plugin/plugin.json",
139
+ ".claude/settings.json",
140
+ ".claude/settings.local.json",
141
+ ]) {
142
+ for (const s of hookScripts(basePath, m))
143
+ scripts.add(s);
144
+ }
145
+ return [...scripts].sort().map((path) => ({
146
+ kind: "hook",
147
+ path,
148
+ name: (0, node_path_1.basename)(path).replace(/\.[^.]+$/, ""),
149
+ tokens: [path],
150
+ userInvoked: false,
151
+ }));
152
+ }
153
+ function discoverTests(basePath, globs, ignore) {
154
+ const found = (0, glob_1.globSync)([...globs], { cwd: basePath, ignore });
155
+ return found.map((path) => ({ path, content: read((0, node_path_1.join)(basePath, path)) }));
156
+ }
157
+ /** Colocated: a test inside a skill dir, or a name-prefixed sibling of an agent/hook. */
158
+ function isColocated(surface, testPath) {
159
+ if (surface.kind === "skill") {
160
+ return testPath.startsWith(`${(0, node_path_1.dirname)(surface.path)}/`);
161
+ }
162
+ return ((0, node_path_1.dirname)(testPath) === (0, node_path_1.dirname)(surface.path) &&
163
+ (0, node_path_1.basename)(testPath).startsWith(`${surface.name}.`));
164
+ }
165
+ function isCovered(surface, tests) {
166
+ for (const t of tests) {
167
+ if (t.path === surface.path)
168
+ continue;
169
+ if (isColocated(surface, t.path))
170
+ return true;
171
+ if (surface.tokens.some((tok) => t.content.includes(tok)))
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Public API
178
+ // ---------------------------------------------------------------------------
179
+ /**
180
+ * Find harness surfaces (skills / agents / hooks) that no test or eval covers.
181
+ * A surface is covered by a colocated `*.{harness,eval}.mjs` OR any discovered
182
+ * test that references its path/namespace. User-invoked skills are exempt unless
183
+ * `includeUserInvokedSkills` is set; any surface with a `vigiles:ignore-test`
184
+ * marker is exempt.
185
+ */
186
+ function findUntestedSurfaces(options = {}) {
187
+ const basePath = options.basePath ?? process.cwd();
188
+ const ignore = [...DEFAULT_IGNORE, ...(options.exclude ?? [])];
189
+ const globs = options.testGlobs ?? DEFAULT_TEST_GLOBS;
190
+ const surfaces = [];
191
+ if (options.skills !== false)
192
+ surfaces.push(...discoverSkills(basePath, ignore));
193
+ if (options.agents !== false)
194
+ surfaces.push(...discoverAgents(basePath, ignore));
195
+ if (options.hooks !== false)
196
+ surfaces.push(...discoverHooks(basePath));
197
+ const considered = surfaces.filter((s) => !s.userInvoked || s.kind !== "skill" || options.includeUserInvokedSkills);
198
+ const exempt = surfaces.length - considered.length;
199
+ const tests = discoverTests(basePath, globs, ignore);
200
+ const covered = [];
201
+ const untested = [];
202
+ for (const s of considered) {
203
+ (isCovered(s, tests) ? covered : untested).push(s);
204
+ }
205
+ return { total: considered.length, covered, untested, exempt };
206
+ }
207
+ /** Suggested colocated test path for an untested surface (shown in the warning). */
208
+ function suggestedTestPath(surface) {
209
+ if (surface.kind === "skill") {
210
+ return `${(0, node_path_1.dirname)(surface.path)}/${surface.name}.eval.mjs`;
211
+ }
212
+ return `${(0, node_path_1.dirname)(surface.path)}/${surface.name}.harness.mjs`;
213
+ }
214
+ /** Format an untested-surface report as human-readable text. */
215
+ function formatUntestedReport(report) {
216
+ if (report.untested.length === 0) {
217
+ const tail = report.exempt > 0 ? ` (${String(report.exempt)} exempt)` : "";
218
+ return `✓ all ${String(report.total)} surface(s) have a test or eval${tail}`;
219
+ }
220
+ const lines = [
221
+ `⚠ ${String(report.untested.length)} surface(s) with no test or eval:`,
222
+ ];
223
+ for (const s of report.untested) {
224
+ lines.push(` ${s.kind} ${s.path} — add e.g. ${suggestedTestPath(s)}`);
225
+ }
226
+ return lines.join("\n");
227
+ }
228
+ //# sourceMappingURL=test-coverage.js.map
package/dist/testing.d.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * separate from `vigiles/claude-code` so this surface can stay harness-agnostic as
6
6
  * more harnesses are added. Granular paths (`vigiles/run-hook`, etc.) still work.
7
7
  */
8
- export * from "./run-hook.js";
9
- export * from "./harness-test.js";
10
- export * from "./eval.js";
8
+ export * from "./adapters/claude-code/run-hook.js";
9
+ export * from "./adapters/claude-code/harness-test.js";
10
+ export * from "./adapters/claude-code/eval.js";
11
11
  export * from "./harness-assert.js";
12
12
  //# sourceMappingURL=testing.d.ts.map
package/dist/testing.js CHANGED
@@ -21,8 +21,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  * separate from `vigiles/claude-code` so this surface can stay harness-agnostic as
22
22
  * more harnesses are added. Granular paths (`vigiles/run-hook`, etc.) still work.
23
23
  */
24
- __exportStar(require("./run-hook.js"), exports);
25
- __exportStar(require("./harness-test.js"), exports);
26
- __exportStar(require("./eval.js"), exports);
24
+ __exportStar(require("./adapters/claude-code/run-hook.js"), exports);
25
+ __exportStar(require("./adapters/claude-code/harness-test.js"), exports);
26
+ __exportStar(require("./adapters/claude-code/eval.js"), exports);
27
27
  __exportStar(require("./harness-assert.js"), exports);
28
28
  //# sourceMappingURL=testing.js.map
package/dist/unit.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `vigiles/unit` — the **no-capability** harness-testing surface.
3
+ *
4
+ * Importing from here is a contract: this test needs **no `claude`, no model, no
5
+ * bubblewrap, no network** — only a process. It is the cheap base of the pyramid:
6
+ * `runHook` (pipe a synthesized event to a hook and read the block/allow
7
+ * decision), the bare `Trace` predicates, the `assert*` helpers, and the pure
8
+ * parsers. A `*.test.ts` should import **only** from here (enforced by lint).
9
+ *
10
+ * Higher tiers re-export this one — dependencies point downward only
11
+ * (e2e → integration → unit), never up. See `vigiles/integration` and
12
+ * `vigiles/e2e`.
13
+ */
14
+ export * from "./harness-assert.js";
15
+ export { runHook, parseHookOutput, decideHook, } from "./adapters/claude-code/run-hook.js";
16
+ export type { HookInput, HookOutput, HookRunResult, RunHookOptions, } from "./adapters/claude-code/run-hook.js";
17
+ //# sourceMappingURL=unit.d.ts.map
package/dist/unit.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.decideHook = exports.parseHookOutput = exports.runHook = void 0;
18
+ /**
19
+ * `vigiles/unit` — the **no-capability** harness-testing surface.
20
+ *
21
+ * Importing from here is a contract: this test needs **no `claude`, no model, no
22
+ * bubblewrap, no network** — only a process. It is the cheap base of the pyramid:
23
+ * `runHook` (pipe a synthesized event to a hook and read the block/allow
24
+ * decision), the bare `Trace` predicates, the `assert*` helpers, and the pure
25
+ * parsers. A `*.test.ts` should import **only** from here (enforced by lint).
26
+ *
27
+ * Higher tiers re-export this one — dependencies point downward only
28
+ * (e2e → integration → unit), never up. See `vigiles/integration` and
29
+ * `vigiles/e2e`.
30
+ */
31
+ __exportStar(require("./harness-assert.js"), exports);
32
+ var run_hook_js_1 = require("./adapters/claude-code/run-hook.js");
33
+ Object.defineProperty(exports, "runHook", { enumerable: true, get: function () { return run_hook_js_1.runHook; } });
34
+ Object.defineProperty(exports, "parseHookOutput", { enumerable: true, get: function () { return run_hook_js_1.parseHookOutput; } });
35
+ Object.defineProperty(exports, "decideHook", { enumerable: true, get: function () { return run_hook_js_1.decideHook; } });
36
+ //# sourceMappingURL=unit.js.map
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ # PostToolUse hook — nudge the agent to express references in instruction files
3
+ # (CLAUDE.md / AGENTS.md / SKILL.md) as vigiles marks, so `vigiles audit` can
4
+ # actually verify them. Non-blocking by default; set the `unmarked-refs` rule to
5
+ # "error" in .vigilesrc.json to turn the nudge into a hard block, or to false to
6
+ # disable it. Runs as its OWN PostToolUse entry so its stdout stays clean JSON.
7
+
8
+ set -uo pipefail
9
+
10
+ INPUT=$(cat)
11
+
12
+ # No npx / not a Node project → nothing to do, never disrupt the edit.
13
+ command -v npx >/dev/null 2>&1 || exit 0
14
+ [ -f package.json ] || exit 0
15
+
16
+ printf '%s' "$INPUT" | npx vigiles refs-hook
17
+ status=${PIPESTATUS[1]}
18
+
19
+ # Propagate ONLY an explicit block (exit 2). Swallow tool/setup errors so a
20
+ # missing dep or transient failure never blocks the agent's edit.
21
+ if [ "$status" -eq 2 ]; then
22
+ exit 2
23
+ fi
24
+ exit 0