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/src/cli.ts ADDED
@@ -0,0 +1,1631 @@
1
+ #!/usr/bin/env node
2
+
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
+
13
+ import {
14
+ writeFileSync,
15
+ existsSync,
16
+ mkdirSync,
17
+ readFileSync,
18
+ lstatSync,
19
+ } from "node:fs";
20
+ import { resolve } from "node:path";
21
+ import { globSync } from "glob";
22
+ import { generateTypes } from "./generate-types.js";
23
+ import { validate, loadConfig } from "./validate.js";
24
+ import type { VigilesConfig } from "./types.js";
25
+
26
+ import {
27
+ compileClaude,
28
+ compileSkill,
29
+ checkFileHash,
30
+ addHash,
31
+ } from "./compile.js";
32
+ import type { CompileError } from "./compile.js";
33
+ import type { ClaudeSpec, SkillSpec } from "./spec.js";
34
+ import { findSimilarRules } from "./proofs.js";
35
+ import { parseInlineRules } from "./inline.js";
36
+ import { checkLinterRule } from "./linters.js";
37
+ import {
38
+ discoverInputs,
39
+ computeInputHash,
40
+ addInputHash,
41
+ checkOutputHashFreshness,
42
+ checkInputHashFreshness,
43
+ } from "./freshness.js";
44
+ import type { FreshnessResult } from "./freshness.js";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Constants
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const IGNORE_NODE_MODULES = ["node_modules/**"];
51
+
52
+ // Config is loaded from .vigilesrc.json via validate.ts::loadConfig().
53
+ // All settings (validation, compilation, freshness) are in one place.
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Spec loading
57
+ // ---------------------------------------------------------------------------
58
+
59
+ function findSpecs(pattern?: string): string[] {
60
+ const glob = pattern ?? "**/*.md.spec.ts";
61
+ return globSync(glob, {
62
+ ignore: [...IGNORE_NODE_MODULES, "dist/**"],
63
+ cwd: process.cwd(),
64
+ });
65
+ }
66
+
67
+ async function loadSpec(
68
+ specPath: string,
69
+ ): Promise<ClaudeSpec | SkillSpec | null> {
70
+ const fullPath = resolve(process.cwd(), specPath);
71
+
72
+ // Try multiple dist/ path strategies
73
+ const candidates: string[] = [];
74
+
75
+ // src/ → dist/ mapping (e.g., src/CLAUDE.md.spec.ts → dist/CLAUDE.md.spec.js)
76
+ if (fullPath.includes("/src/")) {
77
+ candidates.push(
78
+ fullPath.replace(/\/src\//, "/dist/").replace(/\.ts$/, ".js"),
79
+ );
80
+ }
81
+
82
+ // Root-level spec → dist/ (e.g., CLAUDE.md.spec.ts → dist/CLAUDE.md.spec.js)
83
+ const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
84
+ const base = fullPath.substring(fullPath.lastIndexOf("/") + 1);
85
+ candidates.push(resolve(dir, "dist", base.replace(/\.ts$/, ".js")));
86
+
87
+ // examples/ → dist/examples/ mapping
88
+ candidates.push(
89
+ fullPath
90
+ .replace(/\.ts$/, ".js")
91
+ .replace(process.cwd(), resolve(process.cwd(), "dist")),
92
+ );
93
+
94
+ for (const distPath of candidates) {
95
+ if (existsSync(distPath)) {
96
+ try {
97
+ const mod = (await import(distPath)) as {
98
+ default: ClaudeSpec | SkillSpec | { default: ClaudeSpec | SkillSpec };
99
+ };
100
+ // CJS double-default: `{ default: { default: spec } }`.
101
+ const raw = mod.default;
102
+ if (raw && typeof raw === "object" && "default" in raw) {
103
+ return (raw as { default: ClaudeSpec | SkillSpec }).default;
104
+ }
105
+ return raw;
106
+ } catch {
107
+ // Try next candidate
108
+ }
109
+ }
110
+ }
111
+
112
+ // Try loading .ts directly via tsx
113
+ try {
114
+ const { execSync } =
115
+ require("node:child_process") as typeof import("node:child_process");
116
+ // Handle ESM/CJS double-default: m.default may itself have a .default
117
+ const script = `import(${JSON.stringify(fullPath)}).then(m => { const d = m.default?.default ?? m.default; console.log(JSON.stringify(d)); })`;
118
+ const output = execSync(`npx tsx -e '${script.replace(/'/g, "'\\''")}'`, {
119
+ encoding: "utf-8",
120
+ cwd: process.cwd(),
121
+ stdio: ["pipe", "pipe", "pipe"],
122
+ timeout: 15000,
123
+ });
124
+ return JSON.parse(output.trim()) as ClaudeSpec | SkillSpec;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Output helpers
132
+ // ---------------------------------------------------------------------------
133
+
134
+ function printErrors(specFile: string, errors: CompileError[]): void {
135
+ for (const err of errors) {
136
+ const pathInfo = err.path ? ` (${err.path})` : "";
137
+ console.log(` [${err.type}] ${err.message}${pathInfo}`);
138
+ console.log(`::error file=${specFile}::${err.message}`);
139
+ }
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Commands
144
+ // ---------------------------------------------------------------------------
145
+
146
+ async function compile(
147
+ specPaths: string[],
148
+ config: VigilesConfig,
149
+ ): Promise<boolean> {
150
+ let allValid = true;
151
+
152
+ for (const specPath of specPaths) {
153
+ const spec = await loadSpec(specPath);
154
+ if (!spec) {
155
+ console.log(`\n✗ ${specPath} — failed to load`);
156
+ console.log(
157
+ ` Ensure the spec is compiled: run \`npm run build\` first.`,
158
+ );
159
+ allValid = false;
160
+ continue;
161
+ }
162
+
163
+ const basePath = process.cwd();
164
+
165
+ if (spec._specType === "claude") {
166
+ const {
167
+ markdown: rawMarkdown,
168
+ errors,
169
+ linterResults,
170
+ targets,
171
+ } = compileClaude(spec, {
172
+ basePath,
173
+ specFile: specPath,
174
+ maxRules: config.maxRules,
175
+ maxTokens: config.maxTokens,
176
+ maxSectionLines: config.maxSectionLines,
177
+ catalogOnly: config.catalogOnly,
178
+ linters: config.linters,
179
+ });
180
+
181
+ // Embed input hash for freshness tracking
182
+ let markdown = rawMarkdown;
183
+ if (config.freshnessMode === "input-hash") {
184
+ const inputs = discoverInputs(
185
+ specPath,
186
+ spec,
187
+ basePath,
188
+ config.freshnessInputs,
189
+ );
190
+ const inputHash = computeInputHash(inputs.files, basePath);
191
+ markdown = addInputHash(rawMarkdown, inputHash);
192
+ }
193
+
194
+ const linterCount = linterResults.filter((r) => r.exists).length;
195
+ const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
196
+
197
+ if (errors.length === 0) {
198
+ // Write primary target
199
+ writeFileSync(resolve(basePath, primaryOutput), markdown);
200
+ const outputNames = [primaryOutput];
201
+
202
+ // Write additional targets with swapped heading + recomputed hash
203
+ for (const t of targets.slice(1)) {
204
+ // Strip hash, replace heading, recompute hash
205
+ const body = markdown
206
+ .replace(/^<!-- vigiles:[^\n]+\n\n?/, "")
207
+ .replace(/^# [^\n]+/, `# ${t}`);
208
+ const additional = addHash(body, specPath);
209
+ const dir = primaryOutput.substring(
210
+ 0,
211
+ primaryOutput.lastIndexOf("/") + 1,
212
+ );
213
+ const targetPath = dir + t;
214
+ writeFileSync(resolve(basePath, targetPath), additional);
215
+ outputNames.push(targetPath);
216
+ }
217
+
218
+ console.log(`\n✓ ${specPath} → ${outputNames.join(", ")}`);
219
+ console.log(
220
+ ` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`,
221
+ );
222
+ } else {
223
+ console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
224
+ printErrors(specPath, errors);
225
+ allValid = false;
226
+ // Still write the file so the user can see partial output
227
+ writeFileSync(resolve(basePath, primaryOutput), markdown);
228
+ }
229
+ } else if (spec._specType === "skill") {
230
+ const outputPath = specPath.replace(/\.spec\.ts$/, "");
231
+ const { markdown, errors } = compileSkill(spec, {
232
+ basePath,
233
+ specFile: specPath,
234
+ });
235
+
236
+ if (errors.length === 0) {
237
+ writeFileSync(resolve(basePath, outputPath), markdown);
238
+ console.log(`\n✓ ${specPath} → ${outputPath}`);
239
+ } else {
240
+ console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
241
+ printErrors(specPath, errors);
242
+ allValid = false;
243
+ writeFileSync(resolve(basePath, outputPath), markdown);
244
+ }
245
+ }
246
+ }
247
+
248
+ return allValid;
249
+ }
250
+
251
+ /** True when running inside a GitHub Actions workflow. */
252
+ function isGitHubActions(): boolean {
253
+ return process.env.GITHUB_ACTIONS === "true";
254
+ }
255
+
256
+ /**
257
+ * Emit a GitHub Actions annotation for the inline PR experience.
258
+ * No-op outside GitHub Actions.
259
+ */
260
+ function ghAnnotate(
261
+ level: "error" | "warning",
262
+ message: string,
263
+ file?: string,
264
+ line?: number,
265
+ ): void {
266
+ if (!isGitHubActions()) return;
267
+ const locParts: string[] = [];
268
+ if (file) locParts.push(`file=${file}`);
269
+ if (line !== undefined) locParts.push(`line=${String(line)}`);
270
+ const loc = locParts.length > 0 ? " " + locParts.join(",") : "";
271
+ console.log(`::${level}${loc}::${message}`);
272
+ }
273
+
274
+ interface HashCheckResult {
275
+ valid: boolean;
276
+ errorCount: number;
277
+ }
278
+
279
+ function verifyHashes(filePaths: string[], silent = false): HashCheckResult {
280
+ let errorCount = 0;
281
+ const log = (msg: string): void => {
282
+ if (!silent) console.log(msg);
283
+ };
284
+ for (const filePath of filePaths) {
285
+ const fullPath = resolve(process.cwd(), filePath);
286
+
287
+ // If the file doesn't exist at all (typo, deleted), that's an error —
288
+ // not a "no hash" informational message. Without this check, a scoped
289
+ // audit like `vigiles audit typo.md` would silently exit clean.
290
+ if (!existsSync(fullPath)) {
291
+ log(`\n✗ ${filePath} — file not found`);
292
+ if (!silent) {
293
+ ghAnnotate("error", `File not found: ${filePath}`, filePath);
294
+ }
295
+ errorCount++;
296
+ continue;
297
+ }
298
+
299
+ const result = checkFileHash(fullPath);
300
+
301
+ if (!result.hasHash) {
302
+ log(`\n- ${filePath} — no vigiles hash (hand-written or pre-v2)`);
303
+ continue;
304
+ }
305
+
306
+ if (result.valid) {
307
+ log(`\n✓ ${filePath} — hash valid (from ${result.specFile})`);
308
+ continue;
309
+ }
310
+
311
+ log(`\n✗ ${filePath} — hash mismatch (manually edited after compilation)`);
312
+ log(
313
+ ` Re-run \`vigiles compile\` to regenerate from ${result.specFile ?? "spec"}.`,
314
+ );
315
+ if (!silent) {
316
+ ghAnnotate(
317
+ "error",
318
+ "Hash mismatch — file was manually edited after compilation",
319
+ filePath,
320
+ );
321
+ }
322
+ errorCount++;
323
+ }
324
+ return { valid: errorCount === 0, errorCount };
325
+ }
326
+
327
+ function validateSpecs(
328
+ filePaths: string[],
329
+ rulesConfig?: import("./types.js").RulesConfig,
330
+ silent = false,
331
+ ): boolean {
332
+ let allValid = true;
333
+ const log = (msg: string): void => {
334
+ if (!silent) console.log(msg);
335
+ };
336
+ for (const filePath of filePaths) {
337
+ const fullPath = resolve(process.cwd(), filePath);
338
+ let content: string;
339
+ try {
340
+ content = readFileSync(fullPath, "utf-8");
341
+ } catch {
342
+ continue;
343
+ }
344
+ // Multi-target: if file has a "compiled from" hash, it has a spec
345
+ // even if it's not named <file>.spec.ts (e.g., AGENTS.md from CLAUDE.md.spec.ts)
346
+ const hashMatch = content.match(
347
+ /<!-- vigiles:sha256:[a-f0-9]+ compiled from (.+) -->/,
348
+ );
349
+ if (hashMatch) {
350
+ // Verify the referenced spec still exists
351
+ const specRef = resolve(process.cwd(), hashMatch[1]);
352
+ if (!existsSync(specRef)) {
353
+ log(
354
+ ` ✗ [require-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`,
355
+ );
356
+ allValid = false;
357
+ }
358
+ continue;
359
+ }
360
+
361
+ const result = validate(content, {
362
+ filePath: fullPath,
363
+ rules: rulesConfig,
364
+ });
365
+ for (const err of result.errors) {
366
+ log(` ✗ [${err.rule}] ${err.message}`);
367
+ allValid = false;
368
+ }
369
+ for (const warn of result.warnings) {
370
+ log(` ⚠ [${warn.rule}] ${warn.message}`);
371
+ }
372
+ }
373
+ return allValid;
374
+ }
375
+
376
+ interface CombinedCheckResult {
377
+ valid: boolean;
378
+ hashErrors: number;
379
+ validationErrors: number;
380
+ }
381
+
382
+ function check(filePaths: string[], silent = false): CombinedCheckResult {
383
+ const hashes = verifyHashes(filePaths, silent);
384
+ const vConfig = loadConfig();
385
+ const specsValid = validateSpecs(filePaths, vConfig.rules, silent);
386
+ return {
387
+ valid: hashes.valid && specsValid,
388
+ hashErrors: hashes.errorCount,
389
+ // `validateSpecs` only returns a boolean today, so we collapse
390
+ // failures to 1 until it starts reporting counts. Kept in its own
391
+ // counter so audit's "stale hash — run vigiles compile" remediation
392
+ // doesn't misreport a require-spec / other validation failure.
393
+ validationErrors: specsValid ? 0 : 1,
394
+ };
395
+ }
396
+
397
+ interface DuplicateResult {
398
+ valid: boolean;
399
+ pairCount: number;
400
+ }
401
+
402
+ /**
403
+ * Find near-duplicate rules within each spec using NCD similarity.
404
+ * Catches spec bloat — rules that likely say the same thing in different words.
405
+ * Uses information-theoretic distance (gzip-based) — no LLM, fully deterministic.
406
+ */
407
+ async function findDuplicateRules(
408
+ threshold: number = 0.3,
409
+ silent = false,
410
+ scopeFiles?: string[],
411
+ ): Promise<DuplicateResult> {
412
+ const log = (msg: string): void => {
413
+ if (!silent) console.log(msg);
414
+ };
415
+ const allSpecs = findSpecs();
416
+ // If audit was invoked with explicit file arguments, only scan the specs
417
+ // for those files — otherwise an unrelated duplicate elsewhere in the
418
+ // repo would fail a targeted CI check (e.g. `vigiles audit path/foo.md`).
419
+ //
420
+ // Resolve each requested file to its real source spec by reading the
421
+ // compiled-from header. Multi-target projects compile one spec to
422
+ // several targets (e.g. CLAUDE.md.spec.ts → CLAUDE.md + AGENTS.md), so
423
+ // naive `${file}.spec.ts` concatenation would miss the real source for
424
+ // the secondary targets. Fall back to the concatenation rule if the
425
+ // file has no hash header (e.g. freshly hand-written).
426
+ const specs =
427
+ scopeFiles && scopeFiles.length > 0
428
+ ? (() => {
429
+ const wanted = new Set<string>();
430
+ const compiledFromRe =
431
+ /<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from (.+?)\s*-->/;
432
+ for (const f of scopeFiles) {
433
+ let resolved: string | undefined;
434
+ try {
435
+ const content = readFileSync(resolve(process.cwd(), f), "utf-8");
436
+ const m = compiledFromRe.exec(content);
437
+ if (m) {
438
+ resolved = resolve(process.cwd(), m[1].trim());
439
+ }
440
+ } catch {
441
+ // File unreadable — fall through to the naming convention
442
+ }
443
+ if (!resolved) {
444
+ resolved = resolve(process.cwd(), `${f}.spec.ts`);
445
+ }
446
+ wanted.add(resolved);
447
+ }
448
+ return allSpecs.filter((specPath) =>
449
+ wanted.has(resolve(process.cwd(), specPath)),
450
+ );
451
+ })()
452
+ : allSpecs;
453
+ if (specs.length === 0) return { valid: true, pairCount: 0 };
454
+
455
+ let totalPairs = 0;
456
+ let specsWithDuplicates = 0;
457
+
458
+ for (const specPath of specs) {
459
+ const spec = await loadSpec(specPath);
460
+ if (!spec || spec._specType !== "claude") continue;
461
+
462
+ const rules = spec.rules;
463
+ const ruleCount = Object.keys(rules).length;
464
+ if (ruleCount < 2) continue;
465
+
466
+ // Guard against unknown rule kinds (legacy "check" artifacts, JS
467
+ // callers) that would crash ruleToText inside findSimilarRules.
468
+ // Same pattern as the try/catch in runProofSuite.
469
+ let pairs: { idA: string; idB: string; distance: number }[];
470
+ try {
471
+ pairs = findSimilarRules(rules, threshold);
472
+ } catch (e) {
473
+ log(
474
+ ` ⚠ ${specPath}: similarity check failed (${e instanceof Error ? e.message : String(e)})`,
475
+ );
476
+ continue;
477
+ }
478
+ if (pairs.length === 0) continue;
479
+
480
+ if (specsWithDuplicates === 0) {
481
+ log(`Found near-duplicate rules (NCD < ${String(threshold)}):\n`);
482
+ }
483
+ specsWithDuplicates++;
484
+ totalPairs += pairs.length;
485
+
486
+ log(` ${specPath}`);
487
+ for (const pair of pairs.slice(0, 5)) {
488
+ log(
489
+ ` ${pair.idA} ↔ ${pair.idB} (distance: ${pair.distance.toFixed(3)})`,
490
+ );
491
+ }
492
+ if (pairs.length > 5) {
493
+ log(` ... and ${String(pairs.length - 5)} more`);
494
+ }
495
+ }
496
+
497
+ if (totalPairs === 0) {
498
+ log("No near-duplicate rules detected.");
499
+ return { valid: true, pairCount: 0 };
500
+ }
501
+
502
+ log(
503
+ `\n ${String(totalPairs)} duplicate pair(s) in ${String(specsWithDuplicates)} spec(s). Consider merging or rewording.`,
504
+ );
505
+ return { valid: false, pairCount: totalPairs };
506
+ }
507
+
508
+ /**
509
+ * Structured audit report used by --json, --summary, and exit-code logic.
510
+ */
511
+ interface AuditReport {
512
+ hashErrors: number;
513
+ validationErrors: number;
514
+ inlineErrors: number;
515
+ inlineRules: number;
516
+ duplicatePairs: number;
517
+ coverageEnabled: number;
518
+ coverageDocumented: number;
519
+ strengthenSuggestions: number;
520
+ freshnessErrors: number;
521
+ files: string[];
522
+ }
523
+
524
+ /** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
525
+ function auditExitCode(report: AuditReport): 0 | 1 | 2 {
526
+ if (
527
+ report.hashErrors > 0 ||
528
+ report.validationErrors > 0 ||
529
+ report.inlineErrors > 0 ||
530
+ report.freshnessErrors > 0
531
+ )
532
+ return 2;
533
+ if (report.duplicatePairs > 0) return 1;
534
+ // Coverage gaps and guidance counts are informational, not failures
535
+ return 0;
536
+ }
537
+
538
+ /**
539
+ * Verify inline `<!-- vigiles:enforce ... -->` comments in an instruction
540
+ * file. Each comment's linter rule goes through the same verification as
541
+ * spec-declared enforce rules (existence, enabled status, closest-match
542
+ * suggestions on typo).
543
+ */
544
+ function verifyInlineRules(
545
+ filePath: string,
546
+ silent: boolean,
547
+ linterOptions?: {
548
+ catalogOnly?: boolean;
549
+ linters?: Record<string, { rulesDir?: string | string[] }>;
550
+ },
551
+ ): { ok: boolean; errorCount: number; ruleCount: number } {
552
+ const log = (msg: string): void => {
553
+ if (!silent) console.log(msg);
554
+ };
555
+
556
+ let content: string;
557
+ try {
558
+ content = readFileSync(resolve(process.cwd(), filePath), "utf-8");
559
+ } catch {
560
+ return { ok: true, errorCount: 0, ruleCount: 0 };
561
+ }
562
+
563
+ const { rules, errors: parseErrors } = parseInlineRules(content);
564
+ if (rules.length === 0 && parseErrors.length === 0) {
565
+ return { ok: true, errorCount: 0, ruleCount: 0 };
566
+ }
567
+
568
+ let errorCount = 0;
569
+ log(`\n${filePath} (inline mode):`);
570
+
571
+ for (const err of parseErrors) {
572
+ log(` ✗ line ${String(err.line)}: ${err.message}`);
573
+ errorCount++;
574
+ if (!silent) {
575
+ ghAnnotate("error", err.message, filePath, err.line);
576
+ }
577
+ }
578
+
579
+ for (const rule of rules) {
580
+ const result = checkLinterRule(
581
+ rule.linterRule,
582
+ process.cwd(),
583
+ linterOptions,
584
+ );
585
+ if (!result.exists) {
586
+ const message = result.error ?? `Rule "${rule.linterRule}" not found`;
587
+ log(` ✗ line ${String(rule.line)}: ${message}`);
588
+ errorCount++;
589
+ if (!silent) {
590
+ ghAnnotate("error", message, filePath, rule.line);
591
+ }
592
+ } else if (result.enabled === "disabled") {
593
+ const message = `Rule "${rule.linterRule}" exists but is disabled in ${result.linter} config`;
594
+ log(` ✗ line ${String(rule.line)}: ${message}`);
595
+ errorCount++;
596
+ if (!silent) {
597
+ ghAnnotate("error", message, filePath, rule.line);
598
+ }
599
+ } else {
600
+ log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
601
+ }
602
+ }
603
+
604
+ return { ok: errorCount === 0, errorCount, ruleCount: rules.length };
605
+ }
606
+
607
+ /**
608
+ * Unified audit command: verify hashes, report coverage gaps, detect duplicates,
609
+ * suggest improvements.
610
+ *
611
+ * Flags:
612
+ * --summary Print a single-line summary (for SessionStart hooks)
613
+ * --json Print structured JSON report (for CI integration)
614
+ */
615
+ async function audit(
616
+ restArgs: string[],
617
+ flags: string[],
618
+ config?: VigilesConfig,
619
+ ): Promise<AuditReport> {
620
+ const summary = flags.includes("--summary");
621
+ const json = flags.includes("--json");
622
+ const silent = summary || json;
623
+
624
+ const files = findInstructionFiles(restArgs);
625
+
626
+ // 1. Verify hashes and structure
627
+ if (!silent) {
628
+ if (files.length > 0) {
629
+ console.log("Verifying compiled files...\n");
630
+ } else {
631
+ console.log("No compiled instruction files found.\n");
632
+ }
633
+ }
634
+ const hashResult =
635
+ files.length > 0
636
+ ? check(files, silent)
637
+ : { valid: true, hashErrors: 0, validationErrors: 0 };
638
+
639
+ // 1b. Verify inline vigiles:enforce comments in any instruction file
640
+ // that isn't already managed by a .spec.ts. Spec mode is the source of
641
+ // truth when it exists, so a literal `<!-- vigiles:enforce ... -->`
642
+ // snippet that survived into the compiled markdown (or an
643
+ // explanatory example in a spec-managed file) must not trip audit.
644
+ // A file is spec-managed iff it has a sibling `<file>.spec.ts` OR its
645
+ // own `<!-- vigiles:sha256:... compiled from <spec> -->` header.
646
+ // See docs/inline-mode.md.
647
+ let inlineErrors = 0;
648
+ let inlineRules = 0;
649
+ if (!silent && files.length > 0) {
650
+ console.log("\nInline rule verification:");
651
+ }
652
+ const compiledFromRe =
653
+ /<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from .+?\s*-->/;
654
+ for (const filePath of files) {
655
+ const abs = resolve(process.cwd(), filePath);
656
+ if (existsSync(`${abs}.spec.ts`)) {
657
+ continue; // managed by sibling spec
658
+ }
659
+ let content: string;
660
+ try {
661
+ content = readFileSync(abs, "utf-8");
662
+ } catch {
663
+ continue;
664
+ }
665
+ if (compiledFromRe.test(content)) {
666
+ continue; // managed by the spec referenced in the hash header
667
+ }
668
+ const result = verifyInlineRules(filePath, silent, {
669
+ catalogOnly: config?.catalogOnly,
670
+ linters: config?.linters,
671
+ });
672
+ inlineErrors += result.errorCount;
673
+ inlineRules += result.ruleCount;
674
+ }
675
+ if (!silent && files.length > 0 && inlineRules === 0) {
676
+ console.log(" (no inline vigiles:enforce comments found)");
677
+ }
678
+
679
+ // 2. Coverage gaps (discover)
680
+ if (!silent) console.log("\nLinter rule coverage:\n");
681
+ const coverage = discover(silent);
682
+
683
+ // 3. Duplicate rule detection (NCD). Scope to the requested files when
684
+ // audit was invoked with explicit paths, so targeted CI checks don't
685
+ // fail on unrelated duplicates elsewhere in the repo.
686
+ if (!silent) console.log("\nDuplicate rule detection:\n");
687
+ const dups = await findDuplicateRules(
688
+ 0.3,
689
+ silent,
690
+ restArgs.length > 0 ? files : undefined,
691
+ );
692
+
693
+ // 4. Guidance rule count (strengthen suggestions moved to /strengthen skill)
694
+ const guidanceCount = await countGuidanceRules(silent);
695
+
696
+ // 5. Freshness check
697
+ const freshnessSeverity = config?.rules.freshness;
698
+ let freshnessErrors = 0;
699
+ if (freshnessSeverity) {
700
+ if (!silent) console.log("\nFreshness check:\n");
701
+ const mode = config?.freshnessMode ?? "strict";
702
+ freshnessErrors = await checkFreshness(
703
+ files,
704
+ mode,
705
+ config,
706
+ freshnessSeverity,
707
+ silent,
708
+ );
709
+ }
710
+
711
+ const report: AuditReport = {
712
+ hashErrors: hashResult.hashErrors,
713
+ validationErrors: hashResult.validationErrors,
714
+ inlineErrors,
715
+ inlineRules,
716
+ duplicatePairs: dups.pairCount,
717
+ coverageEnabled: coverage.enabled,
718
+ coverageDocumented: coverage.documented,
719
+ strengthenSuggestions: guidanceCount,
720
+ freshnessErrors,
721
+ files,
722
+ };
723
+
724
+ if (summary) {
725
+ printAuditSummary(report);
726
+ } else if (json) {
727
+ console.log(JSON.stringify(report, null, 2));
728
+ }
729
+
730
+ return report;
731
+ }
732
+
733
+ /** Single-line audit summary for SessionStart hooks — minimal token cost. */
734
+ function printAuditSummary(report: AuditReport): void {
735
+ const parts: string[] = [];
736
+ if (report.hashErrors > 0) parts.push(`${String(report.hashErrors)} stale`);
737
+ if (report.validationErrors > 0)
738
+ parts.push(`${String(report.validationErrors)} validation errors`);
739
+ if (report.inlineErrors > 0)
740
+ parts.push(`${String(report.inlineErrors)} inline errors`);
741
+ if (report.duplicatePairs > 0)
742
+ parts.push(`${String(report.duplicatePairs)} duplicates`);
743
+ const undocumented = report.coverageEnabled - report.coverageDocumented;
744
+ if (undocumented > 0)
745
+ parts.push(`${String(undocumented)} undocumented rules`);
746
+ if (report.strengthenSuggestions > 0)
747
+ parts.push(
748
+ `${String(report.strengthenSuggestions)} guidance (run /strengthen to upgrade)`,
749
+ );
750
+ if (report.freshnessErrors > 0)
751
+ parts.push(`${String(report.freshnessErrors)} stale (run vigiles compile)`);
752
+ if (parts.length === 0) {
753
+ console.log("vigiles: clean");
754
+ } else {
755
+ console.log(`vigiles: ${parts.join(" / ")}`);
756
+ }
757
+ }
758
+
759
+ function collectDocumentedRules(): Set<string> {
760
+ const documented = new Set<string>();
761
+ const mdFiles = globSync("**/CLAUDE.md", {
762
+ ignore: IGNORE_NODE_MODULES,
763
+ cwd: process.cwd(),
764
+ });
765
+ for (const mdFile of mdFiles) {
766
+ const content = readFileSync(resolve(process.cwd(), mdFile), "utf-8");
767
+ const enforcedRe = /\*\*Enforced by:\*\*\s*`([^`]+)`/g;
768
+ let m: RegExpExecArray | null;
769
+ while ((m = enforcedRe.exec(content)) !== null) {
770
+ documented.add(m[1]);
771
+ }
772
+ }
773
+ return documented;
774
+ }
775
+
776
+ interface CoverageTotals {
777
+ enabled: number;
778
+ documented: number;
779
+ }
780
+
781
+ function printLinterCoverage(
782
+ linter: { linter: string; rules: string[] },
783
+ documentedRules: Set<string>,
784
+ silent = false,
785
+ ): CoverageTotals {
786
+ const log = (msg: string): void => {
787
+ if (!silent) console.log(msg);
788
+ };
789
+ const documented = linter.rules.filter((r) =>
790
+ documentedRules.has(`${linter.linter}/${r}`),
791
+ );
792
+ const undocumented = linter.rules.filter(
793
+ (r) => !documentedRules.has(`${linter.linter}/${r}`),
794
+ );
795
+ const pct =
796
+ linter.rules.length > 0
797
+ ? Math.round((documented.length / linter.rules.length) * 100)
798
+ : 0;
799
+
800
+ log(
801
+ ` ${linter.linter}: ${String(documented.length)}/${String(linter.rules.length)} rules documented (${String(pct)}%)`,
802
+ );
803
+
804
+ if (documented.length > 0 && documented.length <= 10) {
805
+ for (const r of documented) {
806
+ log(` ✓ ${linter.linter}/${r}`);
807
+ }
808
+ }
809
+
810
+ if (undocumented.length > 0) {
811
+ const show = undocumented.slice(0, 5);
812
+ log(` Top undocumented:`);
813
+ for (const r of show) {
814
+ log(` ✗ ${linter.linter}/${r}`);
815
+ }
816
+ if (undocumented.length > 5) {
817
+ log(` ... and ${String(undocumented.length - 5)} more`);
818
+ }
819
+ }
820
+ log("");
821
+
822
+ return { enabled: linter.rules.length, documented: documented.length };
823
+ }
824
+
825
+ function discover(silent = false): CoverageTotals {
826
+ const log = (msg: string): void => {
827
+ if (!silent) console.log(msg);
828
+ };
829
+ log("Scanning project for linter rules...\n");
830
+
831
+ const result = generateTypes({ basePath: process.cwd() });
832
+ const documentedRules = collectDocumentedRules();
833
+
834
+ log("Detected linters:\n");
835
+
836
+ let totalEnabled = 0;
837
+ let totalDocumented = 0;
838
+
839
+ for (const linter of result.linters) {
840
+ const totals = printLinterCoverage(linter, documentedRules, silent);
841
+ totalEnabled += totals.enabled;
842
+ totalDocumented += totals.documented;
843
+ }
844
+
845
+ if (result.linters.length === 0) {
846
+ log(" No linters detected.\n");
847
+ }
848
+
849
+ const totalPct =
850
+ totalEnabled > 0 ? Math.round((totalDocumented / totalEnabled) * 100) : 0;
851
+ log(
852
+ `Coverage: ${String(totalDocumented)}/${String(totalEnabled)} rules documented (${String(totalPct)}%)`,
853
+ );
854
+
855
+ if (totalDocumented < totalEnabled) {
856
+ log(
857
+ `\nConsider adding enforce() rules for frequently-triggered undocumented rules.`,
858
+ );
859
+ log(`The agent encounters these rules but has no context about WHY.`);
860
+ }
861
+
862
+ return { enabled: totalEnabled, documented: totalDocumented };
863
+ }
864
+
865
+ function init(args: string[]): void {
866
+ const targetFlag = args.find((a) => a.startsWith("--target="));
867
+ const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
868
+ const specPath = `${target}.spec.ts`;
869
+
870
+ if (existsSync(resolve(process.cwd(), specPath))) {
871
+ console.log(`${specPath} already exists.`);
872
+ return;
873
+ }
874
+
875
+ const targetLine = target !== "CLAUDE.md" ? `\n target: "${target}",` : "";
876
+ const template = `import { claude, enforce, guidance } from "vigiles/spec";
877
+
878
+ export default claude({${targetLine}
879
+ sections: {
880
+ // Prose sections become ## headings in the compiled output.
881
+ // Do not add # or ## headers inside sections.
882
+ // positioning: "What this project does and why.",
883
+
884
+ // This section is included in the compiled output to help agents
885
+ // understand how to work with specs. Remove it once your team is familiar.
886
+ "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.",
887
+ },
888
+
889
+ commands: {
890
+ // Commands are verified against package.json at compile time.
891
+ // "npm run build": "Compile the project",
892
+ // "npm test": "Run all tests",
893
+ },
894
+
895
+ keyFiles: {
896
+ // File paths are verified to exist at compile time.
897
+ // "src/index.ts": "Main entry point",
898
+ },
899
+
900
+ rules: {
901
+ // enforce() — backed by a linter rule, verified to exist AND be enabled:
902
+ // "no-console": enforce("eslint/no-console", "Use structured logger."),
903
+ //
904
+ // guidance() — prose only, no enforcement:
905
+ // "research-first": guidance("Google unfamiliar APIs before implementing."),
906
+ },
907
+ });
908
+ `;
909
+ writeFileSync(resolve(process.cwd(), specPath), template);
910
+ console.log(`Created ${specPath} — edit it and run \`vigiles compile\`.`);
911
+ }
912
+
913
+ // ---------------------------------------------------------------------------
914
+ // Setup wizard
915
+ // ---------------------------------------------------------------------------
916
+
917
+ const VIGILES_CI_STEP = ` - name: Verify specs
918
+ run: npx vigiles audit && npx vigiles generate-types --check`;
919
+
920
+ function addGhaStep(): boolean {
921
+ // Find existing GHA workflow
922
+ const ciPaths = [
923
+ ".github/workflows/ci.yml",
924
+ ".github/workflows/ci.yaml",
925
+ ".github/workflows/main.yml",
926
+ ".github/workflows/main.yaml",
927
+ ".github/workflows/test.yml",
928
+ ".github/workflows/test.yaml",
929
+ ];
930
+ for (const ciPath of ciPaths) {
931
+ const fullPath = resolve(process.cwd(), ciPath);
932
+ if (existsSync(fullPath)) {
933
+ const content = readFileSync(fullPath, "utf-8");
934
+ if (content.includes("vigiles")) {
935
+ console.log(`✓ ${ciPath} already has vigiles steps`);
936
+ return true;
937
+ }
938
+ // Append step at end of file (safe for all YAML formats)
939
+ const trimmed = content.trimEnd();
940
+ writeFileSync(fullPath, trimmed + "\n\n" + VIGILES_CI_STEP + "\n");
941
+ console.log(`✓ Added vigiles check step to ${ciPath}`);
942
+ return true;
943
+ }
944
+ }
945
+ return false;
946
+ }
947
+
948
+ // ---------------------------------------------------------------------------
949
+ // Project detection for setup wizard
950
+ // ---------------------------------------------------------------------------
951
+
952
+ interface DetectedProject {
953
+ /** Instruction files found (with or without specs). */
954
+ instructionFiles: { path: string; hasSpec: boolean; isSymlink: boolean }[];
955
+ /** Agent tools detected. */
956
+ agents: string[];
957
+ /** Sync tools detected in package.json. */
958
+ syncTools: string[];
959
+ /** Non-markdown agent config files. */
960
+ otherConfigs: string[];
961
+ /** Whether Claude Code project config exists. */
962
+ hasClaude: boolean;
963
+ }
964
+
965
+ const KNOWN_INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
966
+ const KNOWN_OTHER_CONFIGS: Record<string, string> = {
967
+ ".cursorrules": "Cursor",
968
+ ".github/copilot-instructions.md": "GitHub Copilot",
969
+ ".windsurfrules": "Windsurf",
970
+ };
971
+ const KNOWN_SYNC_TOOLS = [
972
+ "rule-porter",
973
+ "rulesync",
974
+ "vibe-cli",
975
+ "@nichochar/rule-porter",
976
+ ];
977
+
978
+ function detectProject(): DetectedProject {
979
+ const cwd = process.cwd();
980
+ const instructionFiles: DetectedProject["instructionFiles"] = [];
981
+ const agents: string[] = [];
982
+ const otherConfigs: string[] = [];
983
+
984
+ // Check known instruction files
985
+ for (const f of KNOWN_INSTRUCTION_FILES) {
986
+ const full = resolve(cwd, f);
987
+ if (existsSync(full)) {
988
+ let isSymlink = false;
989
+ try {
990
+ isSymlink = lstatSync(full).isSymbolicLink();
991
+ } catch {
992
+ // ignore
993
+ }
994
+ const hasSpec = existsSync(resolve(cwd, `${f}.spec.ts`));
995
+ instructionFiles.push({ path: f, hasSpec, isSymlink });
996
+ }
997
+ }
998
+
999
+ // Detect agents from files
1000
+ if (
1001
+ instructionFiles.some((f) => f.path === "CLAUDE.md") ||
1002
+ existsSync(resolve(cwd, ".claude"))
1003
+ ) {
1004
+ agents.push("Claude Code");
1005
+ }
1006
+ if (instructionFiles.some((f) => f.path === "AGENTS.md")) {
1007
+ agents.push("Codex / GitHub Copilot");
1008
+ }
1009
+
1010
+ // Check non-markdown configs
1011
+ for (const [path, agent] of Object.entries(KNOWN_OTHER_CONFIGS)) {
1012
+ if (existsSync(resolve(cwd, path))) {
1013
+ otherConfigs.push(`${path} (${agent})`);
1014
+ if (!agents.includes(agent)) agents.push(agent);
1015
+ }
1016
+ }
1017
+
1018
+ // Check for sync tools in package.json
1019
+ const syncTools: string[] = [];
1020
+ const pkgPath = resolve(cwd, "package.json");
1021
+ if (existsSync(pkgPath)) {
1022
+ try {
1023
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
1024
+ dependencies?: Record<string, string>;
1025
+ devDependencies?: Record<string, string>;
1026
+ };
1027
+ const allDeps = {
1028
+ ...pkg.dependencies,
1029
+ ...pkg.devDependencies,
1030
+ };
1031
+ for (const tool of KNOWN_SYNC_TOOLS) {
1032
+ if (tool in allDeps) syncTools.push(tool);
1033
+ }
1034
+ } catch {
1035
+ // ignore
1036
+ }
1037
+ }
1038
+
1039
+ return {
1040
+ instructionFiles,
1041
+ agents,
1042
+ syncTools,
1043
+ otherConfigs,
1044
+ hasClaude: existsSync(resolve(cwd, ".claude")),
1045
+ };
1046
+ }
1047
+
1048
+ async function setup(args: string[]): Promise<void> {
1049
+ const targetFlag = args.find((a) => a.startsWith("--target="));
1050
+ const strict = args.includes("--strict");
1051
+ const noGha = args.includes("--no-gha");
1052
+
1053
+ console.log(`vigiles setup${strict ? " (strict mode)" : ""}\n`);
1054
+
1055
+ // Step 1: Detect project
1056
+ const detected = detectProject();
1057
+
1058
+ if (detected.agents.length > 0) {
1059
+ console.log(`Detected: ${detected.agents.join(", ")}`);
1060
+ }
1061
+ if (detected.otherConfigs.length > 0) {
1062
+ console.log(`Other agent configs: ${detected.otherConfigs.join(", ")}`);
1063
+ }
1064
+ if (detected.syncTools.length > 0) {
1065
+ console.log(`Sync tools: ${detected.syncTools.join(", ")}`);
1066
+ }
1067
+ for (const f of detected.instructionFiles) {
1068
+ if (f.isSymlink) {
1069
+ console.log(`Note: ${f.path} is a symlink`);
1070
+ }
1071
+ }
1072
+ if (
1073
+ detected.agents.length > 0 ||
1074
+ detected.otherConfigs.length > 0 ||
1075
+ detected.syncTools.length > 0
1076
+ ) {
1077
+ console.log("");
1078
+ }
1079
+
1080
+ // Step 2: Determine targets
1081
+ let targets: string[];
1082
+ if (targetFlag) {
1083
+ targets = [targetFlag.split("=")[1]];
1084
+ } else {
1085
+ // Auto-detect: create specs for instruction files that need them
1086
+ const needsSpec = detected.instructionFiles.filter((f) => !f.hasSpec);
1087
+ if (needsSpec.length > 0) {
1088
+ // Existing files without specs — suggest migration
1089
+ for (const f of needsSpec) {
1090
+ console.log(
1091
+ `Found ${f.path} without a spec. Migrate with the migrate-to-spec skill`,
1092
+ );
1093
+ console.log(
1094
+ ` or create a blank spec: npx vigiles init --target=${f.path}\n`,
1095
+ );
1096
+ }
1097
+ const hasAnySpec = detected.instructionFiles.some((f) => f.hasSpec);
1098
+ if (
1099
+ !hasAnySpec &&
1100
+ needsSpec.length === detected.instructionFiles.length
1101
+ ) {
1102
+ // ALL existing files need migration — don't create new ones
1103
+ console.log("Install the plugin to use the migration skill:");
1104
+ console.log(" npx skills add zernie/vigiles");
1105
+ return;
1106
+ }
1107
+ }
1108
+
1109
+ // Default: CLAUDE.md, plus AGENTS.md if Codex detected
1110
+ targets = ["CLAUDE.md"];
1111
+ const hasAgentsMd = detected.instructionFiles.some(
1112
+ (f) => f.path === "AGENTS.md",
1113
+ );
1114
+ const hasCodex =
1115
+ detected.agents.includes("Codex / GitHub Copilot") || hasAgentsMd;
1116
+ if (hasCodex && !hasAgentsMd) {
1117
+ targets.push("AGENTS.md");
1118
+ }
1119
+ }
1120
+
1121
+ // Step 3: Create specs
1122
+ for (const target of targets) {
1123
+ const specPath = `${target}.spec.ts`;
1124
+ if (existsSync(resolve(process.cwd(), specPath))) {
1125
+ console.log(`✓ ${specPath} already exists`);
1126
+ } else if (existsSync(resolve(process.cwd(), target))) {
1127
+ console.log(
1128
+ `⚠ ${target} exists without spec — migrate with migrate-to-spec skill`,
1129
+ );
1130
+ } else {
1131
+ init(["--target=" + target]);
1132
+ }
1133
+ }
1134
+
1135
+ // Step 4: Generate types
1136
+ console.log("\nScanning linters and project files...");
1137
+ const typesResult = generateTypes({ basePath: process.cwd() });
1138
+ const outPath = ".vigiles/generated.d.ts";
1139
+ const outDir = resolve(process.cwd(), ".vigiles");
1140
+ if (!existsSync(outDir)) {
1141
+ mkdirSync(outDir, { recursive: true });
1142
+ }
1143
+ writeFileSync(resolve(process.cwd(), outPath), typesResult.dts);
1144
+ for (const l of typesResult.linters) {
1145
+ console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
1146
+ }
1147
+ if (typesResult.scripts.length > 0) {
1148
+ console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
1149
+ }
1150
+ console.log(`✓ Generated ${outPath}`);
1151
+
1152
+ // Step 5: Compile specs
1153
+ console.log("\nCompiling specs...");
1154
+ const specs = findSpecs();
1155
+ if (specs.length > 0) {
1156
+ await compile(specs, loadConfig());
1157
+ }
1158
+
1159
+ // Step 6: Add CI step
1160
+ console.log("");
1161
+ const addedGha = noGha ? false : addGhaStep();
1162
+ if (!addedGha) {
1163
+ console.log(" No CI workflow found. Add this step to your CI:\n");
1164
+ console.log(" npx vigiles audit && npx vigiles generate-types --check");
1165
+ }
1166
+
1167
+ // Step 7: Install Claude Code plugin (hooks + skills)
1168
+ const shouldInstallPlugin =
1169
+ detected.hasClaude || targets.includes("CLAUDE.md");
1170
+ if (shouldInstallPlugin) {
1171
+ let pluginInstalled = false;
1172
+
1173
+ // Try installing the full plugin via skills CLI (gives hooks + skills)
1174
+ try {
1175
+ const { execSync: exec } =
1176
+ require("node:child_process") as typeof import("node:child_process");
1177
+ exec("npx skills add zernie/vigiles", {
1178
+ cwd: process.cwd(),
1179
+ stdio: ["pipe", "pipe", "pipe"],
1180
+ timeout: 30000,
1181
+ });
1182
+ pluginInstalled = true;
1183
+ console.log("✓ Installed vigiles plugin (hooks + skills) via skills CLI");
1184
+ } catch {
1185
+ // skills CLI not available — fall back to direct hook installation
1186
+ }
1187
+
1188
+ if (!pluginInstalled) {
1189
+ // Fall back: write hooks directly to .claude/settings.json
1190
+ // (gives auto-compile + block edits, but no skills)
1191
+ const settingsDir = resolve(process.cwd(), ".claude");
1192
+ const settingsPath = resolve(settingsDir, "settings.json");
1193
+ if (!existsSync(settingsDir)) {
1194
+ mkdirSync(settingsDir, { recursive: true });
1195
+ }
1196
+
1197
+ let settings: Record<string, unknown> = {};
1198
+ if (existsSync(settingsPath)) {
1199
+ try {
1200
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as Record<
1201
+ string,
1202
+ unknown
1203
+ >;
1204
+ } catch {
1205
+ // Ignore malformed settings
1206
+ }
1207
+ }
1208
+
1209
+ if (!settings["hooks"]) {
1210
+ settings["hooks"] = {};
1211
+ }
1212
+ const hooks = settings["hooks"] as Record<string, unknown>;
1213
+
1214
+ 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`;
1215
+ 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`;
1216
+
1217
+ // Append to existing arrays (don't duplicate if already present)
1218
+ const existingStr = JSON.stringify(settings);
1219
+ if (!existingStr.includes("vigiles:sha256")) {
1220
+ const pre = (hooks["PreToolUse"] ?? []) as unknown[];
1221
+ pre.push({ matcher: "Edit|Write", command: preCmd });
1222
+ hooks["PreToolUse"] = pre;
1223
+ }
1224
+ if (!existingStr.includes("vigiles compile")) {
1225
+ const post = (hooks["PostToolUse"] ?? []) as unknown[];
1226
+ post.push({ matcher: "Edit|Write", command: postCmd });
1227
+ hooks["PostToolUse"] = post;
1228
+ }
1229
+
1230
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1231
+ console.log("✓ Installed hooks in .claude/settings.json");
1232
+ console.log(
1233
+ " (For skills like edit-spec and migrate-to-spec, also run: npx skills add zernie/vigiles)",
1234
+ );
1235
+ }
1236
+ }
1237
+
1238
+ // Step 8: Agent-specific guidance
1239
+ const specPathsList = targets.map((t) => `${t}.spec.ts`);
1240
+ const specPaths = specPathsList.join(", ");
1241
+
1242
+ if (targets.includes("AGENTS.md")) {
1243
+ console.log(
1244
+ "\n Codex / Copilot reads AGENTS.md directly — no hooks needed.",
1245
+ );
1246
+ console.log(
1247
+ " Run `npx vigiles compile` after spec edits. CI enforces freshness.",
1248
+ );
1249
+ }
1250
+
1251
+ if (detected.otherConfigs.length > 0 && detected.syncTools.length === 0) {
1252
+ console.log(
1253
+ "\n Non-markdown agent configs detected. Use a sync tool to convert:",
1254
+ );
1255
+ console.log(" npm install -D rule-porter");
1256
+ }
1257
+
1258
+ // Step 8b: Write config if strict mode
1259
+ if (strict) {
1260
+ const configPath = resolve(process.cwd(), ".vigilesrc.json");
1261
+ if (!existsSync(configPath)) {
1262
+ writeFileSync(
1263
+ configPath,
1264
+ JSON.stringify(
1265
+ {
1266
+ rules: {
1267
+ "require-spec": "error",
1268
+ "require-skill-spec": "error",
1269
+ },
1270
+ },
1271
+ null,
1272
+ 2,
1273
+ ) + "\n",
1274
+ );
1275
+ console.log("✓ Created .vigilesrc.json with strict rules");
1276
+ }
1277
+ }
1278
+
1279
+ // Step 9: Summary
1280
+ console.log("\n---");
1281
+ console.log("Setup complete.\n");
1282
+ console.log(` 1. Edit ${specPaths} — add your project's conventions`);
1283
+ console.log(
1284
+ " 2. Run `/strengthen` in Claude Code to upgrade guidance → enforce",
1285
+ );
1286
+ if (!strict) {
1287
+ console.log(
1288
+ " 3. When ready, enforce specs in CI: npx vigiles setup --strict",
1289
+ );
1290
+ }
1291
+ console.log("\n Commit:");
1292
+ const files = [
1293
+ ...targets,
1294
+ ...specPathsList,
1295
+ ".vigiles/generated.d.ts",
1296
+ ...(shouldInstallPlugin ? [".claude/settings.json"] : []),
1297
+ ...(strict ? [".vigilesrc.json"] : []),
1298
+ ];
1299
+ console.log(
1300
+ ` git add ${files.join(" ")} && git commit -m "Add vigiles spec"`,
1301
+ );
1302
+ }
1303
+
1304
+ // ---------------------------------------------------------------------------
1305
+ // Strengthen: guidance() → enforce() suggestions
1306
+ // ---------------------------------------------------------------------------
1307
+
1308
+ async function checkFreshness(
1309
+ files: string[],
1310
+ mode: "strict" | "input-hash" | "output-hash",
1311
+ config: VigilesConfig | undefined,
1312
+ severity: "warn" | "error",
1313
+ silent: boolean,
1314
+ ): Promise<number> {
1315
+ const log = (msg: string): void => {
1316
+ if (!silent) console.log(msg);
1317
+ };
1318
+
1319
+ let errorCount = 0;
1320
+ const basePath = process.cwd();
1321
+
1322
+ for (const filePath of files) {
1323
+ const abs = resolve(basePath, filePath);
1324
+ if (!existsSync(abs)) continue;
1325
+ const content = readFileSync(abs, "utf-8");
1326
+
1327
+ // Find the spec that compiled this file
1328
+ const hashMatch = content.match(
1329
+ /<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from (.+?)\s*-->/,
1330
+ );
1331
+ if (!hashMatch) {
1332
+ // No hash = hand-written file, skip freshness check
1333
+ continue;
1334
+ }
1335
+
1336
+ let result: FreshnessResult;
1337
+ const specFile = hashMatch[1];
1338
+
1339
+ if (mode === "strict") {
1340
+ // Recompile in memory and diff
1341
+ const spec = await loadSpec(specFile);
1342
+ if (!spec || spec._specType !== "claude") {
1343
+ log(` ? ${filePath} — can't load spec "${specFile}", skipping`);
1344
+ continue;
1345
+ }
1346
+ const compiled = compileClaude(spec, {
1347
+ basePath,
1348
+ specFile,
1349
+ maxRules: config?.maxRules,
1350
+ maxTokens: config?.maxTokens,
1351
+ maxSectionLines: config?.maxSectionLines,
1352
+ catalogOnly: config?.catalogOnly,
1353
+ linters: config?.linters,
1354
+ });
1355
+ // Compare markdown body (strip hash/input lines)
1356
+ const metaRe = /^<!-- vigiles:(sha256|inputs):[^\n]+ -->\r?\n?/gm;
1357
+ const existingBody = content.replace(metaRe, "").trim();
1358
+ const compiledBody = compiled.markdown.replace(metaRe, "").trim();
1359
+ if (existingBody === compiledBody) {
1360
+ result = { fresh: true, mode: "strict" };
1361
+ } else {
1362
+ result = {
1363
+ fresh: false,
1364
+ mode: "strict",
1365
+ reason: "Output would differ if recompiled — run `vigiles compile`",
1366
+ };
1367
+ }
1368
+ } else if (mode === "input-hash") {
1369
+ const specFile = hashMatch[1];
1370
+ const spec = await loadSpec(specFile);
1371
+ if (!spec || spec._specType !== "claude") {
1372
+ log(` ? ${filePath} — can't load spec "${specFile}", skipping`);
1373
+ continue;
1374
+ }
1375
+ const inputs = discoverInputs(
1376
+ specFile,
1377
+ spec,
1378
+ basePath,
1379
+ config?.freshnessInputs,
1380
+ );
1381
+ result = checkInputHashFreshness(content, inputs.files, basePath);
1382
+ } else {
1383
+ // output-hash mode
1384
+ result = checkOutputHashFreshness(content);
1385
+ }
1386
+
1387
+ if (!result.fresh) {
1388
+ errorCount++;
1389
+ const marker = severity === "error" ? "✗" : "⚠";
1390
+ log(` ${marker} ${filePath} — ${result.reason ?? "stale"}`);
1391
+ if (result.changedFiles && result.changedFiles.length > 0) {
1392
+ for (const f of result.changedFiles) {
1393
+ log(` changed: ${f}`);
1394
+ }
1395
+ }
1396
+ } else if (!silent) {
1397
+ log(` ✓ ${filePath} — fresh (${mode})`);
1398
+ }
1399
+ }
1400
+
1401
+ if (errorCount === 0) {
1402
+ log(" All files fresh.");
1403
+ }
1404
+
1405
+ return severity === "error" ? errorCount : 0;
1406
+ }
1407
+
1408
+ async function countGuidanceRules(silent = false): Promise<number> {
1409
+ const specs = findSpecs();
1410
+ if (specs.length === 0) return 0;
1411
+
1412
+ let count = 0;
1413
+ for (const specPath of specs) {
1414
+ const spec = await loadSpec(specPath);
1415
+ if (!spec || spec._specType !== "claude") continue;
1416
+ for (const rule of Object.values(spec.rules)) {
1417
+ if (rule._kind === "guidance") count++;
1418
+ }
1419
+ }
1420
+
1421
+ if (!silent && count > 0) {
1422
+ console.log(
1423
+ `${String(count)} guidance rule(s) — run /strengthen to find enforce() upgrades\n`,
1424
+ );
1425
+ }
1426
+ return count;
1427
+ }
1428
+
1429
+ // ---------------------------------------------------------------------------
1430
+ // Command handlers for main()
1431
+ // ---------------------------------------------------------------------------
1432
+
1433
+ function findInstructionFiles(restArgs: string[]): string[] {
1434
+ if (restArgs.length > 0) return restArgs;
1435
+ const patterns = ["**/CLAUDE.md", "**/AGENTS.md", "**/SKILL.md"];
1436
+ const files: string[] = [];
1437
+ for (const pattern of patterns) {
1438
+ files.push(
1439
+ ...globSync(pattern, { ignore: IGNORE_NODE_MODULES, cwd: process.cwd() }),
1440
+ );
1441
+ }
1442
+ return files;
1443
+ }
1444
+
1445
+ function handleGenerateTypes(args: string[], restArgs: string[]): void {
1446
+ const checkOnly = args.includes("--check");
1447
+ const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
1448
+ const fileGlobs = args
1449
+ .filter((a) => a.startsWith("--files="))
1450
+ .map((a) => a.split("=")[1])
1451
+ .filter(Boolean);
1452
+
1453
+ console.log("Scanning project...\n");
1454
+ const result = generateTypes({
1455
+ basePath: process.cwd(),
1456
+ fileGlobs: fileGlobs.length > 0 ? fileGlobs : undefined,
1457
+ });
1458
+
1459
+ for (const l of result.linters) {
1460
+ console.log(
1461
+ ` ${l.linter}: ${String(l.rules.length)} enabled rules (via ${l.via})`,
1462
+ );
1463
+ }
1464
+ if (result.scripts.length > 0) {
1465
+ console.log(` npm scripts: ${String(result.scripts.length)}`);
1466
+ }
1467
+ console.log(` project files: ${String(result.files.length)}`);
1468
+
1469
+ const fullOut = resolve(process.cwd(), outPath);
1470
+
1471
+ if (checkOnly) {
1472
+ // --check: compare against existing file, exit 1 if stale
1473
+ if (!existsSync(fullOut)) {
1474
+ console.log(
1475
+ `\n✗ ${outPath} does not exist. Run \`vigiles generate-types\` to create it.`,
1476
+ );
1477
+ process.exit(1);
1478
+ }
1479
+ const existing = readFileSync(fullOut, "utf-8");
1480
+ // Normalize for formatter differences (trailing whitespace, blank lines)
1481
+ const normalize = (s: string): string =>
1482
+ s
1483
+ .split("\n")
1484
+ .map((l) => l.trimEnd())
1485
+ .join("\n")
1486
+ .replace(/\n{3,}/g, "\n\n")
1487
+ .trim();
1488
+ if (normalize(existing) === normalize(result.dts)) {
1489
+ console.log(`\n✓ ${outPath} is up to date`);
1490
+ } else {
1491
+ console.log(
1492
+ `\n✗ ${outPath} is stale. Run \`vigiles generate-types\` to update.`,
1493
+ );
1494
+ process.exit(1);
1495
+ }
1496
+ return;
1497
+ }
1498
+
1499
+ const outDir = fullOut.substring(0, fullOut.lastIndexOf("/"));
1500
+ if (!existsSync(outDir)) {
1501
+ mkdirSync(outDir, { recursive: true });
1502
+ }
1503
+ writeFileSync(fullOut, result.dts);
1504
+ console.log(`\n✓ Generated ${outPath}`);
1505
+ }
1506
+
1507
+ function printUsage(command: string | undefined): void {
1508
+ console.log("vigiles — compile typed specs to instruction files");
1509
+ console.log("");
1510
+ console.log("Commands:");
1511
+ console.log(
1512
+ " vigiles init [flags] Setup project (--target=X.md, --strict, --no-gha)",
1513
+ );
1514
+ console.log(" vigiles compile [files...] Compile .spec.ts → .md");
1515
+ console.log(
1516
+ " vigiles audit [files...] Verify, find gaps, suggest improvements",
1517
+ );
1518
+ console.log("");
1519
+ console.log("Examples:");
1520
+ console.log(
1521
+ " vigiles init Auto-detect project, create specs, wire CI",
1522
+ );
1523
+ console.log(" vigiles compile Compile all .spec.ts files");
1524
+ console.log(
1525
+ " vigiles audit Verify hashes + coverage + suggestions",
1526
+ );
1527
+ console.log("");
1528
+ console.log("Plumbing:");
1529
+ console.log(" vigiles generate-types [out] Emit .d.ts from project state");
1530
+ console.log(" vigiles generate-types --check Verify .d.ts is up to date");
1531
+ if (command && command !== "--help") {
1532
+ console.log(`\nUnknown command: "${command}"`);
1533
+ process.exit(1);
1534
+ }
1535
+ }
1536
+
1537
+ // ---------------------------------------------------------------------------
1538
+ // Main
1539
+ // ---------------------------------------------------------------------------
1540
+
1541
+ async function main(): Promise<void> {
1542
+ const args = process.argv.slice(2);
1543
+ const command = args[0];
1544
+ const restArgs = args.slice(1).filter((a) => !a.startsWith("--"));
1545
+ const config = loadConfig();
1546
+
1547
+ switch (command) {
1548
+ // --- Primary commands ---
1549
+
1550
+ case "init": {
1551
+ // Explicit --target bypasses the setup wizard and always creates a
1552
+ // bare spec, so `npx vigiles init --target=<file>` is a reliable
1553
+ // remediation for the require-spec validator. Bare `vigiles init`
1554
+ // still runs the full wizard (project detection + auto-targets).
1555
+ const hasTarget = args.some((a) => a.startsWith("--target="));
1556
+ if (hasTarget) {
1557
+ init(args.slice(1));
1558
+ } else {
1559
+ await setup(args);
1560
+ }
1561
+ break;
1562
+ }
1563
+
1564
+ case "compile": {
1565
+ const specs = restArgs.length > 0 ? restArgs : findSpecs();
1566
+ if (specs.length === 0) {
1567
+ console.log("No .spec.ts files found.");
1568
+ console.log("Run `vigiles init` to create one.");
1569
+ process.exit(0);
1570
+ }
1571
+ const valid = await compile(specs, config);
1572
+ console.log("");
1573
+ if (valid) {
1574
+ console.log("Compilation complete.");
1575
+ } else {
1576
+ console.log("Compilation complete with errors.");
1577
+ process.exit(1);
1578
+ }
1579
+ break;
1580
+ }
1581
+
1582
+ case "audit": {
1583
+ // audit = verify + discover + guidance count
1584
+ const flags = args.slice(1).filter((a) => a.startsWith("--"));
1585
+ const report = await audit(restArgs, flags, config);
1586
+ const exitCode = auditExitCode(report);
1587
+ // Skip GH annotations when --json or --summary is active —
1588
+ // those modes promise clean machine-readable stdout, and
1589
+ // ::error/::warning lines would contaminate the output for
1590
+ // callers parsing it as JSON.
1591
+ const structuredOutput =
1592
+ flags.includes("--json") || flags.includes("--summary");
1593
+ if (isGitHubActions() && !structuredOutput) {
1594
+ if (report.hashErrors > 0) {
1595
+ ghAnnotate(
1596
+ "error",
1597
+ `${String(report.hashErrors)} compiled file(s) with stale hash — run vigiles compile`,
1598
+ );
1599
+ }
1600
+ if (report.validationErrors > 0) {
1601
+ ghAnnotate(
1602
+ "error",
1603
+ `${String(report.validationErrors)} spec validation failure(s) — see audit output`,
1604
+ );
1605
+ }
1606
+ if (report.duplicatePairs > 0) {
1607
+ ghAnnotate(
1608
+ "warning",
1609
+ `${String(report.duplicatePairs)} near-duplicate rule pair(s) detected — consider merging`,
1610
+ );
1611
+ }
1612
+ }
1613
+ if (exitCode !== 0) {
1614
+ process.exit(exitCode);
1615
+ }
1616
+ break;
1617
+ }
1618
+
1619
+ // --- Plumbing ---
1620
+
1621
+ case "generate-types":
1622
+ handleGenerateTypes(args, restArgs);
1623
+ break;
1624
+
1625
+ default:
1626
+ printUsage(command);
1627
+ break;
1628
+ }
1629
+ }
1630
+
1631
+ void main();