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