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.
- package/.claude/settings.json +46 -0
- package/.claude/settings.local.json +8 -0
- package/.claude-plugin/hooks/post-edit.sh +34 -0
- package/.claude-plugin/hooks/pre-edit.sh +40 -0
- package/.claude-plugin/hooks/session-start.sh +38 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +47 -0
- package/.github/workflows/ci.yml +81 -0
- package/.prettierignore +1 -0
- package/.vigiles/generated.d.ts +205 -0
- package/CLAUDE.md +95 -0
- package/CLAUDE.md.spec.ts +142 -0
- package/CONTRIBUTING.md +121 -0
- package/LICENSE +21 -0
- package/README.md +377 -0
- package/action.yml +25 -0
- package/dist/action.d.ts +7 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +180 -0
- package/dist/action.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1267 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +2 -0
- package/dist/cli.test.d.ts.map +1 -0
- package/dist/cli.test.js +650 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/compile.d.ts +101 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +503 -0
- package/dist/compile.js.map +1 -0
- package/dist/evolve.d.ts +132 -0
- package/dist/evolve.d.ts.map +1 -0
- package/dist/evolve.js +599 -0
- package/dist/evolve.js.map +1 -0
- package/dist/freshness.d.ts +67 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +244 -0
- package/dist/freshness.js.map +1 -0
- package/dist/freshness.test.d.ts +2 -0
- package/dist/freshness.test.d.ts.map +1 -0
- package/dist/freshness.test.js +356 -0
- package/dist/freshness.test.js.map +1 -0
- package/dist/generate-types.d.ts +34 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +381 -0
- package/dist/generate-types.js.map +1 -0
- package/dist/inline.d.ts +58 -0
- package/dist/inline.d.ts.map +1 -0
- package/dist/inline.js +142 -0
- package/dist/inline.js.map +1 -0
- package/dist/inline.test.d.ts +5 -0
- package/dist/inline.test.d.ts.map +1 -0
- package/dist/inline.test.js +152 -0
- package/dist/inline.test.js.map +1 -0
- package/dist/linters.d.ts +38 -0
- package/dist/linters.d.ts.map +1 -0
- package/dist/linters.js +588 -0
- package/dist/linters.js.map +1 -0
- package/dist/proofs.d.ts +272 -0
- package/dist/proofs.d.ts.map +1 -0
- package/dist/proofs.js +622 -0
- package/dist/proofs.js.map +1 -0
- package/dist/proofs.test.d.ts +9 -0
- package/dist/proofs.test.d.ts.map +1 -0
- package/dist/proofs.test.js +952 -0
- package/dist/proofs.test.js.map +1 -0
- package/dist/spec.d.ts +258 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +113 -0
- package/dist/spec.js.map +1 -0
- package/dist/spec.test.d.ts +2 -0
- package/dist/spec.test.d.ts.map +1 -0
- package/dist/spec.test.js +1222 -0
- package/dist/spec.test.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +10 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +286 -0
- package/dist/validate.js.map +1 -0
- package/dist/validate.test.d.ts +2 -0
- package/dist/validate.test.d.ts.map +1 -0
- package/dist/validate.test.js +531 -0
- package/dist/validate.test.js.map +1 -0
- package/docs/agent-setup.md +85 -0
- package/docs/agent-workflows.md +103 -0
- package/docs/comparison.md +71 -0
- package/docs/freshness.md +124 -0
- package/docs/inline-mode.md +119 -0
- package/docs/linter-support.md +166 -0
- package/docs/spec-format.md +194 -0
- package/eslint.config.mjs +79 -0
- package/examples/CLAUDE.md +54 -0
- package/examples/CLAUDE.md.spec.ts +65 -0
- package/examples/SKILL.md +50 -0
- package/examples/SKILL.md.spec.ts +57 -0
- package/fixtures/example-project/CLAUDE.md +11 -0
- package/fixtures/example-project/package.json +9 -0
- package/fixtures/example-project/src/index.ts +3 -0
- package/fixtures/example-project/src/utils.test.ts +2 -0
- package/fixtures/example-project/src/utils.ts +3 -0
- package/logo.png +0 -0
- package/package.json +42 -0
- package/research/adoption-strategy.md +111 -0
- package/research/agent-integration.md +145 -0
- package/research/ai-code-quality.md +197 -0
- package/research/code-search-for-agents.md +313 -0
- package/research/competitive-landscape.md +163 -0
- package/research/doc-freshness.md +516 -0
- package/research/executable-specs.md +368 -0
- package/research/feature-ideas.md +464 -0
- package/research/formal-proofs-for-agents.md +338 -0
- package/research/fp-for-agent-harness.md +150 -0
- package/research/fp-for-deterministic-ai.md +131 -0
- package/research/self-evolving-specs.md +298 -0
- package/schemas/claude-md-strict.yml +18 -0
- package/schemas/claude-md.yml +6 -0
- package/schemas/skill-strict.yml +12 -0
- package/schemas/skill.yml +5 -0
- package/skills/audit-feedback-loop/SKILL.md +76 -0
- package/skills/edit-spec/SKILL.md +131 -0
- package/skills/enforce-rules-format/SKILL.md +71 -0
- package/skills/generate-logo/SKILL.md +102 -0
- package/skills/generate-rule/SKILL.md +90 -0
- package/skills/linter-docs/clippy.md +241 -0
- package/skills/linter-docs/eslint.md +384 -0
- package/skills/linter-docs/pylint.md +288 -0
- package/skills/linter-docs/rubocop.md +277 -0
- package/skills/linter-docs/ruff.md +187 -0
- package/skills/linter-docs/stylelint.md +247 -0
- package/skills/migrate-to-spec/SKILL.md +124 -0
- package/skills/pr-to-lint-rule/SKILL.md +97 -0
- package/skills/strengthen/SKILL.md +168 -0
- package/src/action.ts +214 -0
- package/src/cli.test.ts +914 -0
- package/src/cli.ts +1631 -0
- package/src/compile.ts +691 -0
- package/src/evolve.ts +781 -0
- package/src/freshness.test.ts +449 -0
- package/src/freshness.ts +299 -0
- package/src/generate-types.ts +448 -0
- package/src/inline.test.ts +206 -0
- package/src/inline.ts +164 -0
- package/src/linters.ts +739 -0
- package/src/proofs.test.ts +1314 -0
- package/src/proofs.ts +849 -0
- package/src/spec.test.ts +1471 -0
- package/src/spec.ts +427 -0
- package/src/types.ts +117 -0
- package/src/validate.test.ts +701 -0
- package/src/validate.ts +381 -0
- 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
|