vigiles 2.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 (156) hide show
  1. package/.claude/settings.json +46 -0
  2. package/.claude/settings.local.json +8 -0
  3. package/.claude-plugin/hooks/post-edit.sh +34 -0
  4. package/.claude-plugin/hooks/pre-edit.sh +40 -0
  5. package/.claude-plugin/hooks/session-start.sh +38 -0
  6. package/.claude-plugin/marketplace.json +14 -0
  7. package/.claude-plugin/plugin.json +47 -0
  8. package/.github/workflows/ci.yml +81 -0
  9. package/.prettierignore +1 -0
  10. package/.vigiles/generated.d.ts +205 -0
  11. package/CLAUDE.md +95 -0
  12. package/CLAUDE.md.spec.ts +142 -0
  13. package/CONTRIBUTING.md +121 -0
  14. package/LICENSE +21 -0
  15. package/README.md +377 -0
  16. package/action.yml +25 -0
  17. package/dist/action.d.ts +7 -0
  18. package/dist/action.d.ts.map +1 -0
  19. package/dist/action.js +180 -0
  20. package/dist/action.js.map +1 -0
  21. package/dist/cli.d.ts +12 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +1267 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/cli.test.d.ts +2 -0
  26. package/dist/cli.test.d.ts.map +1 -0
  27. package/dist/cli.test.js +650 -0
  28. package/dist/cli.test.js.map +1 -0
  29. package/dist/compile.d.ts +101 -0
  30. package/dist/compile.d.ts.map +1 -0
  31. package/dist/compile.js +503 -0
  32. package/dist/compile.js.map +1 -0
  33. package/dist/evolve.d.ts +132 -0
  34. package/dist/evolve.d.ts.map +1 -0
  35. package/dist/evolve.js +599 -0
  36. package/dist/evolve.js.map +1 -0
  37. package/dist/freshness.d.ts +67 -0
  38. package/dist/freshness.d.ts.map +1 -0
  39. package/dist/freshness.js +244 -0
  40. package/dist/freshness.js.map +1 -0
  41. package/dist/freshness.test.d.ts +2 -0
  42. package/dist/freshness.test.d.ts.map +1 -0
  43. package/dist/freshness.test.js +356 -0
  44. package/dist/freshness.test.js.map +1 -0
  45. package/dist/generate-types.d.ts +34 -0
  46. package/dist/generate-types.d.ts.map +1 -0
  47. package/dist/generate-types.js +381 -0
  48. package/dist/generate-types.js.map +1 -0
  49. package/dist/inline.d.ts +58 -0
  50. package/dist/inline.d.ts.map +1 -0
  51. package/dist/inline.js +142 -0
  52. package/dist/inline.js.map +1 -0
  53. package/dist/inline.test.d.ts +5 -0
  54. package/dist/inline.test.d.ts.map +1 -0
  55. package/dist/inline.test.js +152 -0
  56. package/dist/inline.test.js.map +1 -0
  57. package/dist/linters.d.ts +38 -0
  58. package/dist/linters.d.ts.map +1 -0
  59. package/dist/linters.js +588 -0
  60. package/dist/linters.js.map +1 -0
  61. package/dist/proofs.d.ts +272 -0
  62. package/dist/proofs.d.ts.map +1 -0
  63. package/dist/proofs.js +622 -0
  64. package/dist/proofs.js.map +1 -0
  65. package/dist/proofs.test.d.ts +9 -0
  66. package/dist/proofs.test.d.ts.map +1 -0
  67. package/dist/proofs.test.js +952 -0
  68. package/dist/proofs.test.js.map +1 -0
  69. package/dist/spec.d.ts +258 -0
  70. package/dist/spec.d.ts.map +1 -0
  71. package/dist/spec.js +113 -0
  72. package/dist/spec.js.map +1 -0
  73. package/dist/spec.test.d.ts +2 -0
  74. package/dist/spec.test.d.ts.map +1 -0
  75. package/dist/spec.test.js +1222 -0
  76. package/dist/spec.test.js.map +1 -0
  77. package/dist/types.d.ts +101 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +3 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/validate.d.ts +10 -0
  82. package/dist/validate.d.ts.map +1 -0
  83. package/dist/validate.js +286 -0
  84. package/dist/validate.js.map +1 -0
  85. package/dist/validate.test.d.ts +2 -0
  86. package/dist/validate.test.d.ts.map +1 -0
  87. package/dist/validate.test.js +531 -0
  88. package/dist/validate.test.js.map +1 -0
  89. package/docs/agent-setup.md +85 -0
  90. package/docs/agent-workflows.md +103 -0
  91. package/docs/comparison.md +71 -0
  92. package/docs/freshness.md +124 -0
  93. package/docs/inline-mode.md +119 -0
  94. package/docs/linter-support.md +166 -0
  95. package/docs/spec-format.md +194 -0
  96. package/eslint.config.mjs +79 -0
  97. package/examples/CLAUDE.md +54 -0
  98. package/examples/CLAUDE.md.spec.ts +65 -0
  99. package/examples/SKILL.md +50 -0
  100. package/examples/SKILL.md.spec.ts +57 -0
  101. package/fixtures/example-project/CLAUDE.md +11 -0
  102. package/fixtures/example-project/package.json +9 -0
  103. package/fixtures/example-project/src/index.ts +3 -0
  104. package/fixtures/example-project/src/utils.test.ts +2 -0
  105. package/fixtures/example-project/src/utils.ts +3 -0
  106. package/logo.png +0 -0
  107. package/package.json +42 -0
  108. package/research/adoption-strategy.md +111 -0
  109. package/research/agent-integration.md +145 -0
  110. package/research/ai-code-quality.md +197 -0
  111. package/research/code-search-for-agents.md +313 -0
  112. package/research/competitive-landscape.md +163 -0
  113. package/research/doc-freshness.md +516 -0
  114. package/research/executable-specs.md +368 -0
  115. package/research/feature-ideas.md +464 -0
  116. package/research/formal-proofs-for-agents.md +338 -0
  117. package/research/fp-for-agent-harness.md +150 -0
  118. package/research/fp-for-deterministic-ai.md +131 -0
  119. package/research/self-evolving-specs.md +298 -0
  120. package/schemas/claude-md-strict.yml +18 -0
  121. package/schemas/claude-md.yml +6 -0
  122. package/schemas/skill-strict.yml +12 -0
  123. package/schemas/skill.yml +5 -0
  124. package/skills/audit-feedback-loop/SKILL.md +76 -0
  125. package/skills/edit-spec/SKILL.md +131 -0
  126. package/skills/enforce-rules-format/SKILL.md +71 -0
  127. package/skills/generate-logo/SKILL.md +102 -0
  128. package/skills/generate-rule/SKILL.md +90 -0
  129. package/skills/linter-docs/clippy.md +241 -0
  130. package/skills/linter-docs/eslint.md +384 -0
  131. package/skills/linter-docs/pylint.md +288 -0
  132. package/skills/linter-docs/rubocop.md +277 -0
  133. package/skills/linter-docs/ruff.md +187 -0
  134. package/skills/linter-docs/stylelint.md +247 -0
  135. package/skills/migrate-to-spec/SKILL.md +124 -0
  136. package/skills/pr-to-lint-rule/SKILL.md +97 -0
  137. package/skills/strengthen/SKILL.md +168 -0
  138. package/src/action.ts +214 -0
  139. package/src/cli.test.ts +914 -0
  140. package/src/cli.ts +1631 -0
  141. package/src/compile.ts +691 -0
  142. package/src/evolve.ts +781 -0
  143. package/src/freshness.test.ts +449 -0
  144. package/src/freshness.ts +299 -0
  145. package/src/generate-types.ts +448 -0
  146. package/src/inline.test.ts +206 -0
  147. package/src/inline.ts +164 -0
  148. package/src/linters.ts +739 -0
  149. package/src/proofs.test.ts +1314 -0
  150. package/src/proofs.ts +849 -0
  151. package/src/spec.test.ts +1471 -0
  152. package/src/spec.ts +427 -0
  153. package/src/types.ts +117 -0
  154. package/src/validate.test.ts +701 -0
  155. package/src/validate.ts +381 -0
  156. package/tsconfig.json +23 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1267 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * vigiles CLI — compile typed specs to instruction files.
5
+ *
6
+ * Commands:
7
+ * vigiles init — scaffold a spec from scratch
8
+ * vigiles compile — compile .spec.ts → .md with linter verification
9
+ * vigiles audit — verify hashes, report coverage, detect duplicates
10
+ * vigiles generate-types — emit .d.ts with types from project state
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const node_fs_1 = require("node:fs");
14
+ const node_path_1 = require("node:path");
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 compile_js_1 = require("./compile.js");
19
+ const proofs_js_1 = require("./proofs.js");
20
+ const inline_js_1 = require("./inline.js");
21
+ const linters_js_1 = require("./linters.js");
22
+ const freshness_js_1 = require("./freshness.js");
23
+ // ---------------------------------------------------------------------------
24
+ // Constants
25
+ // ---------------------------------------------------------------------------
26
+ 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
+ // ---------------------------------------------------------------------------
30
+ // Spec loading
31
+ // ---------------------------------------------------------------------------
32
+ function findSpecs(pattern) {
33
+ const glob = pattern ?? "**/*.md.spec.ts";
34
+ return (0, glob_1.globSync)(glob, {
35
+ ignore: [...IGNORE_NODE_MODULES, "dist/**"],
36
+ cwd: process.cwd(),
37
+ });
38
+ }
39
+ async function loadSpec(specPath) {
40
+ const fullPath = (0, node_path_1.resolve)(process.cwd(), specPath);
41
+ // Try multiple dist/ path strategies
42
+ const candidates = [];
43
+ // src/ → dist/ mapping (e.g., src/CLAUDE.md.spec.ts → dist/CLAUDE.md.spec.js)
44
+ if (fullPath.includes("/src/")) {
45
+ candidates.push(fullPath.replace(/\/src\//, "/dist/").replace(/\.ts$/, ".js"));
46
+ }
47
+ // Root-level spec → dist/ (e.g., CLAUDE.md.spec.ts → dist/CLAUDE.md.spec.js)
48
+ const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
49
+ const base = fullPath.substring(fullPath.lastIndexOf("/") + 1);
50
+ candidates.push((0, node_path_1.resolve)(dir, "dist", base.replace(/\.ts$/, ".js")));
51
+ // examples/ → dist/examples/ mapping
52
+ candidates.push(fullPath
53
+ .replace(/\.ts$/, ".js")
54
+ .replace(process.cwd(), (0, node_path_1.resolve)(process.cwd(), "dist")));
55
+ for (const distPath of candidates) {
56
+ if ((0, node_fs_1.existsSync)(distPath)) {
57
+ try {
58
+ const mod = (await import(distPath));
59
+ // CJS double-default: `{ default: { default: spec } }`.
60
+ const raw = mod.default;
61
+ if (raw && typeof raw === "object" && "default" in raw) {
62
+ return raw.default;
63
+ }
64
+ return raw;
65
+ }
66
+ catch {
67
+ // Try next candidate
68
+ }
69
+ }
70
+ }
71
+ // Try loading .ts directly via tsx
72
+ try {
73
+ const { execSync } = require("node:child_process");
74
+ // Handle ESM/CJS double-default: m.default may itself have a .default
75
+ const script = `import(${JSON.stringify(fullPath)}).then(m => { const d = m.default?.default ?? m.default; console.log(JSON.stringify(d)); })`;
76
+ const output = execSync(`npx tsx -e '${script.replace(/'/g, "'\\''")}'`, {
77
+ encoding: "utf-8",
78
+ cwd: process.cwd(),
79
+ stdio: ["pipe", "pipe", "pipe"],
80
+ timeout: 15000,
81
+ });
82
+ return JSON.parse(output.trim());
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // Output helpers
90
+ // ---------------------------------------------------------------------------
91
+ function printErrors(specFile, errors) {
92
+ for (const err of errors) {
93
+ const pathInfo = err.path ? ` (${err.path})` : "";
94
+ console.log(` [${err.type}] ${err.message}${pathInfo}`);
95
+ console.log(`::error file=${specFile}::${err.message}`);
96
+ }
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Commands
100
+ // ---------------------------------------------------------------------------
101
+ async function compile(specPaths, config) {
102
+ let allValid = true;
103
+ for (const specPath of specPaths) {
104
+ const spec = await loadSpec(specPath);
105
+ if (!spec) {
106
+ console.log(`\n✗ ${specPath} — failed to load`);
107
+ console.log(` Ensure the spec is compiled: run \`npm run build\` first.`);
108
+ allValid = false;
109
+ continue;
110
+ }
111
+ const basePath = process.cwd();
112
+ 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);
153
+ 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
+ }
158
+ 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);
171
+ allValid = false;
172
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, outputPath), markdown);
173
+ }
174
+ }
175
+ }
176
+ return allValid;
177
+ }
178
+ /** True when running inside a GitHub Actions workflow. */
179
+ function isGitHubActions() {
180
+ return process.env.GITHUB_ACTIONS === "true";
181
+ }
182
+ /**
183
+ * Emit a GitHub Actions annotation for the inline PR experience.
184
+ * No-op outside GitHub Actions.
185
+ */
186
+ function ghAnnotate(level, message, file, line) {
187
+ if (!isGitHubActions())
188
+ return;
189
+ const locParts = [];
190
+ if (file)
191
+ locParts.push(`file=${file}`);
192
+ if (line !== undefined)
193
+ locParts.push(`line=${String(line)}`);
194
+ const loc = locParts.length > 0 ? " " + locParts.join(",") : "";
195
+ console.log(`::${level}${loc}::${message}`);
196
+ }
197
+ function verifyHashes(filePaths, silent = false) {
198
+ let errorCount = 0;
199
+ const log = (msg) => {
200
+ if (!silent)
201
+ console.log(msg);
202
+ };
203
+ for (const filePath of filePaths) {
204
+ const fullPath = (0, node_path_1.resolve)(process.cwd(), filePath);
205
+ // If the file doesn't exist at all (typo, deleted), that's an error —
206
+ // not a "no hash" informational message. Without this check, a scoped
207
+ // audit like `vigiles audit typo.md` would silently exit clean.
208
+ if (!(0, node_fs_1.existsSync)(fullPath)) {
209
+ log(`\n✗ ${filePath} — file not found`);
210
+ if (!silent) {
211
+ ghAnnotate("error", `File not found: ${filePath}`, filePath);
212
+ }
213
+ errorCount++;
214
+ continue;
215
+ }
216
+ const result = (0, compile_js_1.checkFileHash)(fullPath);
217
+ if (!result.hasHash) {
218
+ log(`\n- ${filePath} — no vigiles hash (hand-written or pre-v2)`);
219
+ continue;
220
+ }
221
+ if (result.valid) {
222
+ log(`\n✓ ${filePath} — hash valid (from ${result.specFile})`);
223
+ continue;
224
+ }
225
+ log(`\n✗ ${filePath} — hash mismatch (manually edited after compilation)`);
226
+ log(` Re-run \`vigiles compile\` to regenerate from ${result.specFile ?? "spec"}.`);
227
+ if (!silent) {
228
+ ghAnnotate("error", "Hash mismatch — file was manually edited after compilation", filePath);
229
+ }
230
+ errorCount++;
231
+ }
232
+ return { valid: errorCount === 0, errorCount };
233
+ }
234
+ function validateSpecs(filePaths, rulesConfig, silent = false) {
235
+ let allValid = true;
236
+ const log = (msg) => {
237
+ if (!silent)
238
+ console.log(msg);
239
+ };
240
+ for (const filePath of filePaths) {
241
+ const fullPath = (0, node_path_1.resolve)(process.cwd(), filePath);
242
+ let content;
243
+ try {
244
+ content = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
245
+ }
246
+ catch {
247
+ continue;
248
+ }
249
+ // Multi-target: if file has a "compiled from" hash, it has a spec
250
+ // even if it's not named <file>.spec.ts (e.g., AGENTS.md from CLAUDE.md.spec.ts)
251
+ const hashMatch = content.match(/<!-- vigiles:sha256:[a-f0-9]+ compiled from (.+) -->/);
252
+ if (hashMatch) {
253
+ // Verify the referenced spec still exists
254
+ const specRef = (0, node_path_1.resolve)(process.cwd(), hashMatch[1]);
255
+ if (!(0, node_fs_1.existsSync)(specRef)) {
256
+ log(` ✗ [require-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
257
+ allValid = false;
258
+ }
259
+ continue;
260
+ }
261
+ const result = (0, validate_js_1.validate)(content, {
262
+ filePath: fullPath,
263
+ rules: rulesConfig,
264
+ });
265
+ for (const err of result.errors) {
266
+ log(` ✗ [${err.rule}] ${err.message}`);
267
+ allValid = false;
268
+ }
269
+ for (const warn of result.warnings) {
270
+ log(` ⚠ [${warn.rule}] ${warn.message}`);
271
+ }
272
+ }
273
+ return allValid;
274
+ }
275
+ function check(filePaths, silent = false) {
276
+ const hashes = verifyHashes(filePaths, silent);
277
+ const vConfig = (0, validate_js_1.loadConfig)();
278
+ const specsValid = validateSpecs(filePaths, vConfig.rules, silent);
279
+ return {
280
+ valid: hashes.valid && specsValid,
281
+ hashErrors: hashes.errorCount,
282
+ // `validateSpecs` only returns a boolean today, so we collapse
283
+ // failures to 1 until it starts reporting counts. Kept in its own
284
+ // counter so audit's "stale hash — run vigiles compile" remediation
285
+ // doesn't misreport a require-spec / other validation failure.
286
+ validationErrors: specsValid ? 0 : 1,
287
+ };
288
+ }
289
+ /**
290
+ * Find near-duplicate rules within each spec using NCD similarity.
291
+ * Catches spec bloat — rules that likely say the same thing in different words.
292
+ * Uses information-theoretic distance (gzip-based) — no LLM, fully deterministic.
293
+ */
294
+ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
295
+ const log = (msg) => {
296
+ if (!silent)
297
+ console.log(msg);
298
+ };
299
+ const allSpecs = findSpecs();
300
+ // If audit was invoked with explicit file arguments, only scan the specs
301
+ // for those files — otherwise an unrelated duplicate elsewhere in the
302
+ // repo would fail a targeted CI check (e.g. `vigiles audit path/foo.md`).
303
+ //
304
+ // Resolve each requested file to its real source spec by reading the
305
+ // compiled-from header. Multi-target projects compile one spec to
306
+ // several targets (e.g. CLAUDE.md.spec.ts → CLAUDE.md + AGENTS.md), so
307
+ // naive `${file}.spec.ts` concatenation would miss the real source for
308
+ // the secondary targets. Fall back to the concatenation rule if the
309
+ // file has no hash header (e.g. freshly hand-written).
310
+ const specs = scopeFiles && scopeFiles.length > 0
311
+ ? (() => {
312
+ const wanted = new Set();
313
+ const compiledFromRe = /<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from (.+?)\s*-->/;
314
+ for (const f of scopeFiles) {
315
+ let resolved;
316
+ try {
317
+ const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), f), "utf-8");
318
+ const m = compiledFromRe.exec(content);
319
+ if (m) {
320
+ resolved = (0, node_path_1.resolve)(process.cwd(), m[1].trim());
321
+ }
322
+ }
323
+ catch {
324
+ // File unreadable — fall through to the naming convention
325
+ }
326
+ if (!resolved) {
327
+ resolved = (0, node_path_1.resolve)(process.cwd(), `${f}.spec.ts`);
328
+ }
329
+ wanted.add(resolved);
330
+ }
331
+ return allSpecs.filter((specPath) => wanted.has((0, node_path_1.resolve)(process.cwd(), specPath)));
332
+ })()
333
+ : allSpecs;
334
+ if (specs.length === 0)
335
+ return { valid: true, pairCount: 0 };
336
+ let totalPairs = 0;
337
+ let specsWithDuplicates = 0;
338
+ for (const specPath of specs) {
339
+ const spec = await loadSpec(specPath);
340
+ if (!spec || spec._specType !== "claude")
341
+ continue;
342
+ const rules = spec.rules;
343
+ const ruleCount = Object.keys(rules).length;
344
+ if (ruleCount < 2)
345
+ 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
+ }
357
+ if (pairs.length === 0)
358
+ continue;
359
+ if (specsWithDuplicates === 0) {
360
+ log(`Found near-duplicate rules (NCD < ${String(threshold)}):\n`);
361
+ }
362
+ specsWithDuplicates++;
363
+ totalPairs += pairs.length;
364
+ log(` ${specPath}`);
365
+ for (const pair of pairs.slice(0, 5)) {
366
+ log(` ${pair.idA} ↔ ${pair.idB} (distance: ${pair.distance.toFixed(3)})`);
367
+ }
368
+ if (pairs.length > 5) {
369
+ log(` ... and ${String(pairs.length - 5)} more`);
370
+ }
371
+ }
372
+ if (totalPairs === 0) {
373
+ log("No near-duplicate rules detected.");
374
+ return { valid: true, pairCount: 0 };
375
+ }
376
+ log(`\n ${String(totalPairs)} duplicate pair(s) in ${String(specsWithDuplicates)} spec(s). Consider merging or rewording.`);
377
+ return { valid: false, pairCount: totalPairs };
378
+ }
379
+ /** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
380
+ function auditExitCode(report) {
381
+ if (report.hashErrors > 0 ||
382
+ report.validationErrors > 0 ||
383
+ report.inlineErrors > 0 ||
384
+ report.freshnessErrors > 0)
385
+ return 2;
386
+ if (report.duplicatePairs > 0)
387
+ return 1;
388
+ // Coverage gaps and guidance counts are informational, not failures
389
+ return 0;
390
+ }
391
+ /**
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).
396
+ */
397
+ function verifyInlineRules(filePath, silent, linterOptions) {
398
+ const log = (msg) => {
399
+ if (!silent)
400
+ console.log(msg);
401
+ };
402
+ let content;
403
+ try {
404
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), filePath), "utf-8");
405
+ }
406
+ catch {
407
+ return { ok: true, errorCount: 0, ruleCount: 0 };
408
+ }
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 };
412
+ }
413
+ let errorCount = 0;
414
+ log(`\n${filePath} (inline mode):`);
415
+ for (const err of parseErrors) {
416
+ log(` ✗ line ${String(err.line)}: ${err.message}`);
417
+ errorCount++;
418
+ if (!silent) {
419
+ ghAnnotate("error", err.message, filePath, err.line);
420
+ }
421
+ }
422
+ 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}`);
427
+ errorCount++;
428
+ if (!silent) {
429
+ ghAnnotate("error", message, filePath, rule.line);
430
+ }
431
+ }
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}`);
435
+ errorCount++;
436
+ if (!silent) {
437
+ ghAnnotate("error", message, filePath, rule.line);
438
+ }
439
+ }
440
+ else {
441
+ log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
442
+ }
443
+ }
444
+ return { ok: errorCount === 0, errorCount, ruleCount: rules.length };
445
+ }
446
+ /**
447
+ * Unified audit command: verify hashes, report coverage gaps, detect duplicates,
448
+ * suggest improvements.
449
+ *
450
+ * Flags:
451
+ * --summary Print a single-line summary (for SessionStart hooks)
452
+ * --json Print structured JSON report (for CI integration)
453
+ */
454
+ async function audit(restArgs, flags, config) {
455
+ const summary = flags.includes("--summary");
456
+ const json = flags.includes("--json");
457
+ const silent = summary || json;
458
+ const files = findInstructionFiles(restArgs);
459
+ // 1. Verify hashes and structure
460
+ if (!silent) {
461
+ if (files.length > 0) {
462
+ console.log("Verifying compiled files...\n");
463
+ }
464
+ else {
465
+ console.log("No compiled instruction files found.\n");
466
+ }
467
+ }
468
+ const hashResult = files.length > 0
469
+ ? check(files, silent)
470
+ : { 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
+ }
510
+ // 2. Coverage gaps (discover)
511
+ if (!silent)
512
+ console.log("\nLinter rule coverage:\n");
513
+ const coverage = discover(silent);
514
+ // 3. Duplicate rule detection (NCD). Scope to the requested files when
515
+ // audit was invoked with explicit paths, so targeted CI checks don't
516
+ // fail on unrelated duplicates elsewhere in the repo.
517
+ if (!silent)
518
+ console.log("\nDuplicate rule detection:\n");
519
+ const dups = await findDuplicateRules(0.3, silent, restArgs.length > 0 ? files : undefined);
520
+ // 4. Guidance rule count (strengthen suggestions moved to /strengthen skill)
521
+ const guidanceCount = await countGuidanceRules(silent);
522
+ // 5. Freshness check
523
+ const freshnessSeverity = config?.rules.freshness;
524
+ let freshnessErrors = 0;
525
+ if (freshnessSeverity) {
526
+ if (!silent)
527
+ console.log("\nFreshness check:\n");
528
+ const mode = config?.freshnessMode ?? "strict";
529
+ freshnessErrors = await checkFreshness(files, mode, config, freshnessSeverity, silent);
530
+ }
531
+ const report = {
532
+ hashErrors: hashResult.hashErrors,
533
+ validationErrors: hashResult.validationErrors,
534
+ inlineErrors,
535
+ inlineRules,
536
+ duplicatePairs: dups.pairCount,
537
+ coverageEnabled: coverage.enabled,
538
+ coverageDocumented: coverage.documented,
539
+ strengthenSuggestions: guidanceCount,
540
+ freshnessErrors,
541
+ files,
542
+ };
543
+ if (summary) {
544
+ printAuditSummary(report);
545
+ }
546
+ else if (json) {
547
+ console.log(JSON.stringify(report, null, 2));
548
+ }
549
+ return report;
550
+ }
551
+ /** Single-line audit summary for SessionStart hooks — minimal token cost. */
552
+ function printAuditSummary(report) {
553
+ const parts = [];
554
+ if (report.hashErrors > 0)
555
+ parts.push(`${String(report.hashErrors)} stale`);
556
+ if (report.validationErrors > 0)
557
+ parts.push(`${String(report.validationErrors)} validation errors`);
558
+ if (report.inlineErrors > 0)
559
+ parts.push(`${String(report.inlineErrors)} inline errors`);
560
+ if (report.duplicatePairs > 0)
561
+ parts.push(`${String(report.duplicatePairs)} duplicates`);
562
+ const undocumented = report.coverageEnabled - report.coverageDocumented;
563
+ if (undocumented > 0)
564
+ parts.push(`${String(undocumented)} undocumented rules`);
565
+ if (report.strengthenSuggestions > 0)
566
+ 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)`);
569
+ if (parts.length === 0) {
570
+ console.log("vigiles: clean");
571
+ }
572
+ else {
573
+ console.log(`vigiles: ${parts.join(" / ")}`);
574
+ }
575
+ }
576
+ function collectDocumentedRules() {
577
+ const documented = new Set();
578
+ const mdFiles = (0, glob_1.globSync)("**/CLAUDE.md", {
579
+ ignore: IGNORE_NODE_MODULES,
580
+ cwd: process.cwd(),
581
+ });
582
+ for (const mdFile of mdFiles) {
583
+ const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), mdFile), "utf-8");
584
+ const enforcedRe = /\*\*Enforced by:\*\*\s*`([^`]+)`/g;
585
+ let m;
586
+ while ((m = enforcedRe.exec(content)) !== null) {
587
+ documented.add(m[1]);
588
+ }
589
+ }
590
+ return documented;
591
+ }
592
+ function printLinterCoverage(linter, documentedRules, silent = false) {
593
+ const log = (msg) => {
594
+ if (!silent)
595
+ console.log(msg);
596
+ };
597
+ const documented = linter.rules.filter((r) => documentedRules.has(`${linter.linter}/${r}`));
598
+ const undocumented = linter.rules.filter((r) => !documentedRules.has(`${linter.linter}/${r}`));
599
+ const pct = linter.rules.length > 0
600
+ ? Math.round((documented.length / linter.rules.length) * 100)
601
+ : 0;
602
+ log(` ${linter.linter}: ${String(documented.length)}/${String(linter.rules.length)} rules documented (${String(pct)}%)`);
603
+ if (documented.length > 0 && documented.length <= 10) {
604
+ for (const r of documented) {
605
+ log(` ✓ ${linter.linter}/${r}`);
606
+ }
607
+ }
608
+ if (undocumented.length > 0) {
609
+ const show = undocumented.slice(0, 5);
610
+ log(` Top undocumented:`);
611
+ for (const r of show) {
612
+ log(` ✗ ${linter.linter}/${r}`);
613
+ }
614
+ if (undocumented.length > 5) {
615
+ log(` ... and ${String(undocumented.length - 5)} more`);
616
+ }
617
+ }
618
+ log("");
619
+ return { enabled: linter.rules.length, documented: documented.length };
620
+ }
621
+ function discover(silent = false) {
622
+ const log = (msg) => {
623
+ if (!silent)
624
+ console.log(msg);
625
+ };
626
+ log("Scanning project for linter rules...\n");
627
+ const result = (0, generate_types_js_1.generateTypes)({ basePath: process.cwd() });
628
+ const documentedRules = collectDocumentedRules();
629
+ log("Detected linters:\n");
630
+ let totalEnabled = 0;
631
+ let totalDocumented = 0;
632
+ for (const linter of result.linters) {
633
+ const totals = printLinterCoverage(linter, documentedRules, silent);
634
+ totalEnabled += totals.enabled;
635
+ totalDocumented += totals.documented;
636
+ }
637
+ if (result.linters.length === 0) {
638
+ log(" No linters detected.\n");
639
+ }
640
+ const totalPct = totalEnabled > 0 ? Math.round((totalDocumented / totalEnabled) * 100) : 0;
641
+ log(`Coverage: ${String(totalDocumented)}/${String(totalEnabled)} rules documented (${String(totalPct)}%)`);
642
+ if (totalDocumented < totalEnabled) {
643
+ log(`\nConsider adding enforce() rules for frequently-triggered undocumented rules.`);
644
+ log(`The agent encounters these rules but has no context about WHY.`);
645
+ }
646
+ return { enabled: totalEnabled, documented: totalDocumented };
647
+ }
648
+ function init(args) {
649
+ const targetFlag = args.find((a) => a.startsWith("--target="));
650
+ const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
651
+ const specPath = `${target}.spec.ts`;
652
+ if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), specPath))) {
653
+ console.log(`${specPath} already exists.`);
654
+ return;
655
+ }
656
+ const targetLine = target !== "CLAUDE.md" ? `\n target: "${target}",` : "";
657
+ const template = `import { claude, enforce, guidance } from "vigiles/spec";
658
+
659
+ export default claude({${targetLine}
660
+ sections: {
661
+ // Prose sections become ## headings in the compiled output.
662
+ // Do not add # or ## headers inside sections.
663
+ // positioning: "What this project does and why.",
664
+
665
+ // This section is included in the compiled output to help agents
666
+ // understand how to work with specs. Remove it once your team is familiar.
667
+ "how-to-edit": "This file is compiled from a .spec.ts file. Do not edit it directly — edit the spec and run 'npx vigiles compile'. To add a rule: add to the rules object in the spec.",
668
+ },
669
+
670
+ commands: {
671
+ // Commands are verified against package.json at compile time.
672
+ // "npm run build": "Compile the project",
673
+ // "npm test": "Run all tests",
674
+ },
675
+
676
+ keyFiles: {
677
+ // File paths are verified to exist at compile time.
678
+ // "src/index.ts": "Main entry point",
679
+ },
680
+
681
+ rules: {
682
+ // enforce() — backed by a linter rule, verified to exist AND be enabled:
683
+ // "no-console": enforce("eslint/no-console", "Use structured logger."),
684
+ //
685
+ // guidance() — prose only, no enforcement:
686
+ // "research-first": guidance("Google unfamiliar APIs before implementing."),
687
+ },
688
+ });
689
+ `;
690
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), specPath), template);
691
+ console.log(`Created ${specPath} — edit it and run \`vigiles compile\`.`);
692
+ }
693
+ // ---------------------------------------------------------------------------
694
+ // Setup wizard
695
+ // ---------------------------------------------------------------------------
696
+ const VIGILES_CI_STEP = ` - name: Verify specs
697
+ run: npx vigiles audit && npx vigiles generate-types --check`;
698
+ function addGhaStep() {
699
+ // Find existing GHA workflow
700
+ const ciPaths = [
701
+ ".github/workflows/ci.yml",
702
+ ".github/workflows/ci.yaml",
703
+ ".github/workflows/main.yml",
704
+ ".github/workflows/main.yaml",
705
+ ".github/workflows/test.yml",
706
+ ".github/workflows/test.yaml",
707
+ ];
708
+ for (const ciPath of ciPaths) {
709
+ const fullPath = (0, node_path_1.resolve)(process.cwd(), ciPath);
710
+ if ((0, node_fs_1.existsSync)(fullPath)) {
711
+ const content = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
712
+ if (content.includes("vigiles")) {
713
+ console.log(`✓ ${ciPath} already has vigiles steps`);
714
+ return true;
715
+ }
716
+ // Append step at end of file (safe for all YAML formats)
717
+ const trimmed = content.trimEnd();
718
+ (0, node_fs_1.writeFileSync)(fullPath, trimmed + "\n\n" + VIGILES_CI_STEP + "\n");
719
+ console.log(`✓ Added vigiles check step to ${ciPath}`);
720
+ return true;
721
+ }
722
+ }
723
+ return false;
724
+ }
725
+ const KNOWN_INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
726
+ const KNOWN_OTHER_CONFIGS = {
727
+ ".cursorrules": "Cursor",
728
+ ".github/copilot-instructions.md": "GitHub Copilot",
729
+ ".windsurfrules": "Windsurf",
730
+ };
731
+ const KNOWN_SYNC_TOOLS = [
732
+ "rule-porter",
733
+ "rulesync",
734
+ "vibe-cli",
735
+ "@nichochar/rule-porter",
736
+ ];
737
+ function detectProject() {
738
+ const cwd = process.cwd();
739
+ const instructionFiles = [];
740
+ const agents = [];
741
+ const otherConfigs = [];
742
+ // Check known instruction files
743
+ for (const f of KNOWN_INSTRUCTION_FILES) {
744
+ const full = (0, node_path_1.resolve)(cwd, f);
745
+ if ((0, node_fs_1.existsSync)(full)) {
746
+ let isSymlink = false;
747
+ try {
748
+ isSymlink = (0, node_fs_1.lstatSync)(full).isSymbolicLink();
749
+ }
750
+ catch {
751
+ // ignore
752
+ }
753
+ const hasSpec = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, `${f}.spec.ts`));
754
+ instructionFiles.push({ path: f, hasSpec, isSymlink });
755
+ }
756
+ }
757
+ // Detect agents from files
758
+ if (instructionFiles.some((f) => f.path === "CLAUDE.md") ||
759
+ (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, ".claude"))) {
760
+ agents.push("Claude Code");
761
+ }
762
+ if (instructionFiles.some((f) => f.path === "AGENTS.md")) {
763
+ agents.push("Codex / GitHub Copilot");
764
+ }
765
+ // Check non-markdown configs
766
+ for (const [path, agent] of Object.entries(KNOWN_OTHER_CONFIGS)) {
767
+ if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, path))) {
768
+ otherConfigs.push(`${path} (${agent})`);
769
+ if (!agents.includes(agent))
770
+ agents.push(agent);
771
+ }
772
+ }
773
+ // Check for sync tools in package.json
774
+ const syncTools = [];
775
+ const pkgPath = (0, node_path_1.resolve)(cwd, "package.json");
776
+ if ((0, node_fs_1.existsSync)(pkgPath)) {
777
+ try {
778
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, "utf-8"));
779
+ const allDeps = {
780
+ ...pkg.dependencies,
781
+ ...pkg.devDependencies,
782
+ };
783
+ for (const tool of KNOWN_SYNC_TOOLS) {
784
+ if (tool in allDeps)
785
+ syncTools.push(tool);
786
+ }
787
+ }
788
+ catch {
789
+ // ignore
790
+ }
791
+ }
792
+ return {
793
+ instructionFiles,
794
+ agents,
795
+ syncTools,
796
+ otherConfigs,
797
+ hasClaude: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, ".claude")),
798
+ };
799
+ }
800
+ async function setup(args) {
801
+ const targetFlag = args.find((a) => a.startsWith("--target="));
802
+ const strict = args.includes("--strict");
803
+ const noGha = args.includes("--no-gha");
804
+ console.log(`vigiles setup${strict ? " (strict mode)" : ""}\n`);
805
+ // Step 1: Detect project
806
+ const detected = detectProject();
807
+ if (detected.agents.length > 0) {
808
+ console.log(`Detected: ${detected.agents.join(", ")}`);
809
+ }
810
+ if (detected.otherConfigs.length > 0) {
811
+ console.log(`Other agent configs: ${detected.otherConfigs.join(", ")}`);
812
+ }
813
+ if (detected.syncTools.length > 0) {
814
+ console.log(`Sync tools: ${detected.syncTools.join(", ")}`);
815
+ }
816
+ for (const f of detected.instructionFiles) {
817
+ if (f.isSymlink) {
818
+ console.log(`Note: ${f.path} is a symlink`);
819
+ }
820
+ }
821
+ if (detected.agents.length > 0 ||
822
+ detected.otherConfigs.length > 0 ||
823
+ detected.syncTools.length > 0) {
824
+ console.log("");
825
+ }
826
+ // Step 2: Determine targets
827
+ let targets;
828
+ if (targetFlag) {
829
+ targets = [targetFlag.split("=")[1]];
830
+ }
831
+ else {
832
+ // Auto-detect: create specs for instruction files that need them
833
+ const needsSpec = detected.instructionFiles.filter((f) => !f.hasSpec);
834
+ if (needsSpec.length > 0) {
835
+ // Existing files without specs — suggest migration
836
+ for (const f of needsSpec) {
837
+ console.log(`Found ${f.path} without a spec. Migrate with the migrate-to-spec skill`);
838
+ console.log(` or create a blank spec: npx vigiles init --target=${f.path}\n`);
839
+ }
840
+ const hasAnySpec = detected.instructionFiles.some((f) => f.hasSpec);
841
+ if (!hasAnySpec &&
842
+ needsSpec.length === detected.instructionFiles.length) {
843
+ // ALL existing files need migration — don't create new ones
844
+ console.log("Install the plugin to use the migration skill:");
845
+ console.log(" npx skills add zernie/vigiles");
846
+ return;
847
+ }
848
+ }
849
+ // Default: CLAUDE.md, plus AGENTS.md if Codex detected
850
+ targets = ["CLAUDE.md"];
851
+ const hasAgentsMd = detected.instructionFiles.some((f) => f.path === "AGENTS.md");
852
+ const hasCodex = detected.agents.includes("Codex / GitHub Copilot") || hasAgentsMd;
853
+ if (hasCodex && !hasAgentsMd) {
854
+ targets.push("AGENTS.md");
855
+ }
856
+ }
857
+ // Step 3: Create specs
858
+ for (const target of targets) {
859
+ const specPath = `${target}.spec.ts`;
860
+ if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), specPath))) {
861
+ console.log(`✓ ${specPath} already exists`);
862
+ }
863
+ else if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), target))) {
864
+ console.log(`⚠ ${target} exists without spec — migrate with migrate-to-spec skill`);
865
+ }
866
+ else {
867
+ init(["--target=" + target]);
868
+ }
869
+ }
870
+ // Step 4: Generate types
871
+ console.log("\nScanning linters and project files...");
872
+ const typesResult = (0, generate_types_js_1.generateTypes)({ basePath: process.cwd() });
873
+ const outPath = ".vigiles/generated.d.ts";
874
+ const outDir = (0, node_path_1.resolve)(process.cwd(), ".vigiles");
875
+ if (!(0, node_fs_1.existsSync)(outDir)) {
876
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
877
+ }
878
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outPath), typesResult.dts);
879
+ for (const l of typesResult.linters) {
880
+ console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
881
+ }
882
+ if (typesResult.scripts.length > 0) {
883
+ console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
884
+ }
885
+ console.log(`✓ Generated ${outPath}`);
886
+ // Step 5: Compile specs
887
+ console.log("\nCompiling specs...");
888
+ const specs = findSpecs();
889
+ if (specs.length > 0) {
890
+ await compile(specs, (0, validate_js_1.loadConfig)());
891
+ }
892
+ // Step 6: Add CI step
893
+ console.log("");
894
+ const addedGha = noGha ? false : addGhaStep();
895
+ if (!addedGha) {
896
+ console.log(" No CI workflow found. Add this step to your CI:\n");
897
+ console.log(" npx vigiles audit && npx vigiles generate-types --check");
898
+ }
899
+ // Step 7: Install Claude Code plugin (hooks + skills)
900
+ const shouldInstallPlugin = detected.hasClaude || targets.includes("CLAUDE.md");
901
+ if (shouldInstallPlugin) {
902
+ let pluginInstalled = false;
903
+ // Try installing the full plugin via skills CLI (gives hooks + skills)
904
+ try {
905
+ const { execSync: exec } = require("node:child_process");
906
+ exec("npx skills add zernie/vigiles", {
907
+ cwd: process.cwd(),
908
+ stdio: ["pipe", "pipe", "pipe"],
909
+ timeout: 30000,
910
+ });
911
+ pluginInstalled = true;
912
+ console.log("✓ Installed vigiles plugin (hooks + skills) via skills CLI");
913
+ }
914
+ catch {
915
+ // skills CLI not available — fall back to direct hook installation
916
+ }
917
+ if (!pluginInstalled) {
918
+ // Fall back: write hooks directly to .claude/settings.json
919
+ // (gives auto-compile + block edits, but no skills)
920
+ const settingsDir = (0, node_path_1.resolve)(process.cwd(), ".claude");
921
+ const settingsPath = (0, node_path_1.resolve)(settingsDir, "settings.json");
922
+ if (!(0, node_fs_1.existsSync)(settingsDir)) {
923
+ (0, node_fs_1.mkdirSync)(settingsDir, { recursive: true });
924
+ }
925
+ let settings = {};
926
+ if ((0, node_fs_1.existsSync)(settingsPath)) {
927
+ try {
928
+ settings = JSON.parse((0, node_fs_1.readFileSync)(settingsPath, "utf-8"));
929
+ }
930
+ catch {
931
+ // Ignore malformed settings
932
+ }
933
+ }
934
+ if (!settings["hooks"]) {
935
+ settings["hooks"] = {};
936
+ }
937
+ const hooks = settings["hooks"];
938
+ 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`;
939
+ 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`;
940
+ // Append to existing arrays (don't duplicate if already present)
941
+ const existingStr = JSON.stringify(settings);
942
+ if (!existingStr.includes("vigiles:sha256")) {
943
+ const pre = (hooks["PreToolUse"] ?? []);
944
+ pre.push({ matcher: "Edit|Write", command: preCmd });
945
+ hooks["PreToolUse"] = pre;
946
+ }
947
+ if (!existingStr.includes("vigiles compile")) {
948
+ const post = (hooks["PostToolUse"] ?? []);
949
+ post.push({ matcher: "Edit|Write", command: postCmd });
950
+ hooks["PostToolUse"] = post;
951
+ }
952
+ (0, node_fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
953
+ console.log("✓ Installed hooks in .claude/settings.json");
954
+ console.log(" (For skills like edit-spec and migrate-to-spec, also run: npx skills add zernie/vigiles)");
955
+ }
956
+ }
957
+ // Step 8: Agent-specific guidance
958
+ const specPathsList = targets.map((t) => `${t}.spec.ts`);
959
+ const specPaths = specPathsList.join(", ");
960
+ if (targets.includes("AGENTS.md")) {
961
+ console.log("\n Codex / Copilot reads AGENTS.md directly — no hooks needed.");
962
+ console.log(" Run `npx vigiles compile` after spec edits. CI enforces freshness.");
963
+ }
964
+ if (detected.otherConfigs.length > 0 && detected.syncTools.length === 0) {
965
+ console.log("\n Non-markdown agent configs detected. Use a sync tool to convert:");
966
+ console.log(" npm install -D rule-porter");
967
+ }
968
+ // Step 8b: Write config if strict mode
969
+ if (strict) {
970
+ const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
971
+ if (!(0, node_fs_1.existsSync)(configPath)) {
972
+ (0, node_fs_1.writeFileSync)(configPath, JSON.stringify({
973
+ rules: {
974
+ "require-spec": "error",
975
+ "require-skill-spec": "error",
976
+ },
977
+ }, null, 2) + "\n");
978
+ console.log("✓ Created .vigilesrc.json with strict rules");
979
+ }
980
+ }
981
+ // Step 9: Summary
982
+ console.log("\n---");
983
+ console.log("Setup complete.\n");
984
+ console.log(` 1. Edit ${specPaths} — add your project's conventions`);
985
+ console.log(" 2. Run `/strengthen` in Claude Code to upgrade guidance → enforce");
986
+ if (!strict) {
987
+ console.log(" 3. When ready, enforce specs in CI: npx vigiles setup --strict");
988
+ }
989
+ console.log("\n Commit:");
990
+ const files = [
991
+ ...targets,
992
+ ...specPathsList,
993
+ ".vigiles/generated.d.ts",
994
+ ...(shouldInstallPlugin ? [".claude/settings.json"] : []),
995
+ ...(strict ? [".vigilesrc.json"] : []),
996
+ ];
997
+ console.log(` git add ${files.join(" ")} && git commit -m "Add vigiles spec"`);
998
+ }
999
+ // ---------------------------------------------------------------------------
1000
+ // Strengthen: guidance() → enforce() suggestions
1001
+ // ---------------------------------------------------------------------------
1002
+ async function checkFreshness(files, mode, config, severity, silent) {
1003
+ const log = (msg) => {
1004
+ if (!silent)
1005
+ console.log(msg);
1006
+ };
1007
+ let errorCount = 0;
1008
+ const basePath = process.cwd();
1009
+ for (const filePath of files) {
1010
+ const abs = (0, node_path_1.resolve)(basePath, filePath);
1011
+ if (!(0, node_fs_1.existsSync)(abs))
1012
+ 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) {
1068
+ errorCount++;
1069
+ 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
+ }
1076
+ }
1077
+ else if (!silent) {
1078
+ log(` ✓ ${filePath} — fresh (${mode})`);
1079
+ }
1080
+ }
1081
+ if (errorCount === 0) {
1082
+ log(" All files fresh.");
1083
+ }
1084
+ return severity === "error" ? errorCount : 0;
1085
+ }
1086
+ async function countGuidanceRules(silent = false) {
1087
+ const specs = findSpecs();
1088
+ if (specs.length === 0)
1089
+ return 0;
1090
+ let count = 0;
1091
+ for (const specPath of specs) {
1092
+ const spec = await loadSpec(specPath);
1093
+ if (!spec || spec._specType !== "claude")
1094
+ continue;
1095
+ for (const rule of Object.values(spec.rules)) {
1096
+ if (rule._kind === "guidance")
1097
+ count++;
1098
+ }
1099
+ }
1100
+ if (!silent && count > 0) {
1101
+ console.log(`${String(count)} guidance rule(s) — run /strengthen to find enforce() upgrades\n`);
1102
+ }
1103
+ return count;
1104
+ }
1105
+ // ---------------------------------------------------------------------------
1106
+ // Command handlers for main()
1107
+ // ---------------------------------------------------------------------------
1108
+ function findInstructionFiles(restArgs) {
1109
+ if (restArgs.length > 0)
1110
+ return restArgs;
1111
+ const patterns = ["**/CLAUDE.md", "**/AGENTS.md", "**/SKILL.md"];
1112
+ const files = [];
1113
+ for (const pattern of patterns) {
1114
+ files.push(...(0, glob_1.globSync)(pattern, { ignore: IGNORE_NODE_MODULES, cwd: process.cwd() }));
1115
+ }
1116
+ return files;
1117
+ }
1118
+ function handleGenerateTypes(args, restArgs) {
1119
+ const checkOnly = args.includes("--check");
1120
+ const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
1121
+ const fileGlobs = args
1122
+ .filter((a) => a.startsWith("--files="))
1123
+ .map((a) => a.split("=")[1])
1124
+ .filter(Boolean);
1125
+ console.log("Scanning project...\n");
1126
+ const result = (0, generate_types_js_1.generateTypes)({
1127
+ basePath: process.cwd(),
1128
+ fileGlobs: fileGlobs.length > 0 ? fileGlobs : undefined,
1129
+ });
1130
+ for (const l of result.linters) {
1131
+ console.log(` ${l.linter}: ${String(l.rules.length)} enabled rules (via ${l.via})`);
1132
+ }
1133
+ if (result.scripts.length > 0) {
1134
+ console.log(` npm scripts: ${String(result.scripts.length)}`);
1135
+ }
1136
+ console.log(` project files: ${String(result.files.length)}`);
1137
+ const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
1138
+ if (checkOnly) {
1139
+ // --check: compare against existing file, exit 1 if stale
1140
+ if (!(0, node_fs_1.existsSync)(fullOut)) {
1141
+ console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate-types\` to create it.`);
1142
+ process.exit(1);
1143
+ }
1144
+ const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
1145
+ // Normalize for formatter differences (trailing whitespace, blank lines)
1146
+ const normalize = (s) => s
1147
+ .split("\n")
1148
+ .map((l) => l.trimEnd())
1149
+ .join("\n")
1150
+ .replace(/\n{3,}/g, "\n\n")
1151
+ .trim();
1152
+ if (normalize(existing) === normalize(result.dts)) {
1153
+ console.log(`\n✓ ${outPath} is up to date`);
1154
+ }
1155
+ else {
1156
+ console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate-types\` to update.`);
1157
+ process.exit(1);
1158
+ }
1159
+ return;
1160
+ }
1161
+ const outDir = fullOut.substring(0, fullOut.lastIndexOf("/"));
1162
+ if (!(0, node_fs_1.existsSync)(outDir)) {
1163
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
1164
+ }
1165
+ (0, node_fs_1.writeFileSync)(fullOut, result.dts);
1166
+ console.log(`\n✓ Generated ${outPath}`);
1167
+ }
1168
+ function printUsage(command) {
1169
+ console.log("vigiles — compile typed specs to instruction files");
1170
+ console.log("");
1171
+ console.log("Commands:");
1172
+ console.log(" vigiles init [flags] Setup project (--target=X.md, --strict, --no-gha)");
1173
+ console.log(" vigiles compile [files...] Compile .spec.ts → .md");
1174
+ console.log(" vigiles audit [files...] Verify, find gaps, suggest improvements");
1175
+ console.log("");
1176
+ console.log("Examples:");
1177
+ console.log(" vigiles init Auto-detect project, create specs, wire CI");
1178
+ console.log(" vigiles compile Compile all .spec.ts files");
1179
+ console.log(" vigiles audit Verify hashes + coverage + suggestions");
1180
+ console.log("");
1181
+ console.log("Plumbing:");
1182
+ console.log(" vigiles generate-types [out] Emit .d.ts from project state");
1183
+ console.log(" vigiles generate-types --check Verify .d.ts is up to date");
1184
+ if (command && command !== "--help") {
1185
+ console.log(`\nUnknown command: "${command}"`);
1186
+ process.exit(1);
1187
+ }
1188
+ }
1189
+ // ---------------------------------------------------------------------------
1190
+ // Main
1191
+ // ---------------------------------------------------------------------------
1192
+ async function main() {
1193
+ const args = process.argv.slice(2);
1194
+ const command = args[0];
1195
+ const restArgs = args.slice(1).filter((a) => !a.startsWith("--"));
1196
+ const config = (0, validate_js_1.loadConfig)();
1197
+ switch (command) {
1198
+ // --- Primary commands ---
1199
+ case "init": {
1200
+ // Explicit --target bypasses the setup wizard and always creates a
1201
+ // bare spec, so `npx vigiles init --target=<file>` is a reliable
1202
+ // remediation for the require-spec validator. Bare `vigiles init`
1203
+ // still runs the full wizard (project detection + auto-targets).
1204
+ const hasTarget = args.some((a) => a.startsWith("--target="));
1205
+ if (hasTarget) {
1206
+ init(args.slice(1));
1207
+ }
1208
+ else {
1209
+ await setup(args);
1210
+ }
1211
+ break;
1212
+ }
1213
+ case "compile": {
1214
+ const specs = restArgs.length > 0 ? restArgs : findSpecs();
1215
+ if (specs.length === 0) {
1216
+ console.log("No .spec.ts files found.");
1217
+ console.log("Run `vigiles init` to create one.");
1218
+ process.exit(0);
1219
+ }
1220
+ const valid = await compile(specs, config);
1221
+ console.log("");
1222
+ if (valid) {
1223
+ console.log("Compilation complete.");
1224
+ }
1225
+ else {
1226
+ console.log("Compilation complete with errors.");
1227
+ process.exit(1);
1228
+ }
1229
+ break;
1230
+ }
1231
+ case "audit": {
1232
+ // audit = verify + discover + guidance count
1233
+ const flags = args.slice(1).filter((a) => a.startsWith("--"));
1234
+ const report = await audit(restArgs, flags, config);
1235
+ 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
+ if (exitCode !== 0) {
1253
+ process.exit(exitCode);
1254
+ }
1255
+ break;
1256
+ }
1257
+ // --- Plumbing ---
1258
+ case "generate-types":
1259
+ handleGenerateTypes(args, restArgs);
1260
+ break;
1261
+ default:
1262
+ printUsage(command);
1263
+ break;
1264
+ }
1265
+ }
1266
+ void main();
1267
+ //# sourceMappingURL=cli.js.map