vigiles 2.5.0 → 3.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 (209) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/README.md +73 -434
  3. package/action.yml +143 -7
  4. package/dist/action-gate.d.ts +1 -1
  5. package/dist/action-gate.js +1 -1
  6. package/dist/adapter-conformance.d.ts +30 -0
  7. package/dist/adapter-conformance.js +153 -0
  8. package/dist/adapter-registry.d.ts +42 -0
  9. package/dist/adapter-registry.js +55 -0
  10. package/dist/adapter.d.ts +26 -0
  11. package/dist/adapter.js +16 -0
  12. package/dist/adapters/claude-code/adapter.d.ts +3 -0
  13. package/dist/adapters/claude-code/adapter.js +46 -0
  14. package/dist/{agent-result.d.ts → adapters/claude-code/agent-result.d.ts} +1 -1
  15. package/dist/adapters/claude-code/dialect.d.ts +13 -0
  16. package/dist/adapters/claude-code/dialect.js +51 -0
  17. package/dist/adapters/claude-code/egress-entry.d.ts +2 -0
  18. package/dist/adapters/claude-code/egress-entry.js +115 -0
  19. package/dist/adapters/claude-code/egress-proxy.d.ts +2 -0
  20. package/dist/adapters/claude-code/egress-proxy.js +60 -0
  21. package/dist/adapters/claude-code/egress.d.ts +114 -0
  22. package/dist/adapters/claude-code/egress.js +276 -0
  23. package/dist/adapters/claude-code/eval-baseline.d.ts +68 -0
  24. package/dist/adapters/claude-code/eval-baseline.js +173 -0
  25. package/dist/{eval-cache.d.ts → adapters/claude-code/eval-cache.d.ts} +1 -1
  26. package/dist/{eval-cache.js → adapters/claude-code/eval-cache.js} +1 -1
  27. package/dist/{eval.d.ts → adapters/claude-code/eval.d.ts} +28 -4
  28. package/dist/{eval.js → adapters/claude-code/eval.js} +61 -25
  29. package/dist/{harness-test.d.ts → adapters/claude-code/harness-test.d.ts} +33 -37
  30. package/dist/{harness-test.js → adapters/claude-code/harness-test.js} +124 -43
  31. package/dist/adapters/claude-code/hook-protocol.d.ts +10 -0
  32. package/dist/adapters/claude-code/hook-protocol.js +10 -0
  33. package/dist/adapters/claude-code/layout.d.ts +8 -0
  34. package/dist/adapters/claude-code/layout.js +18 -0
  35. package/dist/{mock-model.d.ts → adapters/claude-code/mock-model.d.ts} +2 -24
  36. package/dist/adapters/claude-code/model-mock.d.ts +11 -0
  37. package/dist/adapters/claude-code/model-mock.js +10 -0
  38. package/dist/adapters/claude-code/plugin-loader.d.ts +25 -0
  39. package/dist/adapters/claude-code/plugin-loader.js +19 -0
  40. package/dist/adapters/claude-code/run-hook.d.ts +204 -0
  41. package/dist/adapters/claude-code/run-hook.js +418 -0
  42. package/dist/adapters/claude-code/run-scripts.d.ts +52 -0
  43. package/dist/adapters/claude-code/run-scripts.js +150 -0
  44. package/dist/adapters/claude-code/runtime.d.ts +16 -0
  45. package/dist/adapters/claude-code/runtime.js +39 -0
  46. package/dist/{sandbox.d.ts → adapters/claude-code/sandbox.d.ts} +45 -4
  47. package/dist/{sandbox.js → adapters/claude-code/sandbox.js} +76 -8
  48. package/dist/{skill-driver.d.ts → adapters/claude-code/skill-driver.d.ts} +1 -1
  49. package/dist/adapters/codex/adapter.d.ts +3 -0
  50. package/dist/adapters/codex/adapter.js +49 -0
  51. package/dist/adapters/codex/dialect.d.ts +10 -0
  52. package/dist/adapters/codex/dialect.js +30 -0
  53. package/dist/adapters/codex/driver.d.ts +20 -0
  54. package/dist/adapters/codex/driver.js +89 -0
  55. package/dist/adapters/codex/hook-protocol.d.ts +10 -0
  56. package/dist/adapters/codex/hook-protocol.js +18 -0
  57. package/dist/adapters/codex/layout.d.ts +16 -0
  58. package/dist/adapters/codex/layout.js +18 -0
  59. package/dist/adapters/codex/mock-model.d.ts +52 -0
  60. package/dist/adapters/codex/mock-model.js +210 -0
  61. package/dist/adapters/codex/model-mock.d.ts +11 -0
  62. package/dist/adapters/codex/model-mock.js +10 -0
  63. package/dist/adapters/codex/runtime.d.ts +38 -0
  64. package/dist/adapters/codex/runtime.js +62 -0
  65. package/dist/adapters/opencode/adapter.d.ts +3 -0
  66. package/dist/adapters/opencode/adapter.js +48 -0
  67. package/dist/adapters/opencode/dialect.d.ts +10 -0
  68. package/dist/adapters/opencode/dialect.js +36 -0
  69. package/dist/adapters/opencode/layout.d.ts +9 -0
  70. package/dist/adapters/opencode/layout.js +25 -0
  71. package/dist/adapters/opencode/model-mock.d.ts +10 -0
  72. package/dist/adapters/opencode/model-mock.js +10 -0
  73. package/dist/adapters/opencode/runtime.d.ts +9 -0
  74. package/dist/adapters/opencode/runtime.js +21 -0
  75. package/dist/claude-code.d.ts +16 -0
  76. package/dist/claude-code.js +32 -0
  77. package/dist/cli-flags.d.ts +22 -0
  78. package/dist/cli-flags.js +38 -0
  79. package/dist/cli.js +406 -206
  80. package/dist/codex.d.ts +20 -0
  81. package/dist/codex.js +36 -0
  82. package/dist/community-skills.d.ts +1 -1
  83. package/dist/community-skills.js +2 -2
  84. package/dist/core/adapter.d.ts +88 -0
  85. package/dist/core/adapter.js +3 -0
  86. package/dist/{compile.d.ts → core/compile.d.ts} +11 -2
  87. package/dist/{compile.js → core/compile.js} +68 -53
  88. package/dist/core/compose.d.ts +79 -0
  89. package/dist/core/compose.js +145 -0
  90. package/dist/core/dialect.d.ts +51 -0
  91. package/dist/core/dialect.js +3 -0
  92. package/dist/core/harness-driver.d.ts +134 -0
  93. package/dist/core/harness-driver.js +3 -0
  94. package/dist/core/hook-protocol.d.ts +28 -0
  95. package/dist/core/hook-protocol.js +3 -0
  96. package/dist/core/layout.d.ts +43 -0
  97. package/dist/core/layout.js +3 -0
  98. package/dist/core/model-mock.d.ts +27 -0
  99. package/dist/core/model-mock.js +3 -0
  100. package/dist/core/refs.d.ts +62 -0
  101. package/dist/{refs.js → core/refs.js} +49 -23
  102. package/dist/core/runtime.d.ts +38 -0
  103. package/dist/core/runtime.js +3 -0
  104. package/dist/{types.d.ts → core/types.d.ts} +29 -0
  105. package/dist/{validate.d.ts → core/validate.d.ts} +1 -1
  106. package/dist/{validate.js → core/validate.js} +41 -4
  107. package/dist/e2e.d.ts +19 -0
  108. package/dist/e2e.js +39 -0
  109. package/dist/harness-assert.d.ts +60 -11
  110. package/dist/harness-assert.js +111 -9
  111. package/dist/integration.d.ts +16 -0
  112. package/dist/integration.js +32 -0
  113. package/dist/leaderboard.d.ts +33 -0
  114. package/dist/leaderboard.js +107 -0
  115. package/dist/linting.d.ts +9 -0
  116. package/dist/linting.js +25 -0
  117. package/dist/plugin-loader.d.ts +6 -3
  118. package/dist/plugin-loader.js +101 -49
  119. package/dist/scan.d.ts +49 -0
  120. package/dist/scan.js +177 -0
  121. package/dist/setup-plan.d.ts +51 -0
  122. package/dist/setup-plan.js +85 -0
  123. package/dist/skill-test.d.ts +1 -1
  124. package/dist/skill-test.js +1 -1
  125. package/dist/test-coverage.d.ts +71 -0
  126. package/dist/test-coverage.js +228 -0
  127. package/dist/testing.d.ts +12 -0
  128. package/dist/testing.js +28 -0
  129. package/dist/unit.d.ts +17 -0
  130. package/dist/unit.js +36 -0
  131. package/hooks/refs-nudge.sh +24 -0
  132. package/package.json +35 -16
  133. package/skills/audit-feedback-loop/SKILL.md +76 -0
  134. package/skills/edit-spec/SKILL.md +131 -0
  135. package/skills/enforce-rules-format/SKILL.md +71 -0
  136. package/skills/generate-logo/SKILL.md +103 -0
  137. package/skills/generate-rule/SKILL.md +64 -0
  138. package/skills/linter-docs/clippy.md +241 -0
  139. package/skills/linter-docs/eslint.md +384 -0
  140. package/skills/linter-docs/pylint.md +288 -0
  141. package/skills/linter-docs/rubocop.md +277 -0
  142. package/skills/linter-docs/ruff.md +187 -0
  143. package/skills/linter-docs/stylelint.md +247 -0
  144. package/skills/migrate-to-spec/SKILL.md +126 -0
  145. package/skills/pr-to-lint-rule/SKILL.md +97 -0
  146. package/skills/strengthen/SKILL.md +168 -0
  147. package/skills/test-harness/SKILL.md +157 -0
  148. package/dist/action.d.ts +0 -7
  149. package/dist/action.js +0 -180
  150. package/dist/refs.d.ts +0 -44
  151. package/dist/run-hook.d.ts +0 -77
  152. package/dist/run-hook.js +0 -80
  153. package/dist/run-scripts.d.ts +0 -20
  154. package/dist/run-scripts.js +0 -70
  155. /package/dist/{agent-result.js → adapters/claude-code/agent-result.js} +0 -0
  156. /package/dist/{agent-runtime.d.ts → adapters/claude-code/agent-runtime.d.ts} +0 -0
  157. /package/dist/{agent-runtime.js → adapters/claude-code/agent-runtime.js} +0 -0
  158. /package/dist/{judge.d.ts → adapters/claude-code/judge.d.ts} +0 -0
  159. /package/dist/{judge.js → adapters/claude-code/judge.js} +0 -0
  160. /package/dist/{mock-entry.d.ts → adapters/claude-code/mock-entry.d.ts} +0 -0
  161. /package/dist/{mock-entry.js → adapters/claude-code/mock-entry.js} +0 -0
  162. /package/dist/{mock-model.js → adapters/claude-code/mock-model.js} +0 -0
  163. /package/dist/{skill-driver.js → adapters/claude-code/skill-driver.js} +0 -0
  164. /package/dist/{skill-runtime.d.ts → adapters/claude-code/skill-runtime.d.ts} +0 -0
  165. /package/dist/{skill-runtime.js → adapters/claude-code/skill-runtime.js} +0 -0
  166. /package/dist/{stats.d.ts → adapters/claude-code/stats.d.ts} +0 -0
  167. /package/dist/{stats.js → adapters/claude-code/stats.js} +0 -0
  168. /package/dist/{compile-generator.d.ts → core/compile-generator.d.ts} +0 -0
  169. /package/dist/{compile-generator.js → core/compile-generator.js} +0 -0
  170. /package/dist/{coverage.d.ts → core/coverage.d.ts} +0 -0
  171. /package/dist/{coverage.js → core/coverage.js} +0 -0
  172. /package/dist/{doc-refs.d.ts → core/doc-refs.d.ts} +0 -0
  173. /package/dist/{doc-refs.js → core/doc-refs.js} +0 -0
  174. /package/dist/{evolve.d.ts → core/evolve.d.ts} +0 -0
  175. /package/dist/{evolve.js → core/evolve.js} +0 -0
  176. /package/dist/{frontmatter.d.ts → core/frontmatter.d.ts} +0 -0
  177. /package/dist/{frontmatter.js → core/frontmatter.js} +0 -0
  178. /package/dist/{generate-schema.d.ts → core/generate-schema.d.ts} +0 -0
  179. /package/dist/{generate-schema.js → core/generate-schema.js} +0 -0
  180. /package/dist/{generate-types.d.ts → core/generate-types.d.ts} +0 -0
  181. /package/dist/{generate-types.js → core/generate-types.js} +0 -0
  182. /package/dist/{hash.d.ts → core/hash.d.ts} +0 -0
  183. /package/dist/{hash.js → core/hash.js} +0 -0
  184. /package/dist/{inline.d.ts → core/inline.d.ts} +0 -0
  185. /package/dist/{inline.js → core/inline.js} +0 -0
  186. /package/dist/{integrity.d.ts → core/integrity.d.ts} +0 -0
  187. /package/dist/{integrity.js → core/integrity.js} +0 -0
  188. /package/dist/{linters.d.ts → core/linters.d.ts} +0 -0
  189. /package/dist/{linters.js → core/linters.js} +0 -0
  190. /package/dist/{mcp.d.ts → core/mcp.d.ts} +0 -0
  191. /package/dist/{mcp.js → core/mcp.js} +0 -0
  192. /package/dist/{orphans.d.ts → core/orphans.d.ts} +0 -0
  193. /package/dist/{orphans.js → core/orphans.js} +0 -0
  194. /package/dist/{proofs.d.ts → core/proofs.d.ts} +0 -0
  195. /package/dist/{proofs.js → core/proofs.js} +0 -0
  196. /package/dist/{session.d.ts → core/session.d.ts} +0 -0
  197. /package/dist/{session.js → core/session.js} +0 -0
  198. /package/dist/{sidecar.d.ts → core/sidecar.d.ts} +0 -0
  199. /package/dist/{sidecar.js → core/sidecar.js} +0 -0
  200. /package/dist/{spec.d.ts → core/spec.d.ts} +0 -0
  201. /package/dist/{spec.js → core/spec.js} +0 -0
  202. /package/dist/{symbols.d.ts → core/symbols.d.ts} +0 -0
  203. /package/dist/{symbols.js → core/symbols.js} +0 -0
  204. /package/dist/{test-utils.d.ts → core/test-utils.d.ts} +0 -0
  205. /package/dist/{test-utils.js → core/test-utils.js} +0 -0
  206. /package/dist/{types.js → core/types.js} +0 -0
  207. /package/{.claude-plugin/hooks → hooks}/post-edit.sh +0 -0
  208. /package/{.claude-plugin/hooks → hooks}/pre-edit.sh +0 -0
  209. /package/{.claude-plugin/hooks → hooks}/session-start.sh +0 -0
package/dist/cli.js CHANGED
@@ -13,27 +13,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  const node_fs_1 = require("node:fs");
14
14
  const node_path_1 = require("node:path");
15
15
  const glob_1 = require("glob");
16
- const generate_types_js_1 = require("./generate-types.js");
17
- const validate_js_1 = require("./validate.js");
18
- const types_js_1 = require("./types.js");
19
- const compile_js_1 = require("./compile.js");
20
- const proofs_js_1 = require("./proofs.js");
21
- const inline_js_1 = require("./inline.js");
22
- const frontmatter_js_1 = require("./frontmatter.js");
23
- const generate_schema_js_1 = require("./generate-schema.js");
24
- const compile_generator_js_1 = require("./compile-generator.js");
16
+ const generate_types_js_1 = require("./core/generate-types.js");
17
+ const validate_js_1 = require("./core/validate.js");
18
+ const cli_flags_js_1 = require("./cli-flags.js");
19
+ const setup_plan_js_1 = require("./setup-plan.js");
20
+ const types_js_1 = require("./core/types.js");
21
+ const test_coverage_js_1 = require("./test-coverage.js");
22
+ const scan_js_1 = require("./scan.js");
23
+ const adapter_registry_js_1 = require("./adapter-registry.js");
24
+ const leaderboard_js_1 = require("./leaderboard.js");
25
+ const compile_js_1 = require("./core/compile.js");
26
+ const dialect_js_1 = require("./adapters/claude-code/dialect.js");
27
+ const proofs_js_1 = require("./core/proofs.js");
28
+ const inline_js_1 = require("./core/inline.js");
29
+ const frontmatter_js_1 = require("./core/frontmatter.js");
30
+ const generate_schema_js_1 = require("./core/generate-schema.js");
31
+ const compile_generator_js_1 = require("./core/compile-generator.js");
25
32
  const action_gate_js_1 = require("./action-gate.js");
26
- const agent_runtime_js_1 = require("./agent-runtime.js");
27
- const refs_js_1 = require("./refs.js");
28
- const mcp_js_1 = require("./mcp.js");
29
- const skill_runtime_js_1 = require("./skill-runtime.js");
30
- const linters_js_1 = require("./linters.js");
31
- const harness_test_js_1 = require("./harness-test.js");
32
- const run_scripts_js_1 = require("./run-scripts.js");
33
- const integrity_js_1 = require("./integrity.js");
34
- const coverage_js_1 = require("./coverage.js");
35
- const orphans_js_1 = require("./orphans.js");
36
- const doc_refs_js_1 = require("./doc-refs.js");
33
+ const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
34
+ const refs_js_1 = require("./core/refs.js");
35
+ const mcp_js_1 = require("./core/mcp.js");
36
+ const skill_runtime_js_1 = require("./adapters/claude-code/skill-runtime.js");
37
+ const linters_js_1 = require("./core/linters.js");
38
+ const harness_test_js_1 = require("./adapters/claude-code/harness-test.js");
39
+ const run_scripts_js_1 = require("./adapters/claude-code/run-scripts.js");
40
+ const integrity_js_1 = require("./core/integrity.js");
41
+ const coverage_js_1 = require("./core/coverage.js");
42
+ const orphans_js_1 = require("./core/orphans.js");
43
+ const doc_refs_js_1 = require("./core/doc-refs.js");
37
44
  // ---------------------------------------------------------------------------
38
45
  // Constants
39
46
  // ---------------------------------------------------------------------------
@@ -133,6 +140,7 @@ function compileClaudeToFile(spec, specPath, config) {
133
140
  const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
134
141
  basePath,
135
142
  specFile: specPath,
143
+ dialect: dialect_js_1.claudeCodeDialect,
136
144
  maxRules: config.maxRules,
137
145
  maxTokens: config.maxTokens,
138
146
  maxSectionLines: config.maxSectionLines,
@@ -168,6 +176,9 @@ function compileSkillToFile(spec, specPath) {
168
176
  const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
169
177
  basePath: process.cwd(),
170
178
  specFile: specPath,
179
+ // Pick the SKILL.md frontmatter profile from the detected harness — a Codex
180
+ // repo gets a minimal (name + description) SKILL.md; CC gets the full set.
181
+ dialect: (0, adapter_registry_js_1.detectAdapter)(process.cwd()).dialect,
171
182
  });
172
183
  (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
173
184
  if (errors.length === 0) {
@@ -184,6 +195,7 @@ function compileAgentToFile(spec, specPath) {
184
195
  const { markdown, errors } = (0, compile_js_1.compileAgent)(spec, {
185
196
  basePath: process.cwd(),
186
197
  specFile: specPath,
198
+ dialect: (0, adapter_registry_js_1.detectAdapter)(process.cwd()).dialect,
187
199
  });
188
200
  (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
189
201
  if (errors.length === 0) {
@@ -541,6 +553,7 @@ function auditExitCode(report) {
541
553
  report.frontmatterErrors > 0 ||
542
554
  report.integrityErrors > 0 ||
543
555
  report.coverageErrors > 0 ||
556
+ report.untestedErrors > 0 ||
544
557
  report.symbolRefErrors > 0 ||
545
558
  report.mcpRefErrors > 0)
546
559
  return 2;
@@ -817,6 +830,10 @@ async function audit(restArgs, flags, config) {
817
830
  console.log(` ${line}`);
818
831
  }
819
832
  }
833
+ // 7b. Untested-surface check — skills/agents/hooks shipping without a test or
834
+ // eval. Warning by default (a nudge, exit 0); set rules.untested-surface to
835
+ // "error" to gate CI. See src/test-coverage.ts and docs/rules/untested-surface.md.
836
+ const untested = checkUntestedSurfaces(config, silent);
820
837
  // 8. Validate vigiles builder calls inside markdown code blocks. Default
821
838
  // is to validate every ref; illustrative blocks opt out via
822
839
  // `<!-- vigiles:ignore -->` (single block) or
@@ -848,6 +865,8 @@ async function audit(restArgs, flags, config) {
848
865
  integrityErrors,
849
866
  coverageErrors,
850
867
  orphanCount: orphanReport.orphans.length,
868
+ untestedSurfaces: untested.untested,
869
+ untestedErrors: untested.errors,
851
870
  docRefErrors: docRefReport.errors.length,
852
871
  symbolRefErrors,
853
872
  mcpRefErrors,
@@ -876,6 +895,8 @@ function printAuditSummary(report) {
876
895
  parts.push(`${String(report.duplicatePairs)} duplicates`);
877
896
  if (report.orphanCount > 0)
878
897
  parts.push(`${String(report.orphanCount)} orphan docs`);
898
+ if (report.untestedSurfaces > 0)
899
+ parts.push(`${String(report.untestedSurfaces)} untested surfaces`);
879
900
  if (report.docRefErrors > 0)
880
901
  parts.push(`${String(report.docRefErrors)} broken doc refs`);
881
902
  if (report.symbolRefErrors > 0)
@@ -1016,34 +1037,133 @@ export default claude({${targetLine}
1016
1037
  // ---------------------------------------------------------------------------
1017
1038
  // Setup wizard
1018
1039
  // ---------------------------------------------------------------------------
1019
- const VIGILES_CI_STEP = ` - name: Verify specs
1020
- run: npx vigiles audit && npx vigiles generate-types --check`;
1021
- function addGhaStep() {
1022
- // Find existing GHA workflow
1023
- const ciPaths = [
1024
- ".github/workflows/ci.yml",
1025
- ".github/workflows/ci.yaml",
1026
- ".github/workflows/main.yml",
1027
- ".github/workflows/main.yaml",
1028
- ".github/workflows/test.yml",
1029
- ".github/workflows/test.yaml",
1030
- ];
1031
- for (const ciPath of ciPaths) {
1032
- const fullPath = (0, node_path_1.resolve)(process.cwd(), ciPath);
1033
- if ((0, node_fs_1.existsSync)(fullPath)) {
1034
- const content = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
1035
- if (content.includes("vigiles")) {
1036
- console.log(`✓ ${ciPath} already has vigiles steps`);
1037
- return true;
1038
- }
1039
- // Append step at end of file (safe for all YAML formats)
1040
- const trimmed = content.trimEnd();
1041
- (0, node_fs_1.writeFileSync)(fullPath, trimmed + "\n\n" + VIGILES_CI_STEP + "\n");
1042
- console.log(`✓ Added vigiles check step to ${ciPath}`);
1043
- return true;
1044
- }
1040
+ /** Full GitHub Actions workflow that wires the production `zernie/vigiles@v1`
1041
+ * Action (Pillar 1) and, when Pillar 2 is set up, a deterministic harness job. */
1042
+ function vigilesWorkflow(plan) {
1043
+ const harness = plan.test
1044
+ ? `
1045
+ harness:
1046
+ # Pillar 2 — run your *.harness.{mjs,ts} tests against the real agent CLI and
1047
+ # a scripted mock model (deterministic, no API key). Drop this job if you only
1048
+ # author runHook unit tests, or keep it for the deterministic tier.
1049
+ runs-on: ubuntu-latest
1050
+ steps:
1051
+ - uses: actions/checkout@v4
1052
+ - uses: actions/setup-node@v4
1053
+ with:
1054
+ node-version: "20"
1055
+ - run: npm install
1056
+ - run: npm i -g @anthropic-ai/claude-code # mock tier needs the binary, no API key
1057
+ - run: npx vigiles test
1058
+ `
1059
+ : "";
1060
+ return `name: vigiles
1061
+ on:
1062
+ pull_request:
1063
+ push:
1064
+ branches: [main]
1065
+
1066
+ permissions:
1067
+ contents: read
1068
+ pull-requests: write # for the sticky PR comment
1069
+
1070
+ jobs:
1071
+ verify:
1072
+ # Pillar 1 — verify the references in your instruction files (composite Action
1073
+ # over the published CLI). Posts a sticky PR comment + a \`valid\` output.
1074
+ runs-on: ubuntu-latest
1075
+ steps:
1076
+ - uses: actions/checkout@v4
1077
+ - uses: actions/setup-node@v4
1078
+ with:
1079
+ node-version: "20"
1080
+ - uses: zernie/vigiles@v1
1081
+ ${harness}`;
1082
+ }
1083
+ /** Create `.github/workflows/vigiles.yml` (or note it already exists). */
1084
+ function wireGha(plan) {
1085
+ const dir = (0, node_path_1.resolve)(process.cwd(), ".github", "workflows");
1086
+ const path = (0, node_path_1.resolve)(dir, "vigiles.yml");
1087
+ if ((0, node_fs_1.existsSync)(path)) {
1088
+ console.log("✓ .github/workflows/vigiles.yml already exists");
1089
+ return;
1090
+ }
1091
+ if (!(0, node_fs_1.existsSync)(dir))
1092
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
1093
+ (0, node_fs_1.writeFileSync)(path, vigilesWorkflow(plan));
1094
+ console.log("✓ Created .github/workflows/vigiles.yml (uses zernie/vigiles@v1)");
1095
+ }
1096
+ const STARTER_HARNESS = `/**
1097
+ * Starter harness test (Pillar 2) — scaffolded by \`vigiles init\`.
1098
+ * Proves a hook actually FIRES, deterministically and with no API key.
1099
+ *
1100
+ * npm i -D vigiles # the testing API this imports
1101
+ * npx vigiles test # or: node vigiles.harness.mjs
1102
+ *
1103
+ * Guide: https://github.com/zernie/vigiles/blob/main/docs/harness-testing.md
1104
+ */
1105
+ import { runHook } from "vigiles/testing";
1106
+ import assert from "node:assert/strict";
1107
+
1108
+ // EXAMPLE — replace with one of YOUR hooks. This PreToolUse Bash guard blocks a
1109
+ // destructive command; runHook pipes it a fake event and checks the decision.
1110
+ const guard =
1111
+ \`CMD=$(cat | jq -r '.tool_input.command // empty'); \` +
1112
+ \`case "$CMD" in *"rm -rf /"*) echo blocked >&2; exit 2 ;; esac; exit 0\`;
1113
+
1114
+ const blocked = runHook(guard, {
1115
+ hook_event_name: "PreToolUse",
1116
+ tool_name: "Bash",
1117
+ tool_input: { command: "rm -rf / --no-preserve-root" },
1118
+ });
1119
+ assert.ok(blocked.blocked, "guard should block \\\`rm -rf /\\\`");
1120
+
1121
+ const allowed = runHook(guard, {
1122
+ hook_event_name: "PreToolUse",
1123
+ tool_name: "Bash",
1124
+ tool_input: { command: "ls -la" },
1125
+ });
1126
+ assert.ok(!allowed.blocked, "guard should allow a safe command");
1127
+
1128
+ console.log("\\u2713 hook blocks rm -rf / and allows safe commands");
1129
+ `;
1130
+ /** Pillar 2 — scaffold a starter harness test the user adapts to their hooks. */
1131
+ function scaffoldPillar2() {
1132
+ const path = (0, node_path_1.resolve)(process.cwd(), "vigiles.harness.mjs");
1133
+ if ((0, node_fs_1.existsSync)(path)) {
1134
+ console.log("✓ vigiles.harness.mjs already exists");
1135
+ return;
1136
+ }
1137
+ (0, node_fs_1.writeFileSync)(path, STARTER_HARNESS);
1138
+ console.log("✓ Scaffolded vigiles.harness.mjs — Pillar 2 starter (npm i -D vigiles && npx vigiles test)");
1139
+ }
1140
+ /** Interactive prompts (TTY only): which pillars, CI, plugin. */
1141
+ async function promptSetup() {
1142
+ const readline = await import("node:readline");
1143
+ const rl = readline.createInterface({
1144
+ input: process.stdin,
1145
+ output: process.stdout,
1146
+ });
1147
+ const ask = (q, def) => new Promise((res) => {
1148
+ rl.question(q, (a) => {
1149
+ res(a.trim() || def);
1150
+ });
1151
+ });
1152
+ const isYes = (s) => /^y(es)?$/i.test(s);
1153
+ try {
1154
+ const pillars = (await ask("Set up which pillars? [both/verify/test] (both): ", "both")).toLowerCase();
1155
+ const gha = isYes(await ask("Wire CI (GitHub Action)? [Y/n]: ", "y"));
1156
+ const plugin = isYes(await ask("Install the Claude Code plugin (hooks + skills)? [Y/n]: ", "y"));
1157
+ return {
1158
+ verify: pillars !== "test",
1159
+ test: pillars !== "verify",
1160
+ gha,
1161
+ plugin,
1162
+ };
1163
+ }
1164
+ finally {
1165
+ rl.close();
1045
1166
  }
1046
- return false;
1047
1167
  }
1048
1168
  const KNOWN_INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1049
1169
  const KNOWN_OTHER_CONFIGS = {
@@ -1120,42 +1240,17 @@ function detectProject() {
1120
1240
  hasClaude: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, ".claude")),
1121
1241
  };
1122
1242
  }
1123
- async function setup(args) {
1124
- const targetFlag = args.find((a) => a.startsWith("--target="));
1125
- const strict = args.includes("--strict");
1126
- const noGha = args.includes("--no-gha");
1127
- console.log(`vigiles setup${strict ? " (strict mode)" : ""}\n`);
1128
- // Step 1: Detect project
1129
- const detected = detectProject();
1130
- if (detected.agents.length > 0) {
1131
- console.log(`Detected: ${detected.agents.join(", ")}`);
1132
- }
1133
- if (detected.otherConfigs.length > 0) {
1134
- console.log(`Other agent configs: ${detected.otherConfigs.join(", ")}`);
1135
- }
1136
- if (detected.syncTools.length > 0) {
1137
- console.log(`Sync tools: ${detected.syncTools.join(", ")}`);
1138
- }
1139
- for (const f of detected.instructionFiles) {
1140
- if (f.isSymlink) {
1141
- console.log(`Note: ${f.path} is a symlink`);
1142
- }
1143
- }
1144
- if (detected.agents.length > 0 ||
1145
- detected.otherConfigs.length > 0 ||
1146
- detected.syncTools.length > 0) {
1147
- console.log("");
1148
- }
1149
- // Step 2: Determine targets
1243
+ /** Pillar 1 — specs + types + schema + compile. Returns the spec targets
1244
+ * created/found (empty when every existing file needs migration instead). */
1245
+ async function setupPillar1(detected, targetValue) {
1246
+ // Determine targets
1150
1247
  let targets;
1151
- if (targetFlag) {
1152
- targets = [targetFlag.split("=")[1]];
1248
+ if (targetValue) {
1249
+ targets = [targetValue];
1153
1250
  }
1154
1251
  else {
1155
- // Auto-detect: create specs for instruction files that need them
1156
1252
  const needsSpec = detected.instructionFiles.filter((f) => !f.hasSpec);
1157
1253
  if (needsSpec.length > 0) {
1158
- // Existing files without specs — suggest migration
1159
1254
  for (const f of needsSpec) {
1160
1255
  console.log(`Found ${f.path} without a spec. Migrate with the migrate-to-spec skill`);
1161
1256
  console.log(` or create a blank spec: npx vigiles init --target=${f.path}\n`);
@@ -1163,21 +1258,18 @@ async function setup(args) {
1163
1258
  const hasAnySpec = detected.instructionFiles.some((f) => f.hasSpec);
1164
1259
  if (!hasAnySpec &&
1165
1260
  needsSpec.length === detected.instructionFiles.length) {
1166
- // ALL existing files need migration — don't create new ones
1167
- console.log("Install the plugin to use the migration skill:");
1168
- console.log(" npx skills add zernie/vigiles");
1169
- return;
1261
+ // ALL existing files need migration — don't create new specs.
1262
+ console.log("Use the migration skill: npx skills add zernie/vigiles\n");
1263
+ return [];
1170
1264
  }
1171
1265
  }
1172
- // Default: CLAUDE.md, plus AGENTS.md if Codex detected
1173
1266
  targets = ["CLAUDE.md"];
1174
1267
  const hasAgentsMd = detected.instructionFiles.some((f) => f.path === "AGENTS.md");
1175
1268
  const hasCodex = detected.agents.includes("Codex / GitHub Copilot") || hasAgentsMd;
1176
- if (hasCodex && !hasAgentsMd) {
1269
+ if (hasCodex && !hasAgentsMd)
1177
1270
  targets.push("AGENTS.md");
1178
- }
1179
1271
  }
1180
- // Step 3: Create specs
1272
+ // Create specs
1181
1273
  for (const target of targets) {
1182
1274
  const specPath = `${target}.spec.ts`;
1183
1275
  if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), specPath))) {
@@ -1190,105 +1282,129 @@ async function setup(args) {
1190
1282
  init(["--target=" + target]);
1191
1283
  }
1192
1284
  }
1193
- // Step 4: Generate types
1285
+ // Generate types + schema
1194
1286
  console.log("\nScanning linters and project files...");
1195
1287
  const typesResult = (0, generate_types_js_1.generateTypes)({ basePath: process.cwd() });
1196
- const outPath = ".vigiles/generated.d.ts";
1197
1288
  const outDir = (0, node_path_1.resolve)(process.cwd(), ".vigiles");
1198
- if (!(0, node_fs_1.existsSync)(outDir)) {
1289
+ if (!(0, node_fs_1.existsSync)(outDir))
1199
1290
  (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
1200
- }
1201
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outPath), typesResult.dts);
1291
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), ".vigiles/generated.d.ts"), typesResult.dts);
1202
1292
  for (const l of typesResult.linters) {
1203
1293
  console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
1204
1294
  }
1205
1295
  if (typesResult.scripts.length > 0) {
1206
1296
  console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
1207
1297
  }
1208
- console.log(`✓ Generated ${outPath}`);
1209
- // Also emit a JSON Schema so `vigiles:` markdown frontmatter (Level 1)
1210
- // gets rule-name autocomplete + typo squiggles from the editor's YAML LSP.
1298
+ console.log("✓ Generated .vigiles/generated.d.ts");
1211
1299
  const schemaResult = (0, generate_schema_js_1.generateSchema)({
1212
1300
  basePath: process.cwd(),
1213
1301
  linters: (0, validate_js_1.loadConfig)().linters,
1214
1302
  });
1215
- const schemaPath = ".vigiles/schema.json";
1216
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), schemaPath), schemaResult.json);
1217
- console.log(`✓ Generated ${schemaPath} (point frontmatter at it with \`# yaml-language-server: $schema=./${schemaPath}\`)`);
1218
- // Step 5: Compile specs
1303
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), ".vigiles/schema.json"), schemaResult.json);
1304
+ console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
1305
+ // Compile specs
1219
1306
  console.log("\nCompiling specs...");
1220
1307
  const specs = findSpecs();
1221
- if (specs.length > 0) {
1308
+ if (specs.length > 0)
1222
1309
  await compile(specs, (0, validate_js_1.loadConfig)());
1310
+ return targets;
1311
+ }
1312
+ /** Install the Claude Code plugin (hooks + skills). Returns whether the
1313
+ * fallback wrote `.claude/settings.json` (for the commit hint). */
1314
+ function installPlugin() {
1315
+ try {
1316
+ const { execSync: exec } = require("node:child_process");
1317
+ exec("npx skills add zernie/vigiles", {
1318
+ cwd: process.cwd(),
1319
+ stdio: ["pipe", "pipe", "pipe"],
1320
+ timeout: 30000,
1321
+ });
1322
+ console.log("✓ Installed vigiles plugin (hooks + skills) via skills CLI");
1323
+ return false;
1223
1324
  }
1224
- // Step 6: Add CI step
1225
- console.log("");
1226
- const addedGha = noGha ? false : addGhaStep();
1227
- if (!addedGha) {
1228
- console.log(" No CI workflow found. Add this step to your CI:\n");
1229
- console.log(" npx vigiles audit && npx vigiles generate-types --check");
1230
- }
1231
- // Step 7: Install Claude Code plugin (hooks + skills)
1232
- const shouldInstallPlugin = detected.hasClaude || targets.includes("CLAUDE.md");
1233
- if (shouldInstallPlugin) {
1234
- let pluginInstalled = false;
1235
- // Try installing the full plugin via skills CLI (gives hooks + skills)
1325
+ catch {
1326
+ // skills CLI not available — fall back to direct hook installation.
1327
+ }
1328
+ const settingsDir = (0, node_path_1.resolve)(process.cwd(), ".claude");
1329
+ const settingsPath = (0, node_path_1.resolve)(settingsDir, "settings.json");
1330
+ if (!(0, node_fs_1.existsSync)(settingsDir))
1331
+ (0, node_fs_1.mkdirSync)(settingsDir, { recursive: true });
1332
+ let settings = {};
1333
+ if ((0, node_fs_1.existsSync)(settingsPath)) {
1236
1334
  try {
1237
- const { execSync: exec } = require("node:child_process");
1238
- exec("npx skills add zernie/vigiles", {
1239
- cwd: process.cwd(),
1240
- stdio: ["pipe", "pipe", "pipe"],
1241
- timeout: 30000,
1242
- });
1243
- pluginInstalled = true;
1244
- console.log("✓ Installed vigiles plugin (hooks + skills) via skills CLI");
1335
+ settings = JSON.parse((0, node_fs_1.readFileSync)(settingsPath, "utf-8"));
1245
1336
  }
1246
1337
  catch {
1247
- // skills CLI not available — fall back to direct hook installation
1248
- }
1249
- if (!pluginInstalled) {
1250
- // Fall back: write hooks directly to .claude/settings.json
1251
- // (gives auto-compile + block edits, but no skills)
1252
- const settingsDir = (0, node_path_1.resolve)(process.cwd(), ".claude");
1253
- const settingsPath = (0, node_path_1.resolve)(settingsDir, "settings.json");
1254
- if (!(0, node_fs_1.existsSync)(settingsDir)) {
1255
- (0, node_fs_1.mkdirSync)(settingsDir, { recursive: true });
1256
- }
1257
- let settings = {};
1258
- if ((0, node_fs_1.existsSync)(settingsPath)) {
1259
- try {
1260
- settings = JSON.parse((0, node_fs_1.readFileSync)(settingsPath, "utf-8"));
1261
- }
1262
- catch {
1263
- // Ignore malformed settings
1264
- }
1265
- }
1266
- if (!settings["hooks"]) {
1267
- settings["hooks"] = {};
1268
- }
1269
- const hooks = settings["hooks"];
1270
- const preCmd = `FILE=$(cat | jq -r '.tool_input.file_path // empty') && case "$FILE" in *.md) [ -f "$FILE" ] && head -1 "$FILE" | grep -q 'vigiles:sha256:' && { SPEC=$(head -1 "$FILE" | sed -n 's/.*compiled from \\(.*\\) -->/\\1/p'); echo "BLOCKED: Edit $SPEC instead." >&2; exit 2; } ;; esac; exit 0`;
1271
- const postCmd = `FILE=$(cat | jq -r '.tool_input.file_path // empty') && case "$(basename "$FILE")" in eslint.config.*|.eslintrc*|package.json|pyproject.toml|Cargo.toml) npx vigiles generate-types 2>&1 || true ;; esac && case "$FILE" in *.spec.ts) npx vigiles compile 2>&1 || true ;; esac`;
1272
- // Append to existing arrays (don't duplicate if already present)
1273
- const existingStr = JSON.stringify(settings);
1274
- if (!existingStr.includes("vigiles:sha256")) {
1275
- const pre = (hooks["PreToolUse"] ?? []);
1276
- pre.push({ matcher: "Edit|Write", command: preCmd });
1277
- hooks["PreToolUse"] = pre;
1278
- }
1279
- if (!existingStr.includes("vigiles compile")) {
1280
- const post = (hooks["PostToolUse"] ?? []);
1281
- post.push({ matcher: "Edit|Write", command: postCmd });
1282
- hooks["PostToolUse"] = post;
1283
- }
1284
- (0, node_fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1285
- console.log("✓ Installed hooks in .claude/settings.json");
1286
- console.log(" (For skills like edit-spec and migrate-to-spec, also run: npx skills add zernie/vigiles)");
1287
- }
1338
+ // Ignore malformed settings.
1339
+ }
1340
+ }
1341
+ if (!settings["hooks"])
1342
+ settings["hooks"] = {};
1343
+ const hooks = settings["hooks"];
1344
+ const preCmd = `FILE=$(cat | jq -r '.tool_input.file_path // empty') && case "$FILE" in *.md) [ -f "$FILE" ] && head -1 "$FILE" | grep -q 'vigiles:sha256:' && { SPEC=$(head -1 "$FILE" | sed -n 's/.*compiled from \\(.*\\) -->/\\1/p'); echo "BLOCKED: Edit $SPEC instead." >&2; exit 2; } ;; esac; exit 0`;
1345
+ const postCmd = `FILE=$(cat | jq -r '.tool_input.file_path // empty') && case "$(basename "$FILE")" in eslint.config.*|.eslintrc*|package.json|pyproject.toml|Cargo.toml) npx vigiles generate-types 2>&1 || true ;; esac && case "$FILE" in *.spec.ts) npx vigiles compile 2>&1 || true ;; esac`;
1346
+ const existingStr = JSON.stringify(settings);
1347
+ if (!existingStr.includes("vigiles:sha256")) {
1348
+ const pre = (hooks["PreToolUse"] ?? []);
1349
+ pre.push({ matcher: "Edit|Write", command: preCmd });
1350
+ hooks["PreToolUse"] = pre;
1351
+ }
1352
+ if (!existingStr.includes("vigiles compile")) {
1353
+ const post = (hooks["PostToolUse"] ?? []);
1354
+ post.push({ matcher: "Edit|Write", command: postCmd });
1355
+ hooks["PostToolUse"] = post;
1356
+ }
1357
+ (0, node_fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1358
+ console.log("✓ Installed hooks in .claude/settings.json");
1359
+ console.log(" (For skills like edit-spec and migrate-to-spec, also run: npx skills add zernie/vigiles)");
1360
+ return true;
1361
+ }
1362
+ async function setup(args) {
1363
+ const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
1364
+ const strict = parsed.strict;
1365
+ // Plan: defaults → flags → interactive prompts (only a human at a TTY).
1366
+ let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
1367
+ if ((0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false)) {
1368
+ plan = (0, setup_plan_js_1.resolvePlan)(parsed, await promptSetup());
1369
+ }
1370
+ const pillars = [plan.verify && "verify", plan.test && "test"]
1371
+ .filter(Boolean)
1372
+ .join(" + ");
1373
+ console.log(`vigiles setup${strict ? " (strict)" : ""} — pillars: ${pillars}\n`);
1374
+ // Detect project.
1375
+ const detected = detectProject();
1376
+ if (detected.agents.length > 0) {
1377
+ console.log(`Detected: ${detected.agents.join(", ")}`);
1288
1378
  }
1289
- // Step 8: Agent-specific guidance
1290
- const specPathsList = targets.map((t) => `${t}.spec.ts`);
1291
- const specPaths = specPathsList.join(", ");
1379
+ if (detected.otherConfigs.length > 0) {
1380
+ console.log(`Other agent configs: ${detected.otherConfigs.join(", ")}`);
1381
+ }
1382
+ if (detected.syncTools.length > 0) {
1383
+ console.log(`Sync tools: ${detected.syncTools.join(", ")}`);
1384
+ }
1385
+ for (const f of detected.instructionFiles) {
1386
+ if (f.isSymlink)
1387
+ console.log(`Note: ${f.path} is a symlink`);
1388
+ }
1389
+ // Pillar 1 — verify instruction files.
1390
+ let targets = [];
1391
+ if (plan.verify) {
1392
+ console.log("");
1393
+ targets = await setupPillar1(detected, parsed.target);
1394
+ }
1395
+ // Pillar 2 — test the harness.
1396
+ if (plan.test) {
1397
+ console.log("");
1398
+ scaffoldPillar2();
1399
+ }
1400
+ // CI — the production Action (+ a harness job when Pillar 2 is on).
1401
+ if (plan.gha) {
1402
+ console.log("");
1403
+ wireGha(plan);
1404
+ }
1405
+ // Claude Code plugin (hooks + skills).
1406
+ const wroteSettings = plan.plugin ? installPlugin() : false;
1407
+ // Agent-specific guidance.
1292
1408
  if (targets.includes("AGENTS.md")) {
1293
1409
  console.log("\n Codex / Copilot reads AGENTS.md directly — no hooks needed.");
1294
1410
  console.log(" Run `npx vigiles compile` after spec edits. CI enforces freshness.");
@@ -1297,37 +1413,42 @@ async function setup(args) {
1297
1413
  console.log("\n Non-markdown agent configs detected. Use a sync tool to convert:");
1298
1414
  console.log(" npm install -D rule-porter");
1299
1415
  }
1300
- // Step 8b: Write config if strict mode
1416
+ // Strict config.
1301
1417
  if (strict) {
1302
1418
  const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
1303
1419
  if (!(0, node_fs_1.existsSync)(configPath)) {
1304
- (0, node_fs_1.writeFileSync)(configPath, JSON.stringify({
1305
- rules: {
1306
- "require-spec": "error",
1307
- "require-skill-spec": "error",
1308
- },
1309
- }, null, 2) + "\n");
1420
+ (0, node_fs_1.writeFileSync)(configPath, JSON.stringify({ rules: { "require-spec": "error", "require-skill-spec": "error" } }, null, 2) + "\n");
1310
1421
  console.log("✓ Created .vigilesrc.json with strict rules");
1311
1422
  }
1312
1423
  }
1313
- // Step 9: Summary
1314
- console.log("\n---");
1315
- console.log("Setup complete.\n");
1316
- console.log(` 1. Edit ${specPaths} — add your project's conventions`);
1317
- console.log(" 2. Run `/strengthen` in Claude Code to upgrade guidance → enforce");
1424
+ // Summary.
1425
+ const specPathsList = targets.map((t) => `${t}.spec.ts`);
1426
+ console.log("\n---\nSetup complete.\n");
1427
+ const nextSteps = [];
1428
+ if (specPathsList.length > 0) {
1429
+ nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
1430
+ }
1431
+ if (plan.test) {
1432
+ nextSteps.push("Edit vigiles.harness.mjs to test a real hook, then `npx vigiles test`");
1433
+ }
1318
1434
  if (!strict) {
1319
- console.log(" 3. When ready, enforce specs in CI: npx vigiles setup --strict");
1435
+ nextSteps.push("When ready, enforce in CI: npx vigiles init --strict");
1320
1436
  }
1321
- console.log("\n Commit:");
1437
+ nextSteps.forEach((s, i) => {
1438
+ console.log(` ${String(i + 1)}. ${s}`);
1439
+ });
1322
1440
  const files = [
1323
1441
  ...targets,
1324
1442
  ...specPathsList,
1325
- ".vigiles/generated.d.ts",
1326
- ".vigiles/schema.json",
1327
- ...(shouldInstallPlugin ? [".claude/settings.json"] : []),
1443
+ ...(plan.verify ? [".vigiles/generated.d.ts", ".vigiles/schema.json"] : []),
1444
+ ...(plan.test ? ["vigiles.harness.mjs"] : []),
1445
+ ...(plan.gha ? [".github/workflows/vigiles.yml"] : []),
1446
+ ...(wroteSettings ? [".claude/settings.json"] : []),
1328
1447
  ...(strict ? [".vigilesrc.json"] : []),
1329
1448
  ];
1330
- console.log(` git add ${files.join(" ")} && git commit -m "Add vigiles spec"`);
1449
+ if (files.length > 0) {
1450
+ console.log(`\n Commit:\n git add ${files.join(" ")} && git commit -m "Add vigiles"`);
1451
+ }
1331
1452
  }
1332
1453
  // ---------------------------------------------------------------------------
1333
1454
  // Strengthen: guidance() → enforce() suggestions
@@ -1358,6 +1479,32 @@ function checkIntegrityForFiles(files, severity, silent) {
1358
1479
  }
1359
1480
  return severity === "error" ? errorCount : 0;
1360
1481
  }
1482
+ /**
1483
+ * Apply the `untested-surface` rule: find skills/agents/hooks with no test or
1484
+ * eval (see src/test-coverage.ts). Returns the raw untested count plus the
1485
+ * severity-gated error count — "warn" prints but never fails CI (errors=0),
1486
+ * "error" fails (exit 2), mirroring the integrity check.
1487
+ */
1488
+ function checkUntestedSurfaces(config, silent) {
1489
+ const severity = (0, types_js_1.ruleSeverity)(config?.rules["untested-surface"]);
1490
+ if (!severity)
1491
+ return { untested: 0, errors: 0 };
1492
+ const opts = (0, types_js_1.ruleOptions)(config?.rules["untested-surface"]);
1493
+ const report = (0, test_coverage_js_1.findUntestedSurfaces)({ basePath: process.cwd(), ...opts });
1494
+ if (!silent) {
1495
+ console.log("\nUntested surfaces:\n");
1496
+ for (const line of (0, test_coverage_js_1.formatUntestedReport)(report).split("\n")) {
1497
+ console.log(` ${line}`);
1498
+ }
1499
+ for (const s of report.untested) {
1500
+ ghAnnotate(severity === "error" ? "error" : "warning", `${s.kind} ${s.path} ships without a test or eval`, s.path);
1501
+ }
1502
+ }
1503
+ return {
1504
+ untested: report.untested.length,
1505
+ errors: severity === "error" ? report.untested.length : 0,
1506
+ };
1507
+ }
1361
1508
  /**
1362
1509
  * Apply the configured coverage thresholds. Returns the number of failing
1363
1510
  * thresholds (so the audit can fail CI when severity is "error").
@@ -1534,16 +1681,19 @@ function handleGenerateSchema(args, restArgs) {
1534
1681
  */
1535
1682
  function handleRunScripts(kind, args, restArgs) {
1536
1683
  const cwd = process.cwd();
1537
- const defaultGlob = kind === "test" ? "**/*.harness.mjs" : "**/*.eval.mjs";
1538
- if (kind === "test" && !(0, harness_test_js_1.claudeAvailable)()) {
1539
- console.log("vigiles test: `claude` CLI not found — skipping harness tests.");
1540
- return;
1541
- }
1684
+ // Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
1685
+ const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
1542
1686
  const files = (0, run_scripts_js_1.discoverScripts)(restArgs, defaultGlob, cwd);
1543
1687
  if (files.length === 0) {
1544
1688
  console.log(`No ${defaultGlob} files found.`);
1545
1689
  return;
1546
1690
  }
1691
+ // No blanket skip: unit-tier (runHook) tests need no `claude`, so always run.
1692
+ // A script whose tier DOES need `claude` self-reports `⊘ SKIPPED` (exit 77) —
1693
+ // loud, never a silent green. Just flag up front that some may skip.
1694
+ if (kind === "test" && !(0, harness_test_js_1.claudeAvailable)()) {
1695
+ console.log("ℹ `claude` CLI not found — unit-tier tests run; tests that need it report SKIPPED.\n");
1696
+ }
1547
1697
  const trialsFlag = args.find((a) => a.startsWith("--trials="));
1548
1698
  const env = {};
1549
1699
  if (trialsFlag)
@@ -1551,8 +1701,16 @@ function handleRunScripts(kind, args, restArgs) {
1551
1701
  console.log(`Running ${String(files.length)} ${kind} file(s):\n`);
1552
1702
  const results = (0, run_scripts_js_1.runScripts)(files, cwd, env);
1553
1703
  console.log("\n" + (0, run_scripts_js_1.formatScriptSummary)(results));
1554
- if (results.some((r) => r.code !== 0))
1704
+ if ((0, run_scripts_js_1.anyFailed)(results))
1705
+ process.exit(1);
1706
+ // `--no-skip`: in a context that ASSERTS the capability is present (a CI job),
1707
+ // a skipped tier is untested surface — fail loudly instead of passing green.
1708
+ if (args.includes("--no-skip") && results.some((r) => r.status === "skip")) {
1709
+ const n = results.filter((r) => r.status === "skip").length;
1710
+ console.log(`\n✗ --no-skip: ${String(n)} tier(s) SKIPPED — untested surface here. ` +
1711
+ "Install the missing capability (e.g. the `claude` CLI) or scope the run.");
1555
1712
  process.exit(1);
1713
+ }
1556
1714
  }
1557
1715
  function printUsage(command) {
1558
1716
  console.log("vigiles — compile typed specs to instruction files");
@@ -1785,17 +1943,10 @@ function isInstructionFile(file) {
1785
1943
  * was found. `basePath` is the file's own directory (where paths resolve).
1786
1944
  */
1787
1945
  function reportRefIssues(markdown, basePath, log) {
1788
- const broken = (0, refs_js_1.verifySymbolRefs)(markdown, basePath);
1789
- const unmarked = (0, refs_js_1.unmarkedCodeRefs)(markdown);
1790
- for (const b of broken) {
1791
- log(` ✗ line ${String(b.line)}: ${b.reason}`);
1792
- }
1793
- for (const u of unmarked) {
1794
- const callee = u.text.replace(/\s*\([^)]*\)\s*$/, "");
1795
- log(` ✗ line ${String(u.line)}: \`${u.text}\` is an unmarked code reference — ` +
1796
- `mark it as \`vigiles:symbol path/to/file.ext#${callee}\` or add <!-- vigiles:ignore --> if it is prose`);
1797
- }
1798
- return broken.length > 0 || unmarked.length > 0;
1946
+ const issues = (0, refs_js_1.collectRefIssues)(markdown, basePath);
1947
+ for (const m of issues)
1948
+ log(` ✗ ${m}`);
1949
+ return issues.length > 0;
1799
1950
  }
1800
1951
  /** `vigiles refs <file>` — check a file's symbol references (exit 2 on issues). */
1801
1952
  function refsCommand(target) {
@@ -1845,6 +1996,9 @@ function refsHookCommand() {
1845
1996
  }
1846
1997
  if (!file || !isInstructionFile(file))
1847
1998
  return;
1999
+ const severity = (0, types_js_1.ruleSeverity)((0, validate_js_1.loadConfig)().rules["unmarked-refs"]);
2000
+ if (severity === false)
2001
+ return;
1848
2002
  const cwd = process.cwd();
1849
2003
  const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
1850
2004
  let markdown;
@@ -1854,22 +2008,39 @@ function refsHookCommand() {
1854
2008
  catch {
1855
2009
  return;
1856
2010
  }
1857
- const lines = [];
1858
- const bad = reportRefIssues(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)), (m) => {
1859
- lines.push(m);
1860
- });
1861
- if (bad) {
2011
+ const issues = (0, refs_js_1.collectRefIssues)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)));
2012
+ const action = (0, refs_js_1.refsHookAction)(issues.length, severity);
2013
+ if (action === "ok")
2014
+ return;
2015
+ if (action === "block") {
2016
+ // Opt-in (`unmarked-refs: "error"`): exit 2 feeds stderr to the model.
1862
2017
  console.error(`vigiles: fix the code references in ${target}:`);
1863
- for (const l of lines)
1864
- console.error(l);
2018
+ for (const m of issues)
2019
+ console.error(` ✗ ${m}`);
1865
2020
  process.exit(2);
1866
2021
  }
2022
+ // Default ("warn"): a non-blocking nudge injected into the agent's context.
2023
+ const context = `vigiles: ${target} has reference(s) that won't be verified unless they ` +
2024
+ `are vigiles marks:\n` +
2025
+ issues.map((m) => ` - ${m}`).join("\n") +
2026
+ `\nExpress references as marks (\`enforce()\` / \`file()\` / \`cmd()\` / a ` +
2027
+ `\`vigiles:symbol\` span / an inline \`<!-- vigiles:enforce -->\` comment) so ` +
2028
+ `\`vigiles audit\` can check them — or add \`<!-- vigiles:ignore -->\` if it ` +
2029
+ `is prose, not a reference.`;
2030
+ process.stdout.write(JSON.stringify({
2031
+ hookSpecificOutput: {
2032
+ hookEventName: "PostToolUse",
2033
+ additionalContext: context,
2034
+ },
2035
+ }) + "\n");
1867
2036
  }
1868
2037
  async function main() {
1869
2038
  const args = process.argv.slice(2);
1870
2039
  const command = args[0];
1871
2040
  const restArgs = args.slice(1).filter((a) => !a.startsWith("--"));
1872
- const config = (0, validate_js_1.loadConfig)();
2041
+ // Shared flags (--max-rules, --catalog-only) override the loaded config so
2042
+ // every GitHub Action input maps to a real CLI flag. See src/cli-flags.ts.
2043
+ const config = (0, cli_flags_js_1.applyConfigFlags)((0, validate_js_1.loadConfig)(), args);
1873
2044
  switch (command) {
1874
2045
  // --- Primary commands ---
1875
2046
  case "init": {
@@ -1921,6 +2092,35 @@ async function main() {
1921
2092
  case "eval":
1922
2093
  handleRunScripts("eval", args, restArgs);
1923
2094
  break;
2095
+ case "scan": {
2096
+ const dirs = restArgs.length > 0 ? restArgs : ["."];
2097
+ const json = args.includes("--json");
2098
+ if (dirs.length > 1) {
2099
+ // Multiple targets → rank them (the leaderboard engine).
2100
+ const scores = (0, leaderboard_js_1.rankPlugins)(dirs);
2101
+ console.log(json ? JSON.stringify(scores, null, 2) : (0, leaderboard_js_1.formatLeaderboard)(scores));
2102
+ }
2103
+ else {
2104
+ const root = (0, node_path_1.resolve)(dirs[0]);
2105
+ const harnessFlag = args
2106
+ .find((a) => a.startsWith("--harness="))
2107
+ ?.slice("--harness=".length);
2108
+ const det = (0, adapter_registry_js_1.detectAdapterResult)(root);
2109
+ const adapter = harnessFlag
2110
+ ? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
2111
+ : det.adapter;
2112
+ const report = (0, scan_js_1.scanPlugin)(dirs[0], adapter.layout);
2113
+ if (!json) {
2114
+ console.log(`Detected harness: ${adapter.name}`);
2115
+ if (!harnessFlag && det.ambiguousWith.length > 0) {
2116
+ console.log(`⚠ repo also matches: ${det.ambiguousWith.join(", ")} — override with --harness=<name>`);
2117
+ }
2118
+ console.log("");
2119
+ }
2120
+ console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
2121
+ }
2122
+ break;
2123
+ }
1924
2124
  // --- Plumbing ---
1925
2125
  case "generate-types":
1926
2126
  handleGenerateTypes(args, restArgs);