vigiles 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +182 -134
  2. package/dist/action-gate.d.ts +28 -0
  3. package/dist/action-gate.js +73 -0
  4. package/dist/cli.js +705 -230
  5. package/dist/community-skills.d.ts +22 -0
  6. package/dist/community-skills.js +86 -0
  7. package/dist/compile-generator.d.ts +48 -0
  8. package/dist/compile-generator.js +322 -0
  9. package/dist/compile.d.ts +4 -0
  10. package/dist/compile.js +239 -45
  11. package/dist/coverage.d.ts +56 -0
  12. package/dist/coverage.js +178 -0
  13. package/dist/doc-refs.d.ts +60 -0
  14. package/dist/doc-refs.js +246 -0
  15. package/dist/eval.d.ts +62 -0
  16. package/dist/eval.js +174 -0
  17. package/dist/evolve.js +60 -125
  18. package/dist/frontmatter.d.ts +75 -0
  19. package/dist/frontmatter.js +263 -0
  20. package/dist/generate-schema.d.ts +51 -0
  21. package/dist/generate-schema.js +125 -0
  22. package/dist/generate-types.js +39 -1
  23. package/dist/harness-test.d.ts +38 -0
  24. package/dist/harness-test.js +129 -0
  25. package/dist/hash.d.ts +8 -0
  26. package/dist/hash.js +16 -0
  27. package/dist/inline.d.ts +22 -4
  28. package/dist/inline.js +60 -13
  29. package/dist/integrity.d.ts +29 -0
  30. package/dist/integrity.js +44 -0
  31. package/dist/linters.d.ts +5 -3
  32. package/dist/linters.js +144 -4
  33. package/dist/mock-model.d.ts +31 -0
  34. package/dist/mock-model.js +189 -0
  35. package/dist/orphans.d.ts +52 -0
  36. package/dist/orphans.js +124 -0
  37. package/dist/proofs.js +22 -16
  38. package/dist/refs.d.ts +44 -0
  39. package/dist/refs.js +144 -0
  40. package/dist/session.d.ts +97 -0
  41. package/dist/session.js +306 -0
  42. package/dist/sidecar.d.ts +35 -0
  43. package/dist/sidecar.js +102 -0
  44. package/dist/skill-driver.d.ts +77 -0
  45. package/dist/skill-driver.js +76 -0
  46. package/dist/skill-runtime.d.ts +101 -0
  47. package/dist/skill-runtime.js +289 -0
  48. package/dist/skill-test.d.ts +47 -0
  49. package/dist/skill-test.js +77 -0
  50. package/dist/spec.d.ts +119 -13
  51. package/dist/spec.js +51 -4
  52. package/dist/symbols.d.ts +30 -0
  53. package/dist/symbols.js +142 -0
  54. package/dist/test-utils.d.ts +8 -0
  55. package/dist/test-utils.js +41 -0
  56. package/dist/types.d.ts +34 -8
  57. package/dist/types.js +19 -0
  58. package/dist/validate.js +5 -3
  59. package/package.json +26 -5
  60. package/.claude/settings.json +0 -46
  61. package/.claude/settings.local.json +0 -8
  62. package/.github/workflows/ci.yml +0 -81
  63. package/.prettierignore +0 -1
  64. package/.vigiles/generated.d.ts +0 -205
  65. package/CLAUDE.md +0 -95
  66. package/CLAUDE.md.spec.ts +0 -142
  67. package/CONTRIBUTING.md +0 -121
  68. package/dist/action.d.ts.map +0 -1
  69. package/dist/action.js.map +0 -1
  70. package/dist/cli.d.ts.map +0 -1
  71. package/dist/cli.js.map +0 -1
  72. package/dist/cli.test.d.ts +0 -2
  73. package/dist/cli.test.d.ts.map +0 -1
  74. package/dist/cli.test.js +0 -650
  75. package/dist/cli.test.js.map +0 -1
  76. package/dist/compile.d.ts.map +0 -1
  77. package/dist/compile.js.map +0 -1
  78. package/dist/evolve.d.ts.map +0 -1
  79. package/dist/evolve.js.map +0 -1
  80. package/dist/freshness.d.ts +0 -67
  81. package/dist/freshness.d.ts.map +0 -1
  82. package/dist/freshness.js +0 -244
  83. package/dist/freshness.js.map +0 -1
  84. package/dist/freshness.test.d.ts +0 -2
  85. package/dist/freshness.test.d.ts.map +0 -1
  86. package/dist/freshness.test.js +0 -356
  87. package/dist/freshness.test.js.map +0 -1
  88. package/dist/generate-types.d.ts.map +0 -1
  89. package/dist/generate-types.js.map +0 -1
  90. package/dist/inline.d.ts.map +0 -1
  91. package/dist/inline.js.map +0 -1
  92. package/dist/inline.test.d.ts +0 -5
  93. package/dist/inline.test.d.ts.map +0 -1
  94. package/dist/inline.test.js +0 -152
  95. package/dist/inline.test.js.map +0 -1
  96. package/dist/linters.d.ts.map +0 -1
  97. package/dist/linters.js.map +0 -1
  98. package/dist/proofs.d.ts.map +0 -1
  99. package/dist/proofs.js.map +0 -1
  100. package/dist/proofs.test.d.ts +0 -9
  101. package/dist/proofs.test.d.ts.map +0 -1
  102. package/dist/proofs.test.js +0 -952
  103. package/dist/proofs.test.js.map +0 -1
  104. package/dist/spec.d.ts.map +0 -1
  105. package/dist/spec.js.map +0 -1
  106. package/dist/spec.test.d.ts +0 -2
  107. package/dist/spec.test.d.ts.map +0 -1
  108. package/dist/spec.test.js +0 -1222
  109. package/dist/spec.test.js.map +0 -1
  110. package/dist/types.d.ts.map +0 -1
  111. package/dist/types.js.map +0 -1
  112. package/dist/validate.d.ts.map +0 -1
  113. package/dist/validate.js.map +0 -1
  114. package/dist/validate.test.d.ts +0 -2
  115. package/dist/validate.test.d.ts.map +0 -1
  116. package/dist/validate.test.js +0 -531
  117. package/dist/validate.test.js.map +0 -1
  118. package/docs/agent-setup.md +0 -85
  119. package/docs/agent-workflows.md +0 -103
  120. package/docs/comparison.md +0 -71
  121. package/docs/freshness.md +0 -124
  122. package/docs/inline-mode.md +0 -119
  123. package/docs/linter-support.md +0 -166
  124. package/docs/spec-format.md +0 -194
  125. package/eslint.config.mjs +0 -79
  126. package/examples/CLAUDE.md +0 -54
  127. package/examples/CLAUDE.md.spec.ts +0 -65
  128. package/examples/SKILL.md +0 -50
  129. package/examples/SKILL.md.spec.ts +0 -57
  130. package/fixtures/example-project/CLAUDE.md +0 -11
  131. package/fixtures/example-project/package.json +0 -9
  132. package/fixtures/example-project/src/index.ts +0 -3
  133. package/fixtures/example-project/src/utils.test.ts +0 -2
  134. package/fixtures/example-project/src/utils.ts +0 -3
  135. package/logo.png +0 -0
  136. package/research/adoption-strategy.md +0 -111
  137. package/research/agent-integration.md +0 -145
  138. package/research/ai-code-quality.md +0 -197
  139. package/research/code-search-for-agents.md +0 -313
  140. package/research/competitive-landscape.md +0 -163
  141. package/research/doc-freshness.md +0 -516
  142. package/research/executable-specs.md +0 -368
  143. package/research/feature-ideas.md +0 -464
  144. package/research/formal-proofs-for-agents.md +0 -338
  145. package/research/fp-for-agent-harness.md +0 -150
  146. package/research/fp-for-deterministic-ai.md +0 -131
  147. package/research/self-evolving-specs.md +0 -298
  148. package/schemas/claude-md-strict.yml +0 -18
  149. package/schemas/claude-md.yml +0 -6
  150. package/schemas/skill-strict.yml +0 -12
  151. package/schemas/skill.yml +0 -5
  152. package/skills/audit-feedback-loop/SKILL.md +0 -76
  153. package/skills/edit-spec/SKILL.md +0 -131
  154. package/skills/enforce-rules-format/SKILL.md +0 -71
  155. package/skills/generate-logo/SKILL.md +0 -102
  156. package/skills/generate-rule/SKILL.md +0 -90
  157. package/skills/linter-docs/clippy.md +0 -241
  158. package/skills/linter-docs/eslint.md +0 -384
  159. package/skills/linter-docs/pylint.md +0 -288
  160. package/skills/linter-docs/rubocop.md +0 -277
  161. package/skills/linter-docs/ruff.md +0 -187
  162. package/skills/linter-docs/stylelint.md +0 -247
  163. package/skills/migrate-to-spec/SKILL.md +0 -124
  164. package/skills/pr-to-lint-rule/SKILL.md +0 -97
  165. package/skills/strengthen/SKILL.md +0 -168
  166. package/src/action.ts +0 -214
  167. package/src/cli.test.ts +0 -914
  168. package/src/cli.ts +0 -1631
  169. package/src/compile.ts +0 -691
  170. package/src/evolve.ts +0 -781
  171. package/src/freshness.test.ts +0 -449
  172. package/src/freshness.ts +0 -299
  173. package/src/generate-types.ts +0 -448
  174. package/src/inline.test.ts +0 -206
  175. package/src/inline.ts +0 -164
  176. package/src/linters.ts +0 -739
  177. package/src/proofs.test.ts +0 -1314
  178. package/src/proofs.ts +0 -849
  179. package/src/spec.test.ts +0 -1471
  180. package/src/spec.ts +0 -427
  181. package/src/types.ts +0 -117
  182. package/src/validate.test.ts +0 -701
  183. package/src/validate.ts +0 -381
  184. package/tsconfig.json +0 -23
package/dist/cli.js CHANGED
@@ -15,17 +15,25 @@ const node_path_1 = require("node:path");
15
15
  const glob_1 = require("glob");
16
16
  const generate_types_js_1 = require("./generate-types.js");
17
17
  const validate_js_1 = require("./validate.js");
18
+ const types_js_1 = require("./types.js");
18
19
  const compile_js_1 = require("./compile.js");
19
20
  const proofs_js_1 = require("./proofs.js");
20
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");
25
+ const action_gate_js_1 = require("./action-gate.js");
26
+ const refs_js_1 = require("./refs.js");
27
+ const skill_runtime_js_1 = require("./skill-runtime.js");
21
28
  const linters_js_1 = require("./linters.js");
22
- const freshness_js_1 = require("./freshness.js");
29
+ const integrity_js_1 = require("./integrity.js");
30
+ const coverage_js_1 = require("./coverage.js");
31
+ const orphans_js_1 = require("./orphans.js");
32
+ const doc_refs_js_1 = require("./doc-refs.js");
23
33
  // ---------------------------------------------------------------------------
24
34
  // Constants
25
35
  // ---------------------------------------------------------------------------
26
36
  const IGNORE_NODE_MODULES = ["node_modules/**"];
27
- // Config is loaded from .vigilesrc.json via validate.ts::loadConfig().
28
- // All settings (validation, compilation, freshness) are in one place.
29
37
  // ---------------------------------------------------------------------------
30
38
  // Spec loading
31
39
  // ---------------------------------------------------------------------------
@@ -98,9 +106,84 @@ function printErrors(specFile, errors) {
98
106
  // ---------------------------------------------------------------------------
99
107
  // Commands
100
108
  // ---------------------------------------------------------------------------
109
+ /** Compile a generator-skill spec from source → SKILL.md. Returns validity. */
110
+ function compileGeneratorSkillToFile(specPath, source) {
111
+ const outputPath = specPath.replace(/\.spec\.ts$/, "");
112
+ const { markdown, errors } = (0, compile_generator_js_1.compileGeneratorSkill)(source, {
113
+ basePath: process.cwd(),
114
+ specFile: specPath,
115
+ });
116
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
117
+ if (errors.length === 0) {
118
+ console.log(`\n✓ ${specPath} → ${outputPath} (generator skill)`);
119
+ return true;
120
+ }
121
+ console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
122
+ for (const e of errors)
123
+ console.log(` ${e.type}: ${e.message}`);
124
+ return false;
125
+ }
126
+ /** Compile a ClaudeSpec → its primary + any additional targets. */
127
+ function compileClaudeToFile(spec, specPath, config) {
128
+ const basePath = process.cwd();
129
+ const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
130
+ basePath,
131
+ specFile: specPath,
132
+ maxRules: config.maxRules,
133
+ maxTokens: config.maxTokens,
134
+ maxSectionLines: config.maxSectionLines,
135
+ catalogOnly: config.catalogOnly,
136
+ linters: config.linters,
137
+ });
138
+ const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
139
+ if (errors.length > 0) {
140
+ console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
141
+ printErrors(specPath, errors);
142
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
143
+ return false;
144
+ }
145
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
146
+ const outputNames = [primaryOutput];
147
+ for (const t of targets.slice(1)) {
148
+ const body = markdown
149
+ .replace(/^<!-- vigiles:[^\n]+\n\n?/, "")
150
+ .replace(/^# [^\n]+/, `# ${t}`);
151
+ const dir = primaryOutput.substring(0, primaryOutput.lastIndexOf("/") + 1);
152
+ const targetPath = dir + t;
153
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, targetPath), (0, compile_js_1.addHash)(body, specPath));
154
+ outputNames.push(targetPath);
155
+ }
156
+ const linterCount = linterResults.filter((r) => r.exists).length;
157
+ console.log(`\n✓ ${specPath} → ${outputNames.join(", ")}`);
158
+ console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
159
+ return true;
160
+ }
161
+ /** Compile a declarative SkillSpec → SKILL.md. */
162
+ function compileSkillToFile(spec, specPath) {
163
+ const outputPath = specPath.replace(/\.spec\.ts$/, "");
164
+ const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
165
+ basePath: process.cwd(),
166
+ specFile: specPath,
167
+ });
168
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
169
+ if (errors.length === 0) {
170
+ console.log(`\n✓ ${specPath} → ${outputPath}`);
171
+ return true;
172
+ }
173
+ console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
174
+ printErrors(specPath, errors);
175
+ return false;
176
+ }
101
177
  async function compile(specPaths, config) {
102
178
  let allValid = true;
103
179
  for (const specPath of specPaths) {
180
+ // Generator skills can't be executed to markdown — compile from source.
181
+ const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), specPath), "utf-8");
182
+ if (/\bgenSkill\s*\(/.test(source)) {
183
+ if (!compileGeneratorSkillToFile(specPath, source))
184
+ allValid = false;
185
+ continue;
186
+ }
104
187
  const spec = await loadSpec(specPath);
105
188
  if (!spec) {
106
189
  console.log(`\n✗ ${specPath} — failed to load`);
@@ -108,69 +191,13 @@ async function compile(specPaths, config) {
108
191
  allValid = false;
109
192
  continue;
110
193
  }
111
- const basePath = process.cwd();
112
194
  if (spec._specType === "claude") {
113
- const { markdown: rawMarkdown, errors, linterResults, targets, } = (0, compile_js_1.compileClaude)(spec, {
114
- basePath,
115
- specFile: specPath,
116
- maxRules: config.maxRules,
117
- maxTokens: config.maxTokens,
118
- maxSectionLines: config.maxSectionLines,
119
- catalogOnly: config.catalogOnly,
120
- linters: config.linters,
121
- });
122
- // Embed input hash for freshness tracking
123
- let markdown = rawMarkdown;
124
- if (config.freshnessMode === "input-hash") {
125
- const inputs = (0, freshness_js_1.discoverInputs)(specPath, spec, basePath, config.freshnessInputs);
126
- const inputHash = (0, freshness_js_1.computeInputHash)(inputs.files, basePath);
127
- markdown = (0, freshness_js_1.addInputHash)(rawMarkdown, inputHash);
128
- }
129
- const linterCount = linterResults.filter((r) => r.exists).length;
130
- const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
131
- if (errors.length === 0) {
132
- // Write primary target
133
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
134
- const outputNames = [primaryOutput];
135
- // Write additional targets with swapped heading + recomputed hash
136
- for (const t of targets.slice(1)) {
137
- // Strip hash, replace heading, recompute hash
138
- const body = markdown
139
- .replace(/^<!-- vigiles:[^\n]+\n\n?/, "")
140
- .replace(/^# [^\n]+/, `# ${t}`);
141
- const additional = (0, compile_js_1.addHash)(body, specPath);
142
- const dir = primaryOutput.substring(0, primaryOutput.lastIndexOf("/") + 1);
143
- const targetPath = dir + t;
144
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, targetPath), additional);
145
- outputNames.push(targetPath);
146
- }
147
- console.log(`\n✓ ${specPath} → ${outputNames.join(", ")}`);
148
- console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
149
- }
150
- else {
151
- console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
152
- printErrors(specPath, errors);
195
+ if (!compileClaudeToFile(spec, specPath, config))
153
196
  allValid = false;
154
- // Still write the file so the user can see partial output
155
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
156
- }
157
197
  }
158
198
  else if (spec._specType === "skill") {
159
- const outputPath = specPath.replace(/\.spec\.ts$/, "");
160
- const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
161
- basePath,
162
- specFile: specPath,
163
- });
164
- if (errors.length === 0) {
165
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, outputPath), markdown);
166
- console.log(`\n✓ ${specPath} → ${outputPath}`);
167
- }
168
- else {
169
- console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
170
- printErrors(specPath, errors);
199
+ if (!compileSkillToFile(spec, specPath))
171
200
  allValid = false;
172
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, outputPath), markdown);
173
- }
174
201
  }
175
202
  }
176
203
  return allValid;
@@ -343,17 +370,7 @@ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
343
370
  const ruleCount = Object.keys(rules).length;
344
371
  if (ruleCount < 2)
345
372
  continue;
346
- // Guard against unknown rule kinds (legacy "check" artifacts, JS
347
- // callers) that would crash ruleToText inside findSimilarRules.
348
- // Same pattern as the try/catch in runProofSuite.
349
- let pairs;
350
- try {
351
- pairs = (0, proofs_js_1.findSimilarRules)(rules, threshold);
352
- }
353
- catch (e) {
354
- log(` ⚠ ${specPath}: similarity check failed (${e instanceof Error ? e.message : String(e)})`);
355
- continue;
356
- }
373
+ const pairs = (0, proofs_js_1.findSimilarRules)(rules, threshold);
357
374
  if (pairs.length === 0)
358
375
  continue;
359
376
  if (specsWithDuplicates === 0) {
@@ -376,24 +393,117 @@ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
376
393
  log(`\n ${String(totalPairs)} duplicate pair(s) in ${String(specsWithDuplicates)} spec(s). Consider merging or rewording.`);
377
394
  return { valid: false, pairCount: totalPairs };
378
395
  }
396
+ /**
397
+ * Verify the file-qualified symbol references (`path.ext#symbol`) in instruction
398
+ * files: the named file must exist and define the named symbol. The author
399
+ * names the file, so this is a *declared* reference — a broken one is an error.
400
+ * Each named file is parsed on demand; there is no project-wide index. Returns
401
+ * the count of broken references.
402
+ */
403
+ function verifyMarkdownSymbols(files, silent) {
404
+ if (files.length === 0)
405
+ return 0;
406
+ const cwd = process.cwd();
407
+ let printedHeader = false;
408
+ let errors = 0;
409
+ for (const f of files) {
410
+ let markdown;
411
+ try {
412
+ markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, f), "utf-8");
413
+ }
414
+ catch {
415
+ continue;
416
+ }
417
+ const broken = (0, refs_js_1.verifySymbolRefs)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, f)));
418
+ if (broken.length === 0)
419
+ continue;
420
+ if (!silent) {
421
+ if (!printedHeader) {
422
+ console.log("\nSymbol reference check:\n");
423
+ printedHeader = true;
424
+ }
425
+ for (const b of broken) {
426
+ console.log(` ✗ ${f}:${String(b.line)} ${b.reason}`);
427
+ ghAnnotate("error", b.reason, f, b.line);
428
+ }
429
+ }
430
+ errors += broken.length;
431
+ }
432
+ return errors;
433
+ }
379
434
  /** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
380
435
  function auditExitCode(report) {
381
436
  if (report.hashErrors > 0 ||
382
437
  report.validationErrors > 0 ||
383
438
  report.inlineErrors > 0 ||
384
- report.freshnessErrors > 0)
439
+ report.frontmatterErrors > 0 ||
440
+ report.integrityErrors > 0 ||
441
+ report.coverageErrors > 0 ||
442
+ report.symbolRefErrors > 0)
385
443
  return 2;
386
- if (report.duplicatePairs > 0)
444
+ if (report.duplicatePairs > 0 ||
445
+ report.orphanCount > 0 ||
446
+ report.docRefErrors > 0)
387
447
  return 1;
388
- // Coverage gaps and guidance counts are informational, not failures
448
+ // Guidance counts are informational, not failures
389
449
  return 0;
390
450
  }
391
451
  /**
392
- * Verify inline `<!-- vigiles:enforce ... -->` comments in an instruction
393
- * file. Each comment's linter rule goes through the same verification as
394
- * spec-declared enforce rules (existence, enabled status, closest-match
395
- * suggestions on typo).
452
+ * Verify one parsed enforce rule against the linter catalog/config, logging
453
+ * and annotating on failure. Returns true when the rule is valid+enabled.
454
+ */
455
+ function verifyOneRule(rule, filePath, silent, linterOptions) {
456
+ const log = (msg) => {
457
+ if (!silent)
458
+ console.log(msg);
459
+ };
460
+ const result = (0, linters_js_1.checkLinterRule)(rule.linterRule, process.cwd(), linterOptions);
461
+ if (!result.exists) {
462
+ const message = result.error ?? `Rule "${rule.linterRule}" not found`;
463
+ log(` ✗ line ${String(rule.line)}: ${message}`);
464
+ if (!silent)
465
+ ghAnnotate("error", message, filePath, rule.line);
466
+ return false;
467
+ }
468
+ if (result.enabled === "disabled") {
469
+ const message = `Rule "${rule.linterRule}" exists but is disabled in ${result.linter} config`;
470
+ log(` ✗ line ${String(rule.line)}: ${message}`);
471
+ if (!silent)
472
+ ghAnnotate("error", message, filePath, rule.line);
473
+ return false;
474
+ }
475
+ log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
476
+ return true;
477
+ }
478
+ /**
479
+ * Verify the `vigiles:file` / `vigiles:cmd` references a markdown file declares
480
+ * (inline comments or frontmatter lists), using the same engine spec mode uses:
481
+ * file paths via existsSync, npm scripts and script-runner commands via
482
+ * package.json / the filesystem. References resolve relative to the markdown
483
+ * file's own directory. Returns the number of stale references found.
396
484
  */
485
+ function verifyMarkdownRefs(files, commands, filePath, silent) {
486
+ const basePath = (0, node_path_1.dirname)((0, node_path_1.resolve)(process.cwd(), filePath));
487
+ let errorCount = 0;
488
+ const report = (err, line) => {
489
+ if (!silent) {
490
+ console.log(` ✗ line ${String(line)}: ${err.message}`);
491
+ ghAnnotate("error", err.message, filePath, line);
492
+ }
493
+ errorCount++;
494
+ };
495
+ for (const f of files) {
496
+ const err = (0, compile_js_1.validateFileRef)(f.path, basePath);
497
+ if (err)
498
+ report(err, f.line);
499
+ }
500
+ for (const c of commands) {
501
+ const err = (0, compile_js_1.validateCommandRef)(c.command, basePath);
502
+ if (err)
503
+ report(err, c.line);
504
+ }
505
+ return errorCount;
506
+ }
397
507
  function verifyInlineRules(filePath, silent, linterOptions) {
398
508
  const log = (msg) => {
399
509
  if (!silent)
@@ -404,11 +514,14 @@ function verifyInlineRules(filePath, silent, linterOptions) {
404
514
  content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), filePath), "utf-8");
405
515
  }
406
516
  catch {
407
- return { ok: true, errorCount: 0, ruleCount: 0 };
517
+ return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
408
518
  }
409
- const { rules, errors: parseErrors } = (0, inline_js_1.parseInlineRules)(content);
410
- if (rules.length === 0 && parseErrors.length === 0) {
411
- return { ok: true, errorCount: 0, ruleCount: 0 };
519
+ const { rules, files, commands, errors: parseErrors, } = (0, inline_js_1.parseInlineRules)(content);
520
+ if (rules.length === 0 &&
521
+ files.length === 0 &&
522
+ commands.length === 0 &&
523
+ parseErrors.length === 0) {
524
+ return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
412
525
  }
413
526
  let errorCount = 0;
414
527
  log(`\n${filePath} (inline mode):`);
@@ -420,28 +533,118 @@ function verifyInlineRules(filePath, silent, linterOptions) {
420
533
  }
421
534
  }
422
535
  for (const rule of rules) {
423
- const result = (0, linters_js_1.checkLinterRule)(rule.linterRule, process.cwd(), linterOptions);
424
- if (!result.exists) {
425
- const message = result.error ?? `Rule "${rule.linterRule}" not found`;
426
- log(` ✗ line ${String(rule.line)}: ${message}`);
536
+ if (!verifyOneRule(rule, filePath, silent, linterOptions))
427
537
  errorCount++;
428
- if (!silent) {
429
- ghAnnotate("error", message, filePath, rule.line);
430
- }
538
+ }
539
+ errorCount += verifyMarkdownRefs(files, commands, filePath, silent);
540
+ return {
541
+ ok: errorCount === 0,
542
+ errorCount,
543
+ ruleCount: rules.length + files.length + commands.length,
544
+ ruleNames: rules.map((r) => r.linterRule),
545
+ };
546
+ }
547
+ /**
548
+ * Verify `vigiles.enforce` rules declared in a file's YAML frontmatter.
549
+ * Same engine as inline/spec rules. Rules whose reference already appeared
550
+ * in `exclude` (e.g. declared inline in the same file) are skipped so a
551
+ * rule present in both sources is reported once, not twice.
552
+ */
553
+ function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
554
+ const log = (msg) => {
555
+ if (!silent)
556
+ console.log(msg);
557
+ };
558
+ let content;
559
+ try {
560
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), filePath), "utf-8");
561
+ }
562
+ catch {
563
+ return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
564
+ }
565
+ const { rules: allRules, files, commands, errors: parseErrors, } = (0, frontmatter_js_1.parseFrontmatterRules)(content);
566
+ const rules = allRules.filter((r) => !exclude.has(r.linterRule));
567
+ if (rules.length === 0 &&
568
+ files.length === 0 &&
569
+ commands.length === 0 &&
570
+ parseErrors.length === 0) {
571
+ return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
572
+ }
573
+ let errorCount = 0;
574
+ log(`\n${filePath} (frontmatter mode):`);
575
+ for (const err of parseErrors) {
576
+ log(` ✗ line ${String(err.line)}: ${err.message}`);
577
+ errorCount++;
578
+ if (!silent) {
579
+ ghAnnotate("error", err.message, filePath, err.line);
431
580
  }
432
- else if (result.enabled === "disabled") {
433
- const message = `Rule "${rule.linterRule}" exists but is disabled in ${result.linter} config`;
434
- log(` ✗ line ${String(rule.line)}: ${message}`);
581
+ }
582
+ for (const rule of rules) {
583
+ if (!verifyOneRule(rule, filePath, silent, linterOptions))
435
584
  errorCount++;
436
- if (!silent) {
437
- ghAnnotate("error", message, filePath, rule.line);
438
- }
585
+ }
586
+ errorCount += verifyMarkdownRefs(files, commands, filePath, silent);
587
+ return {
588
+ ok: errorCount === 0,
589
+ errorCount,
590
+ ruleCount: rules.length + files.length + commands.length,
591
+ ruleNames: rules.map((r) => r.linterRule),
592
+ };
593
+ }
594
+ /**
595
+ * Verify inline `<!-- vigiles:enforce -->` comments and `vigiles:` YAML
596
+ * frontmatter in instruction files that aren't managed by a spec.
597
+ *
598
+ * Spec mode is the source of truth when it exists, so a literal
599
+ * `<!-- vigiles:enforce ... -->` snippet that survived into compiled
600
+ * markdown (or an example in a spec-managed file) must not trip audit. A
601
+ * file is spec-managed iff it has a sibling `<file>.spec.ts` OR its own
602
+ * `<!-- vigiles:sha256:... compiled from <spec> -->` header. A rule
603
+ * declared both inline and in frontmatter is verified once (inline wins as
604
+ * the first source). See docs/markdown-mode.md.
605
+ */
606
+ function verifyMarkdownModeRules(files, silent, config) {
607
+ const totals = {
608
+ inlineErrors: 0,
609
+ inlineRules: 0,
610
+ frontmatterErrors: 0,
611
+ frontmatterRules: 0,
612
+ };
613
+ if (!silent && files.length > 0) {
614
+ console.log("\nInline + frontmatter rule verification:");
615
+ }
616
+ const linterOptions = {
617
+ catalogOnly: config?.catalogOnly,
618
+ linters: config?.linters,
619
+ };
620
+ const compiledFromRe = /<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from .+?\s*-->/;
621
+ for (const filePath of files) {
622
+ const abs = (0, node_path_1.resolve)(process.cwd(), filePath);
623
+ if ((0, node_fs_1.existsSync)(`${abs}.spec.ts`))
624
+ continue; // managed by sibling spec
625
+ let content;
626
+ try {
627
+ content = (0, node_fs_1.readFileSync)(abs, "utf-8");
439
628
  }
440
- else {
441
- log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
629
+ catch {
630
+ continue;
442
631
  }
443
- }
444
- return { ok: errorCount === 0, errorCount, ruleCount: rules.length };
632
+ if (compiledFromRe.test(content))
633
+ continue; // managed via hash header
634
+ const inline = verifyInlineRules(filePath, silent, linterOptions);
635
+ totals.inlineErrors += inline.errorCount;
636
+ totals.inlineRules += inline.ruleCount;
637
+ const fm = verifyFrontmatterRules(filePath, silent, new Set(inline.ruleNames), linterOptions);
638
+ totals.frontmatterErrors += fm.errorCount;
639
+ totals.frontmatterRules += fm.ruleCount;
640
+ }
641
+ if (!silent &&
642
+ files.length > 0 &&
643
+ totals.inlineRules === 0 &&
644
+ totals.frontmatterRules === 0) {
645
+ console.log(" (no inline vigiles:enforce comments or vigiles: frontmatter found)");
646
+ }
647
+ return totals;
445
648
  }
446
649
  /**
447
650
  * Unified audit command: verify hashes, report coverage gaps, detect duplicates,
@@ -468,45 +671,10 @@ async function audit(restArgs, flags, config) {
468
671
  const hashResult = files.length > 0
469
672
  ? check(files, silent)
470
673
  : { valid: true, hashErrors: 0, validationErrors: 0 };
471
- // 1b. Verify inline vigiles:enforce comments in any instruction file
472
- // that isn't already managed by a .spec.ts. Spec mode is the source of
473
- // truth when it exists, so a literal `<!-- vigiles:enforce ... -->`
474
- // snippet that survived into the compiled markdown (or an
475
- // explanatory example in a spec-managed file) must not trip audit.
476
- // A file is spec-managed iff it has a sibling `<file>.spec.ts` OR its
477
- // own `<!-- vigiles:sha256:... compiled from <spec> -->` header.
478
- // See docs/inline-mode.md.
479
- let inlineErrors = 0;
480
- let inlineRules = 0;
481
- if (!silent && files.length > 0) {
482
- console.log("\nInline rule verification:");
483
- }
484
- const compiledFromRe = /<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from .+?\s*-->/;
485
- for (const filePath of files) {
486
- const abs = (0, node_path_1.resolve)(process.cwd(), filePath);
487
- if ((0, node_fs_1.existsSync)(`${abs}.spec.ts`)) {
488
- continue; // managed by sibling spec
489
- }
490
- let content;
491
- try {
492
- content = (0, node_fs_1.readFileSync)(abs, "utf-8");
493
- }
494
- catch {
495
- continue;
496
- }
497
- if (compiledFromRe.test(content)) {
498
- continue; // managed by the spec referenced in the hash header
499
- }
500
- const result = verifyInlineRules(filePath, silent, {
501
- catalogOnly: config?.catalogOnly,
502
- linters: config?.linters,
503
- });
504
- inlineErrors += result.errorCount;
505
- inlineRules += result.ruleCount;
506
- }
507
- if (!silent && files.length > 0 && inlineRules === 0) {
508
- console.log(" (no inline vigiles:enforce comments found)");
509
- }
674
+ // 1b. Verify inline + frontmatter rules in instruction files not managed
675
+ // by a spec. See verifyMarkdownModeRules / docs/markdown-mode.md.
676
+ const md = verifyMarkdownModeRules(files, silent, config);
677
+ const { inlineErrors, inlineRules, frontmatterErrors, frontmatterRules } = md;
510
678
  // 2. Coverage gaps (discover)
511
679
  if (!silent)
512
680
  console.log("\nLinter rule coverage:\n");
@@ -519,25 +687,63 @@ async function audit(restArgs, flags, config) {
519
687
  const dups = await findDuplicateRules(0.3, silent, restArgs.length > 0 ? files : undefined);
520
688
  // 4. Guidance rule count (strengthen suggestions moved to /strengthen skill)
521
689
  const guidanceCount = await countGuidanceRules(silent);
522
- // 5. Freshness check
523
- const freshnessSeverity = config?.rules.freshness;
524
- let freshnessErrors = 0;
525
- if (freshnessSeverity) {
690
+ // 5. Integrity check (hand-edit detection via SHA-256 hash)
691
+ const integritySeverity = config?.rules.integrity ?? "warn";
692
+ let integrityErrors = 0;
693
+ if (integritySeverity) {
526
694
  if (!silent)
527
- console.log("\nFreshness check:\n");
528
- const mode = config?.freshnessMode ?? "strict";
529
- freshnessErrors = await checkFreshness(files, mode, config, freshnessSeverity, silent);
695
+ console.log("\nIntegrity check:\n");
696
+ integrityErrors = checkIntegrityForFiles(files, integritySeverity, silent);
697
+ }
698
+ // 6. Coverage thresholds (gates CI when severity is "error")
699
+ const coverageErrors = await checkCoverageThresholds(coverage, config, silent);
700
+ // 7. Orphan docs check — find .md files no other markdown references.
701
+ // Enforces the `vigiles/orphan-docs` built-in rule when declared in a
702
+ // spec. Include/exclude come from .vigilesrc.json#orphans (tsconfig-
703
+ // style globs); default include is docs/ + research/ for the
704
+ // vigiles-repo convention.
705
+ if (!silent)
706
+ console.log("\nOrphan docs check:\n");
707
+ const orphanReport = (0, orphans_js_1.findOrphanDocs)({
708
+ basePath: process.cwd(),
709
+ include: config?.orphans?.include,
710
+ exclude: config?.orphans?.exclude,
711
+ });
712
+ if (!silent) {
713
+ for (const line of (0, orphans_js_1.formatOrphanReport)(orphanReport).split("\n")) {
714
+ console.log(` ${line}`);
715
+ }
716
+ }
717
+ // 8. Validate vigiles builder calls inside markdown code blocks. Default
718
+ // is to validate every ref; illustrative blocks opt out via
719
+ // `<!-- vigiles:ignore -->` (single block) or
720
+ // `<!-- vigiles:ignore-file -->` (whole file). Same engine as spec.ts.
721
+ if (!silent)
722
+ console.log("\nMarkdown code block refs:\n");
723
+ const docRefReport = (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd() });
724
+ if (!silent) {
725
+ for (const line of (0, doc_refs_js_1.formatDocRefReport)(docRefReport).split("\n")) {
726
+ console.log(` ${line}`);
727
+ }
530
728
  }
729
+ // 9. Verify code-shaped symbol references live (see src/refs.ts).
730
+ const symbolRefErrors = verifyMarkdownSymbols(files, silent);
531
731
  const report = {
532
732
  hashErrors: hashResult.hashErrors,
533
733
  validationErrors: hashResult.validationErrors,
534
734
  inlineErrors,
535
735
  inlineRules,
736
+ frontmatterErrors,
737
+ frontmatterRules,
536
738
  duplicatePairs: dups.pairCount,
537
739
  coverageEnabled: coverage.enabled,
538
740
  coverageDocumented: coverage.documented,
539
741
  strengthenSuggestions: guidanceCount,
540
- freshnessErrors,
742
+ integrityErrors,
743
+ coverageErrors,
744
+ orphanCount: orphanReport.orphans.length,
745
+ docRefErrors: docRefReport.errors.length,
746
+ symbolRefErrors,
541
747
  files,
542
748
  };
543
749
  if (summary) {
@@ -557,15 +763,23 @@ function printAuditSummary(report) {
557
763
  parts.push(`${String(report.validationErrors)} validation errors`);
558
764
  if (report.inlineErrors > 0)
559
765
  parts.push(`${String(report.inlineErrors)} inline errors`);
766
+ if (report.frontmatterErrors > 0)
767
+ parts.push(`${String(report.frontmatterErrors)} frontmatter errors`);
560
768
  if (report.duplicatePairs > 0)
561
769
  parts.push(`${String(report.duplicatePairs)} duplicates`);
770
+ if (report.orphanCount > 0)
771
+ parts.push(`${String(report.orphanCount)} orphan docs`);
772
+ if (report.docRefErrors > 0)
773
+ parts.push(`${String(report.docRefErrors)} broken doc refs`);
774
+ if (report.symbolRefErrors > 0)
775
+ parts.push(`${String(report.symbolRefErrors)} broken symbol refs`);
562
776
  const undocumented = report.coverageEnabled - report.coverageDocumented;
563
777
  if (undocumented > 0)
564
778
  parts.push(`${String(undocumented)} undocumented rules`);
565
779
  if (report.strengthenSuggestions > 0)
566
780
  parts.push(`${String(report.strengthenSuggestions)} guidance (run /strengthen to upgrade)`);
567
- if (report.freshnessErrors > 0)
568
- parts.push(`${String(report.freshnessErrors)} stale (run vigiles compile)`);
781
+ if (report.integrityErrors > 0)
782
+ parts.push(`${String(report.integrityErrors)} tampered (edit the .spec.ts source)`);
569
783
  if (parts.length === 0) {
570
784
  console.log("vigiles: clean");
571
785
  }
@@ -883,6 +1097,15 @@ async function setup(args) {
883
1097
  console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
884
1098
  }
885
1099
  console.log(`✓ Generated ${outPath}`);
1100
+ // Also emit a JSON Schema so `vigiles:` markdown frontmatter (Level 1)
1101
+ // gets rule-name autocomplete + typo squiggles from the editor's YAML LSP.
1102
+ const schemaResult = (0, generate_schema_js_1.generateSchema)({
1103
+ basePath: process.cwd(),
1104
+ linters: (0, validate_js_1.loadConfig)().linters,
1105
+ });
1106
+ const schemaPath = ".vigiles/schema.json";
1107
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), schemaPath), schemaResult.json);
1108
+ console.log(`✓ Generated ${schemaPath} (point frontmatter at it with \`# yaml-language-server: $schema=./${schemaPath}\`)`);
886
1109
  // Step 5: Compile specs
887
1110
  console.log("\nCompiling specs...");
888
1111
  const specs = findSpecs();
@@ -991,6 +1214,7 @@ async function setup(args) {
991
1214
  ...targets,
992
1215
  ...specPathsList,
993
1216
  ".vigiles/generated.d.ts",
1217
+ ".vigiles/schema.json",
994
1218
  ...(shouldInstallPlugin ? [".claude/settings.json"] : []),
995
1219
  ...(strict ? [".vigilesrc.json"] : []),
996
1220
  ];
@@ -999,7 +1223,7 @@ async function setup(args) {
999
1223
  // ---------------------------------------------------------------------------
1000
1224
  // Strengthen: guidance() → enforce() suggestions
1001
1225
  // ---------------------------------------------------------------------------
1002
- async function checkFreshness(files, mode, config, severity, silent) {
1226
+ function checkIntegrityForFiles(files, severity, silent) {
1003
1227
  const log = (msg) => {
1004
1228
  if (!silent)
1005
1229
  console.log(msg);
@@ -1010,79 +1234,66 @@ async function checkFreshness(files, mode, config, severity, silent) {
1010
1234
  const abs = (0, node_path_1.resolve)(basePath, filePath);
1011
1235
  if (!(0, node_fs_1.existsSync)(abs))
1012
1236
  continue;
1013
- const content = (0, node_fs_1.readFileSync)(abs, "utf-8");
1014
- // Find the spec that compiled this file
1015
- const hashMatch = content.match(/<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from (.+?)\s*-->/);
1016
- if (!hashMatch) {
1017
- // No hash = hand-written file, skip freshness check
1018
- continue;
1019
- }
1020
- let result;
1021
- const specFile = hashMatch[1];
1022
- if (mode === "strict") {
1023
- // Recompile in memory and diff
1024
- const spec = await loadSpec(specFile);
1025
- if (!spec || spec._specType !== "claude") {
1026
- log(` ? ${filePath} — can't load spec "${specFile}", skipping`);
1027
- continue;
1028
- }
1029
- const compiled = (0, compile_js_1.compileClaude)(spec, {
1030
- basePath,
1031
- specFile,
1032
- maxRules: config?.maxRules,
1033
- maxTokens: config?.maxTokens,
1034
- maxSectionLines: config?.maxSectionLines,
1035
- catalogOnly: config?.catalogOnly,
1036
- linters: config?.linters,
1037
- });
1038
- // Compare markdown body (strip hash/input lines)
1039
- const metaRe = /^<!-- vigiles:(sha256|inputs):[^\n]+ -->\r?\n?/gm;
1040
- const existingBody = content.replace(metaRe, "").trim();
1041
- const compiledBody = compiled.markdown.replace(metaRe, "").trim();
1042
- if (existingBody === compiledBody) {
1043
- result = { fresh: true, mode: "strict" };
1044
- }
1045
- else {
1046
- result = {
1047
- fresh: false,
1048
- mode: "strict",
1049
- reason: "Output would differ if recompiled — run `vigiles compile`",
1050
- };
1051
- }
1052
- }
1053
- else if (mode === "input-hash") {
1054
- const specFile = hashMatch[1];
1055
- const spec = await loadSpec(specFile);
1056
- if (!spec || spec._specType !== "claude") {
1057
- log(` ? ${filePath} — can't load spec "${specFile}", skipping`);
1058
- continue;
1059
- }
1060
- const inputs = (0, freshness_js_1.discoverInputs)(specFile, spec, basePath, config?.freshnessInputs);
1061
- result = (0, freshness_js_1.checkInputHashFreshness)(content, inputs.files, basePath);
1062
- }
1063
- else {
1064
- // output-hash mode
1065
- result = (0, freshness_js_1.checkOutputHashFreshness)(content);
1066
- }
1067
- if (!result.fresh) {
1237
+ const result = (0, integrity_js_1.checkIntegrity)((0, node_fs_1.readFileSync)(abs, "utf-8"));
1238
+ if (!result.intact) {
1068
1239
  errorCount++;
1069
1240
  const marker = severity === "error" ? "✗" : "⚠";
1070
- log(` ${marker} ${filePath} — ${result.reason ?? "stale"}`);
1071
- if (result.changedFiles && result.changedFiles.length > 0) {
1072
- for (const f of result.changedFiles) {
1073
- log(` changed: ${f}`);
1074
- }
1075
- }
1241
+ log(` ${marker} ${filePath} — ${result.reason ?? "tampered"}`);
1076
1242
  }
1077
1243
  else if (!silent) {
1078
- log(` ✓ ${filePath} — fresh (${mode})`);
1244
+ log(` ✓ ${filePath}`);
1079
1245
  }
1080
1246
  }
1081
1247
  if (errorCount === 0) {
1082
- log(" All files fresh.");
1248
+ log(" All compiled files intact.");
1083
1249
  }
1084
1250
  return severity === "error" ? errorCount : 0;
1085
1251
  }
1252
+ /**
1253
+ * Apply the configured coverage thresholds. Returns the number of failing
1254
+ * thresholds (so the audit can fail CI when severity is "error").
1255
+ *
1256
+ * Loads specs directly via loadSpec() when the scripts threshold is set —
1257
+ * avoids depending on a pre-built `dist/` tree, which the setup-generated
1258
+ * CI step doesn't guarantee.
1259
+ */
1260
+ async function checkCoverageThresholds(coverage, config, silent) {
1261
+ const severity = (0, types_js_1.ruleSeverity)(config?.rules.coverage);
1262
+ if (!severity)
1263
+ return 0;
1264
+ const opts = (0, types_js_1.ruleOptions)(config?.rules.coverage);
1265
+ if (!opts)
1266
+ return 0;
1267
+ const log = (msg) => {
1268
+ if (!silent)
1269
+ console.log(msg);
1270
+ };
1271
+ let failing = 0;
1272
+ if (!silent)
1273
+ console.log("\nCoverage thresholds:\n");
1274
+ if (opts.linterRules !== undefined) {
1275
+ const pct = coverage.enabled > 0
1276
+ ? Math.round((coverage.documented / coverage.enabled) * 100)
1277
+ : 100;
1278
+ const ok = pct >= opts.linterRules;
1279
+ if (!ok)
1280
+ failing++;
1281
+ const marker = ok ? "✓" : severity === "error" ? "✗" : "⚠";
1282
+ log(` ${marker} linterRules: ${String(pct)}% (threshold: ${String(opts.linterRules)}%)`);
1283
+ }
1284
+ if (opts.scripts !== undefined) {
1285
+ // Load all claude specs so coverage doesn't depend on a built dist/.
1286
+ const loaded = await Promise.all(findSpecs().map(loadSpec));
1287
+ const claudeSpecs = loaded.filter((s) => s?._specType === "claude");
1288
+ const metric = (0, coverage_js_1.computeScriptCoverage)(process.cwd(), opts.scripts, claudeSpecs);
1289
+ const ok = metric.passing;
1290
+ if (!ok)
1291
+ failing++;
1292
+ const marker = ok ? "✓" : severity === "error" ? "✗" : "⚠";
1293
+ log(` ${marker} scripts: ${String(metric.percent)}% (threshold: ${String(opts.scripts)}%)`);
1294
+ }
1295
+ return severity === "error" ? failing : 0;
1296
+ }
1086
1297
  async function countGuidanceRules(silent = false) {
1087
1298
  const specs = findSpecs();
1088
1299
  if (specs.length === 0)
@@ -1165,6 +1376,43 @@ function handleGenerateTypes(args, restArgs) {
1165
1376
  (0, node_fs_1.writeFileSync)(fullOut, result.dts);
1166
1377
  console.log(`\n✓ Generated ${outPath}`);
1167
1378
  }
1379
+ function handleGenerateSchema(args, restArgs) {
1380
+ const checkOnly = args.includes("--check");
1381
+ const outPath = restArgs[0] ?? ".vigiles/schema.json";
1382
+ console.log("Scanning linters...\n");
1383
+ const result = (0, generate_schema_js_1.generateSchema)({
1384
+ basePath: process.cwd(),
1385
+ linters: (0, validate_js_1.loadConfig)().linters,
1386
+ });
1387
+ for (const l of result.linters) {
1388
+ console.log(` ${l.linter}: ${String(l.count)} rules`);
1389
+ }
1390
+ console.log(` schema enum: ${String(result.ruleNames.length)} rule names`);
1391
+ const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
1392
+ if (checkOnly) {
1393
+ if (!(0, node_fs_1.existsSync)(fullOut)) {
1394
+ console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate-schema\` to create it.`);
1395
+ process.exit(1);
1396
+ }
1397
+ const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
1398
+ if (existing.trim() === result.json.trim()) {
1399
+ console.log(`\n✓ ${outPath} is up to date`);
1400
+ }
1401
+ else {
1402
+ console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate-schema\` to update.`);
1403
+ process.exit(1);
1404
+ }
1405
+ return;
1406
+ }
1407
+ const outDir = fullOut.substring(0, fullOut.lastIndexOf("/"));
1408
+ if (outDir && !(0, node_fs_1.existsSync)(outDir)) {
1409
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
1410
+ }
1411
+ (0, node_fs_1.writeFileSync)(fullOut, result.json);
1412
+ console.log(`\n✓ Generated ${outPath}`);
1413
+ console.log(" Add to your markdown frontmatter:\n" +
1414
+ ` # yaml-language-server: $schema=./${outPath}`);
1415
+ }
1168
1416
  function printUsage(command) {
1169
1417
  console.log("vigiles — compile typed specs to instruction files");
1170
1418
  console.log("");
@@ -1181,6 +1429,8 @@ function printUsage(command) {
1181
1429
  console.log("Plumbing:");
1182
1430
  console.log(" vigiles generate-types [out] Emit .d.ts from project state");
1183
1431
  console.log(" vigiles generate-types --check Verify .d.ts is up to date");
1432
+ console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
1433
+ console.log(" vigiles generate-schema --check Verify schema.json is up to date");
1184
1434
  if (command && command !== "--help") {
1185
1435
  console.log(`\nUnknown command: "${command}"`);
1186
1436
  process.exit(1);
@@ -1189,6 +1439,242 @@ function printUsage(command) {
1189
1439
  // ---------------------------------------------------------------------------
1190
1440
  // Main
1191
1441
  // ---------------------------------------------------------------------------
1442
+ /**
1443
+ * Emit GitHub Actions annotations for an audit report. Skipped when --json or
1444
+ * --summary is active — those modes promise clean machine-readable stdout, and
1445
+ * ::error/::warning lines would contaminate output parsed as JSON.
1446
+ */
1447
+ function annotateAuditForGitHub(report, flags) {
1448
+ const structuredOutput = flags.includes("--json") || flags.includes("--summary");
1449
+ if (!isGitHubActions() || structuredOutput)
1450
+ return;
1451
+ if (report.hashErrors > 0) {
1452
+ ghAnnotate("error", `${String(report.hashErrors)} compiled file(s) with stale hash — run vigiles compile`);
1453
+ }
1454
+ if (report.validationErrors > 0) {
1455
+ ghAnnotate("error", `${String(report.validationErrors)} spec validation failure(s) — see audit output`);
1456
+ }
1457
+ if (report.duplicatePairs > 0) {
1458
+ ghAnnotate("warning", `${String(report.duplicatePairs)} near-duplicate rule pair(s) detected — consider merging`);
1459
+ }
1460
+ }
1461
+ /**
1462
+ * Run a compiled skill's deterministic gate ladder: execute each step gate in
1463
+ * order (short-circuiting on the first failure), then the result gate. This is
1464
+ * the v0 runtime — it enforces the `vigiles:gate`/`vigiles:result` markers a
1465
+ * compiled SKILL.md carries. It does not yet drive the model through the prose
1466
+ * steps (that needs a live harness).
1467
+ */
1468
+ function runSkillCommand(target) {
1469
+ if (!target) {
1470
+ console.error("Usage: vigiles run-skill <SKILL.md>");
1471
+ process.exit(2);
1472
+ }
1473
+ const path = (0, node_path_1.resolve)(process.cwd(), target);
1474
+ if (!(0, node_fs_1.existsSync)(path)) {
1475
+ console.error(`Not found: ${target}`);
1476
+ process.exit(2);
1477
+ }
1478
+ const gates = (0, skill_runtime_js_1.parseSkillGates)((0, node_fs_1.readFileSync)(path, "utf-8"));
1479
+ if (gates.steps.length === 0 && !gates.result) {
1480
+ console.log(`No vigiles:gate / vigiles:result markers in ${target}.`);
1481
+ return;
1482
+ }
1483
+ console.log(`Running gate ladder for ${target}:\n`);
1484
+ const report = (0, skill_runtime_js_1.runSkillGates)(gates, process.cwd());
1485
+ for (const r of report.results) {
1486
+ const label = r.at === "result" ? "result" : `step ${String(r.at)}`;
1487
+ console.log(` ${r.ok ? "✓" : "✗"} ${label} — ${(0, skill_runtime_js_1.gateLabel)(r.gate)}`);
1488
+ if (!r.ok && r.output) {
1489
+ console.log(r.output
1490
+ .split("\n")
1491
+ .map((l) => ` ${l}`)
1492
+ .join("\n"));
1493
+ }
1494
+ }
1495
+ if (report.ok) {
1496
+ console.log("\n✓ All gates passed.");
1497
+ }
1498
+ else {
1499
+ const where = report.blockedAt === "result"
1500
+ ? "the result gate"
1501
+ : `step ${String(report.blockedAt)}`;
1502
+ console.log(`\n✗ Blocked at ${where} — fix it before the skill is done.`);
1503
+ process.exit(2);
1504
+ }
1505
+ }
1506
+ /**
1507
+ * Stop-hook entrypoint: run the active skill's result gate and decide whether
1508
+ * the agent may stop. Exit 2 (with the reason on stderr) blocks the stop and
1509
+ * feeds the message back to the model; exit 0 allows it and clears the marker.
1510
+ */
1511
+ function skillHookCommand() {
1512
+ const decision = (0, skill_runtime_js_1.evaluateStopHook)(process.cwd());
1513
+ if (decision.allow) {
1514
+ if (decision.message)
1515
+ console.log(decision.message);
1516
+ (0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
1517
+ return;
1518
+ }
1519
+ console.error(decision.message);
1520
+ process.exit(2);
1521
+ }
1522
+ /** Mark a skill active so the Stop hook enforces its result gate. */
1523
+ function skillStartCommand(target) {
1524
+ if (!target) {
1525
+ console.error("Usage: vigiles skill-start <SKILL.md>");
1526
+ process.exit(2);
1527
+ }
1528
+ (0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
1529
+ console.log(`Active skill: ${target}`);
1530
+ }
1531
+ /** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
1532
+ function handleSkillCommand(command, restArgs) {
1533
+ switch (command) {
1534
+ case "run-skill":
1535
+ runSkillCommand(restArgs[0]);
1536
+ return true;
1537
+ case "skill-start":
1538
+ skillStartCommand(restArgs[0]);
1539
+ return true;
1540
+ case "skill-done":
1541
+ (0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
1542
+ return true;
1543
+ case "skill-hook":
1544
+ skillHookCommand();
1545
+ return true;
1546
+ case "action-hook":
1547
+ actionHookCommand();
1548
+ return true;
1549
+ case "refs":
1550
+ refsCommand(restArgs[0]);
1551
+ return true;
1552
+ case "refs-hook":
1553
+ refsHookCommand();
1554
+ return true;
1555
+ default:
1556
+ return false;
1557
+ }
1558
+ }
1559
+ /**
1560
+ * PostToolUse-hook entrypoint for action gates. Reads the tool event on stdin,
1561
+ * runs the matching action gates from `.vigiles/action-gates.json`, and blocks
1562
+ * (exit 2 + reason on stderr) if any fails — plan-agnostic, so it works inside
1563
+ * dynamic workflows where there is no static step to attach a gate to.
1564
+ */
1565
+ function actionHookCommand() {
1566
+ let raw = "";
1567
+ try {
1568
+ raw = (0, node_fs_1.readFileSync)(0, "utf-8");
1569
+ }
1570
+ catch {
1571
+ /* no stdin */
1572
+ }
1573
+ let event = { tool: "" };
1574
+ try {
1575
+ const j = JSON.parse(raw);
1576
+ event = { tool: j.tool_name ?? "", input: j.tool_input };
1577
+ }
1578
+ catch {
1579
+ /* malformed input → no event, allow */
1580
+ }
1581
+ const decision = (0, action_gate_js_1.evaluateAction)(event, (0, action_gate_js_1.loadActionGates)(process.cwd()), process.cwd());
1582
+ if (!decision.allow) {
1583
+ console.error(decision.message);
1584
+ process.exit(2);
1585
+ }
1586
+ }
1587
+ const INSTRUCTION_FILE = /^(SKILL|CLAUDE|AGENTS)\.md$/;
1588
+ function isInstructionFile(file) {
1589
+ return INSTRUCTION_FILE.test((0, node_path_1.basename)(file));
1590
+ }
1591
+ /**
1592
+ * Inspect an instruction file's symbol references: broken file-qualified refs
1593
+ * (`path.ext#symbol` whose file/symbol is wrong) and code-shaped references not
1594
+ * yet marked. Emits one line per finding via `log`; returns whether any issue
1595
+ * was found. `basePath` is the file's own directory (where paths resolve).
1596
+ */
1597
+ function reportRefIssues(markdown, basePath, log) {
1598
+ const broken = (0, refs_js_1.verifySymbolRefs)(markdown, basePath);
1599
+ const unmarked = (0, refs_js_1.unmarkedCodeRefs)(markdown);
1600
+ for (const b of broken) {
1601
+ log(` ✗ line ${String(b.line)}: ${b.reason}`);
1602
+ }
1603
+ for (const u of unmarked) {
1604
+ const callee = u.text.replace(/\s*\([^)]*\)\s*$/, "");
1605
+ log(` ✗ line ${String(u.line)}: \`${u.text}\` is an unmarked code reference — ` +
1606
+ `mark it as \`vigiles:symbol path/to/file.ext#${callee}\` or add <!-- vigiles:ignore --> if it is prose`);
1607
+ }
1608
+ return broken.length > 0 || unmarked.length > 0;
1609
+ }
1610
+ /** `vigiles refs <file>` — check a file's symbol references (exit 2 on issues). */
1611
+ function refsCommand(target) {
1612
+ if (!target) {
1613
+ console.error("Usage: vigiles refs <instruction-file.md>");
1614
+ process.exit(2);
1615
+ }
1616
+ const cwd = process.cwd();
1617
+ let markdown;
1618
+ try {
1619
+ markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, target), "utf-8");
1620
+ }
1621
+ catch {
1622
+ console.error(`Cannot read ${target}`);
1623
+ process.exit(2);
1624
+ }
1625
+ const bad = reportRefIssues(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, target)), (m) => {
1626
+ console.log(m);
1627
+ });
1628
+ if (bad)
1629
+ process.exit(2);
1630
+ console.log(`✓ ${target}: all code references are marked and resolve.`);
1631
+ }
1632
+ /**
1633
+ * PostToolUse-hook entrypoint: when the agent edits an instruction file, force
1634
+ * every code reference to carry a file-qualified mark (`path.ext#symbol`) and
1635
+ * verify the marked ones against the named file. Exit 2 (reason on stderr)
1636
+ * blocks the edit and feeds the fix back to the agent — the harness makes the
1637
+ * agent mark its references, at write time, with full context. `vigiles:ignore`
1638
+ * opts a prose span out.
1639
+ */
1640
+ function refsHookCommand() {
1641
+ let raw = "";
1642
+ try {
1643
+ raw = (0, node_fs_1.readFileSync)(0, "utf-8");
1644
+ }
1645
+ catch {
1646
+ /* no stdin */
1647
+ }
1648
+ let file = "";
1649
+ try {
1650
+ const j = JSON.parse(raw);
1651
+ file = j.tool_input?.file_path ?? "";
1652
+ }
1653
+ catch {
1654
+ /* malformed → nothing to do */
1655
+ }
1656
+ if (!file || !isInstructionFile(file))
1657
+ return;
1658
+ const cwd = process.cwd();
1659
+ const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
1660
+ let markdown;
1661
+ try {
1662
+ markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, file), "utf-8");
1663
+ }
1664
+ catch {
1665
+ return;
1666
+ }
1667
+ const lines = [];
1668
+ const bad = reportRefIssues(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)), (m) => {
1669
+ lines.push(m);
1670
+ });
1671
+ if (bad) {
1672
+ console.error(`vigiles: fix the code references in ${target}:`);
1673
+ for (const l of lines)
1674
+ console.error(l);
1675
+ process.exit(2);
1676
+ }
1677
+ }
1192
1678
  async function main() {
1193
1679
  const args = process.argv.slice(2);
1194
1680
  const command = args[0];
@@ -1232,23 +1718,8 @@ async function main() {
1232
1718
  // audit = verify + discover + guidance count
1233
1719
  const flags = args.slice(1).filter((a) => a.startsWith("--"));
1234
1720
  const report = await audit(restArgs, flags, config);
1721
+ annotateAuditForGitHub(report, flags);
1235
1722
  const exitCode = auditExitCode(report);
1236
- // Skip GH annotations when --json or --summary is active —
1237
- // those modes promise clean machine-readable stdout, and
1238
- // ::error/::warning lines would contaminate the output for
1239
- // callers parsing it as JSON.
1240
- const structuredOutput = flags.includes("--json") || flags.includes("--summary");
1241
- if (isGitHubActions() && !structuredOutput) {
1242
- if (report.hashErrors > 0) {
1243
- ghAnnotate("error", `${String(report.hashErrors)} compiled file(s) with stale hash — run vigiles compile`);
1244
- }
1245
- if (report.validationErrors > 0) {
1246
- ghAnnotate("error", `${String(report.validationErrors)} spec validation failure(s) — see audit output`);
1247
- }
1248
- if (report.duplicatePairs > 0) {
1249
- ghAnnotate("warning", `${String(report.duplicatePairs)} near-duplicate rule pair(s) detected — consider merging`);
1250
- }
1251
- }
1252
1723
  if (exitCode !== 0) {
1253
1724
  process.exit(exitCode);
1254
1725
  }
@@ -1258,8 +1729,12 @@ async function main() {
1258
1729
  case "generate-types":
1259
1730
  handleGenerateTypes(args, restArgs);
1260
1731
  break;
1732
+ case "generate-schema":
1733
+ handleGenerateSchema(args, restArgs);
1734
+ break;
1261
1735
  default:
1262
- printUsage(command);
1736
+ if (!handleSkillCommand(command, restArgs))
1737
+ printUsage(command);
1263
1738
  break;
1264
1739
  }
1265
1740
  }