vigiles 2.1.1 → 2.3.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 +127 -5
- package/dist/action-gate.d.ts +28 -0
- package/dist/action-gate.js +73 -0
- package/dist/cli.js +450 -75
- 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 +3 -0
- package/dist/compile.js +217 -26
- package/dist/eval.d.ts +87 -0
- package/dist/eval.js +208 -0
- package/dist/frontmatter.d.ts +24 -6
- package/dist/frontmatter.js +103 -30
- package/dist/generate-schema.js +10 -0
- package/dist/harness-assert.d.ts +68 -0
- package/dist/harness-assert.js +127 -0
- package/dist/harness-test.d.ts +45 -0
- package/dist/harness-test.js +138 -0
- package/dist/inline.d.ts +22 -4
- package/dist/inline.js +60 -13
- package/dist/jest.d.ts +9 -0
- package/dist/jest.js +23 -0
- package/dist/judge.d.ts +29 -0
- package/dist/judge.js +88 -0
- package/dist/linters.js +28 -0
- package/dist/mock-model.d.ts +31 -0
- package/dist/mock-model.js +189 -0
- package/dist/plugin-loader.d.ts +37 -0
- package/dist/plugin-loader.js +195 -0
- package/dist/refs.d.ts +44 -0
- package/dist/refs.js +144 -0
- package/dist/run-hook.d.ts +77 -0
- package/dist/run-hook.js +80 -0
- package/dist/run-scripts.d.ts +20 -0
- package/dist/run-scripts.js +70 -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 +90 -4
- package/dist/spec.js +29 -0
- package/dist/symbols.d.ts +30 -0
- package/dist/symbols.js +142 -0
- package/dist/vitest.d.mts +9 -0
- package/dist/vitest.mjs +22 -0
- package/package.json +45 -6
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,13 @@ const proofs_js_1 = require("./proofs.js");
|
|
|
21
21
|
const inline_js_1 = require("./inline.js");
|
|
22
22
|
const frontmatter_js_1 = require("./frontmatter.js");
|
|
23
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");
|
|
24
28
|
const linters_js_1 = require("./linters.js");
|
|
29
|
+
const harness_test_js_1 = require("./harness-test.js");
|
|
30
|
+
const run_scripts_js_1 = require("./run-scripts.js");
|
|
25
31
|
const integrity_js_1 = require("./integrity.js");
|
|
26
32
|
const coverage_js_1 = require("./coverage.js");
|
|
27
33
|
const orphans_js_1 = require("./orphans.js");
|
|
@@ -102,9 +108,84 @@ function printErrors(specFile, errors) {
|
|
|
102
108
|
// ---------------------------------------------------------------------------
|
|
103
109
|
// Commands
|
|
104
110
|
// ---------------------------------------------------------------------------
|
|
111
|
+
/** Compile a generator-skill spec from source → SKILL.md. Returns validity. */
|
|
112
|
+
function compileGeneratorSkillToFile(specPath, source) {
|
|
113
|
+
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
114
|
+
const { markdown, errors } = (0, compile_generator_js_1.compileGeneratorSkill)(source, {
|
|
115
|
+
basePath: process.cwd(),
|
|
116
|
+
specFile: specPath,
|
|
117
|
+
});
|
|
118
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
|
|
119
|
+
if (errors.length === 0) {
|
|
120
|
+
console.log(`\n✓ ${specPath} → ${outputPath} (generator skill)`);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
|
|
124
|
+
for (const e of errors)
|
|
125
|
+
console.log(` ${e.type}: ${e.message}`);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/** Compile a ClaudeSpec → its primary + any additional targets. */
|
|
129
|
+
function compileClaudeToFile(spec, specPath, config) {
|
|
130
|
+
const basePath = process.cwd();
|
|
131
|
+
const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
|
|
132
|
+
basePath,
|
|
133
|
+
specFile: specPath,
|
|
134
|
+
maxRules: config.maxRules,
|
|
135
|
+
maxTokens: config.maxTokens,
|
|
136
|
+
maxSectionLines: config.maxSectionLines,
|
|
137
|
+
catalogOnly: config.catalogOnly,
|
|
138
|
+
linters: config.linters,
|
|
139
|
+
});
|
|
140
|
+
const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
|
|
141
|
+
if (errors.length > 0) {
|
|
142
|
+
console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
|
|
143
|
+
printErrors(specPath, errors);
|
|
144
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
|
|
148
|
+
const outputNames = [primaryOutput];
|
|
149
|
+
for (const t of targets.slice(1)) {
|
|
150
|
+
const body = markdown
|
|
151
|
+
.replace(/^<!-- vigiles:[^\n]+\n\n?/, "")
|
|
152
|
+
.replace(/^# [^\n]+/, `# ${t}`);
|
|
153
|
+
const dir = primaryOutput.substring(0, primaryOutput.lastIndexOf("/") + 1);
|
|
154
|
+
const targetPath = dir + t;
|
|
155
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, targetPath), (0, compile_js_1.addHash)(body, specPath));
|
|
156
|
+
outputNames.push(targetPath);
|
|
157
|
+
}
|
|
158
|
+
const linterCount = linterResults.filter((r) => r.exists).length;
|
|
159
|
+
console.log(`\n✓ ${specPath} → ${outputNames.join(", ")}`);
|
|
160
|
+
console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/** Compile a declarative SkillSpec → SKILL.md. */
|
|
164
|
+
function compileSkillToFile(spec, specPath) {
|
|
165
|
+
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
166
|
+
const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
|
|
167
|
+
basePath: process.cwd(),
|
|
168
|
+
specFile: specPath,
|
|
169
|
+
});
|
|
170
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
|
|
171
|
+
if (errors.length === 0) {
|
|
172
|
+
console.log(`\n✓ ${specPath} → ${outputPath}`);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
|
|
176
|
+
printErrors(specPath, errors);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
105
179
|
async function compile(specPaths, config) {
|
|
106
180
|
let allValid = true;
|
|
107
181
|
for (const specPath of specPaths) {
|
|
182
|
+
// Generator skills can't be executed to markdown — compile from source.
|
|
183
|
+
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), specPath), "utf-8");
|
|
184
|
+
if (/\bgenSkill\s*\(/.test(source)) {
|
|
185
|
+
if (!compileGeneratorSkillToFile(specPath, source))
|
|
186
|
+
allValid = false;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
108
189
|
const spec = await loadSpec(specPath);
|
|
109
190
|
if (!spec) {
|
|
110
191
|
console.log(`\n✗ ${specPath} — failed to load`);
|
|
@@ -112,62 +193,13 @@ async function compile(specPaths, config) {
|
|
|
112
193
|
allValid = false;
|
|
113
194
|
continue;
|
|
114
195
|
}
|
|
115
|
-
const basePath = process.cwd();
|
|
116
196
|
if (spec._specType === "claude") {
|
|
117
|
-
|
|
118
|
-
basePath,
|
|
119
|
-
specFile: specPath,
|
|
120
|
-
maxRules: config.maxRules,
|
|
121
|
-
maxTokens: config.maxTokens,
|
|
122
|
-
maxSectionLines: config.maxSectionLines,
|
|
123
|
-
catalogOnly: config.catalogOnly,
|
|
124
|
-
linters: config.linters,
|
|
125
|
-
});
|
|
126
|
-
const linterCount = linterResults.filter((r) => r.exists).length;
|
|
127
|
-
const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
|
|
128
|
-
if (errors.length === 0) {
|
|
129
|
-
// Write primary target
|
|
130
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
|
|
131
|
-
const outputNames = [primaryOutput];
|
|
132
|
-
// Write additional targets with swapped heading + recomputed hash
|
|
133
|
-
for (const t of targets.slice(1)) {
|
|
134
|
-
// Strip hash, replace heading, recompute hash
|
|
135
|
-
const body = markdown
|
|
136
|
-
.replace(/^<!-- vigiles:[^\n]+\n\n?/, "")
|
|
137
|
-
.replace(/^# [^\n]+/, `# ${t}`);
|
|
138
|
-
const additional = (0, compile_js_1.addHash)(body, specPath);
|
|
139
|
-
const dir = primaryOutput.substring(0, primaryOutput.lastIndexOf("/") + 1);
|
|
140
|
-
const targetPath = dir + t;
|
|
141
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, targetPath), additional);
|
|
142
|
-
outputNames.push(targetPath);
|
|
143
|
-
}
|
|
144
|
-
console.log(`\n✓ ${specPath} → ${outputNames.join(", ")}`);
|
|
145
|
-
console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
|
|
149
|
-
printErrors(specPath, errors);
|
|
197
|
+
if (!compileClaudeToFile(spec, specPath, config))
|
|
150
198
|
allValid = false;
|
|
151
|
-
// Still write the file so the user can see partial output
|
|
152
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
|
|
153
|
-
}
|
|
154
199
|
}
|
|
155
200
|
else if (spec._specType === "skill") {
|
|
156
|
-
|
|
157
|
-
const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
|
|
158
|
-
basePath,
|
|
159
|
-
specFile: specPath,
|
|
160
|
-
});
|
|
161
|
-
if (errors.length === 0) {
|
|
162
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, outputPath), markdown);
|
|
163
|
-
console.log(`\n✓ ${specPath} → ${outputPath}`);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
|
|
167
|
-
printErrors(specPath, errors);
|
|
201
|
+
if (!compileSkillToFile(spec, specPath))
|
|
168
202
|
allValid = false;
|
|
169
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, outputPath), markdown);
|
|
170
|
-
}
|
|
171
203
|
}
|
|
172
204
|
}
|
|
173
205
|
return allValid;
|
|
@@ -363,6 +395,44 @@ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
|
|
|
363
395
|
log(`\n ${String(totalPairs)} duplicate pair(s) in ${String(specsWithDuplicates)} spec(s). Consider merging or rewording.`);
|
|
364
396
|
return { valid: false, pairCount: totalPairs };
|
|
365
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Verify the file-qualified symbol references (`path.ext#symbol`) in instruction
|
|
400
|
+
* files: the named file must exist and define the named symbol. The author
|
|
401
|
+
* names the file, so this is a *declared* reference — a broken one is an error.
|
|
402
|
+
* Each named file is parsed on demand; there is no project-wide index. Returns
|
|
403
|
+
* the count of broken references.
|
|
404
|
+
*/
|
|
405
|
+
function verifyMarkdownSymbols(files, silent) {
|
|
406
|
+
if (files.length === 0)
|
|
407
|
+
return 0;
|
|
408
|
+
const cwd = process.cwd();
|
|
409
|
+
let printedHeader = false;
|
|
410
|
+
let errors = 0;
|
|
411
|
+
for (const f of files) {
|
|
412
|
+
let markdown;
|
|
413
|
+
try {
|
|
414
|
+
markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, f), "utf-8");
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const broken = (0, refs_js_1.verifySymbolRefs)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, f)));
|
|
420
|
+
if (broken.length === 0)
|
|
421
|
+
continue;
|
|
422
|
+
if (!silent) {
|
|
423
|
+
if (!printedHeader) {
|
|
424
|
+
console.log("\nSymbol reference check:\n");
|
|
425
|
+
printedHeader = true;
|
|
426
|
+
}
|
|
427
|
+
for (const b of broken) {
|
|
428
|
+
console.log(` ✗ ${f}:${String(b.line)} ${b.reason}`);
|
|
429
|
+
ghAnnotate("error", b.reason, f, b.line);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
errors += broken.length;
|
|
433
|
+
}
|
|
434
|
+
return errors;
|
|
435
|
+
}
|
|
366
436
|
/** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
|
|
367
437
|
function auditExitCode(report) {
|
|
368
438
|
if (report.hashErrors > 0 ||
|
|
@@ -370,7 +440,8 @@ function auditExitCode(report) {
|
|
|
370
440
|
report.inlineErrors > 0 ||
|
|
371
441
|
report.frontmatterErrors > 0 ||
|
|
372
442
|
report.integrityErrors > 0 ||
|
|
373
|
-
report.coverageErrors > 0
|
|
443
|
+
report.coverageErrors > 0 ||
|
|
444
|
+
report.symbolRefErrors > 0)
|
|
374
445
|
return 2;
|
|
375
446
|
if (report.duplicatePairs > 0 ||
|
|
376
447
|
report.orphanCount > 0 ||
|
|
@@ -406,6 +477,35 @@ function verifyOneRule(rule, filePath, silent, linterOptions) {
|
|
|
406
477
|
log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
|
|
407
478
|
return true;
|
|
408
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Verify the `vigiles:file` / `vigiles:cmd` references a markdown file declares
|
|
482
|
+
* (inline comments or frontmatter lists), using the same engine spec mode uses:
|
|
483
|
+
* file paths via existsSync, npm scripts and script-runner commands via
|
|
484
|
+
* package.json / the filesystem. References resolve relative to the markdown
|
|
485
|
+
* file's own directory. Returns the number of stale references found.
|
|
486
|
+
*/
|
|
487
|
+
function verifyMarkdownRefs(files, commands, filePath, silent) {
|
|
488
|
+
const basePath = (0, node_path_1.dirname)((0, node_path_1.resolve)(process.cwd(), filePath));
|
|
489
|
+
let errorCount = 0;
|
|
490
|
+
const report = (err, line) => {
|
|
491
|
+
if (!silent) {
|
|
492
|
+
console.log(` ✗ line ${String(line)}: ${err.message}`);
|
|
493
|
+
ghAnnotate("error", err.message, filePath, line);
|
|
494
|
+
}
|
|
495
|
+
errorCount++;
|
|
496
|
+
};
|
|
497
|
+
for (const f of files) {
|
|
498
|
+
const err = (0, compile_js_1.validateFileRef)(f.path, basePath);
|
|
499
|
+
if (err)
|
|
500
|
+
report(err, f.line);
|
|
501
|
+
}
|
|
502
|
+
for (const c of commands) {
|
|
503
|
+
const err = (0, compile_js_1.validateCommandRef)(c.command, basePath);
|
|
504
|
+
if (err)
|
|
505
|
+
report(err, c.line);
|
|
506
|
+
}
|
|
507
|
+
return errorCount;
|
|
508
|
+
}
|
|
409
509
|
function verifyInlineRules(filePath, silent, linterOptions) {
|
|
410
510
|
const log = (msg) => {
|
|
411
511
|
if (!silent)
|
|
@@ -418,8 +518,11 @@ function verifyInlineRules(filePath, silent, linterOptions) {
|
|
|
418
518
|
catch {
|
|
419
519
|
return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
|
|
420
520
|
}
|
|
421
|
-
const { rules, errors: parseErrors } = (0, inline_js_1.parseInlineRules)(content);
|
|
422
|
-
if (rules.length === 0 &&
|
|
521
|
+
const { rules, files, commands, errors: parseErrors, } = (0, inline_js_1.parseInlineRules)(content);
|
|
522
|
+
if (rules.length === 0 &&
|
|
523
|
+
files.length === 0 &&
|
|
524
|
+
commands.length === 0 &&
|
|
525
|
+
parseErrors.length === 0) {
|
|
423
526
|
return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
|
|
424
527
|
}
|
|
425
528
|
let errorCount = 0;
|
|
@@ -435,10 +538,11 @@ function verifyInlineRules(filePath, silent, linterOptions) {
|
|
|
435
538
|
if (!verifyOneRule(rule, filePath, silent, linterOptions))
|
|
436
539
|
errorCount++;
|
|
437
540
|
}
|
|
541
|
+
errorCount += verifyMarkdownRefs(files, commands, filePath, silent);
|
|
438
542
|
return {
|
|
439
543
|
ok: errorCount === 0,
|
|
440
544
|
errorCount,
|
|
441
|
-
ruleCount: rules.length,
|
|
545
|
+
ruleCount: rules.length + files.length + commands.length,
|
|
442
546
|
ruleNames: rules.map((r) => r.linterRule),
|
|
443
547
|
};
|
|
444
548
|
}
|
|
@@ -460,9 +564,12 @@ function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
|
|
|
460
564
|
catch {
|
|
461
565
|
return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
|
|
462
566
|
}
|
|
463
|
-
const { rules: allRules, errors: parseErrors } = (0, frontmatter_js_1.parseFrontmatterRules)(content);
|
|
567
|
+
const { rules: allRules, files, commands, errors: parseErrors, } = (0, frontmatter_js_1.parseFrontmatterRules)(content);
|
|
464
568
|
const rules = allRules.filter((r) => !exclude.has(r.linterRule));
|
|
465
|
-
if (rules.length === 0 &&
|
|
569
|
+
if (rules.length === 0 &&
|
|
570
|
+
files.length === 0 &&
|
|
571
|
+
commands.length === 0 &&
|
|
572
|
+
parseErrors.length === 0) {
|
|
466
573
|
return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
|
|
467
574
|
}
|
|
468
575
|
let errorCount = 0;
|
|
@@ -478,10 +585,11 @@ function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
|
|
|
478
585
|
if (!verifyOneRule(rule, filePath, silent, linterOptions))
|
|
479
586
|
errorCount++;
|
|
480
587
|
}
|
|
588
|
+
errorCount += verifyMarkdownRefs(files, commands, filePath, silent);
|
|
481
589
|
return {
|
|
482
590
|
ok: errorCount === 0,
|
|
483
591
|
errorCount,
|
|
484
|
-
ruleCount: rules.length,
|
|
592
|
+
ruleCount: rules.length + files.length + commands.length,
|
|
485
593
|
ruleNames: rules.map((r) => r.linterRule),
|
|
486
594
|
};
|
|
487
595
|
}
|
|
@@ -620,6 +728,8 @@ async function audit(restArgs, flags, config) {
|
|
|
620
728
|
console.log(` ${line}`);
|
|
621
729
|
}
|
|
622
730
|
}
|
|
731
|
+
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
732
|
+
const symbolRefErrors = verifyMarkdownSymbols(files, silent);
|
|
623
733
|
const report = {
|
|
624
734
|
hashErrors: hashResult.hashErrors,
|
|
625
735
|
validationErrors: hashResult.validationErrors,
|
|
@@ -635,6 +745,7 @@ async function audit(restArgs, flags, config) {
|
|
|
635
745
|
coverageErrors,
|
|
636
746
|
orphanCount: orphanReport.orphans.length,
|
|
637
747
|
docRefErrors: docRefReport.errors.length,
|
|
748
|
+
symbolRefErrors,
|
|
638
749
|
files,
|
|
639
750
|
};
|
|
640
751
|
if (summary) {
|
|
@@ -662,6 +773,8 @@ function printAuditSummary(report) {
|
|
|
662
773
|
parts.push(`${String(report.orphanCount)} orphan docs`);
|
|
663
774
|
if (report.docRefErrors > 0)
|
|
664
775
|
parts.push(`${String(report.docRefErrors)} broken doc refs`);
|
|
776
|
+
if (report.symbolRefErrors > 0)
|
|
777
|
+
parts.push(`${String(report.symbolRefErrors)} broken symbol refs`);
|
|
665
778
|
const undocumented = report.coverageEnabled - report.coverageDocumented;
|
|
666
779
|
if (undocumented > 0)
|
|
667
780
|
parts.push(`${String(undocumented)} undocumented rules`);
|
|
@@ -1302,6 +1415,38 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
1302
1415
|
console.log(" Add to your markdown frontmatter:\n" +
|
|
1303
1416
|
` # yaml-language-server: $schema=./${outPath}`);
|
|
1304
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* `vigiles test` / `vigiles eval` — discover and run the two-tier harness
|
|
1420
|
+
* scripts (deterministic `*.harness.mjs` / real-model `*.eval.mjs`) as child
|
|
1421
|
+
* `node` processes, aggregating exit codes so they work as a CI command. See
|
|
1422
|
+
* src/run-scripts.ts.
|
|
1423
|
+
*
|
|
1424
|
+
* `vigiles test` skips clean when the `claude` CLI is absent (the deterministic
|
|
1425
|
+
* tier needs it, just like the node:test suite). `--trials=N` is forwarded to
|
|
1426
|
+
* eval scripts via the `VIGILES_TRIALS` env var.
|
|
1427
|
+
*/
|
|
1428
|
+
function handleRunScripts(kind, args, restArgs) {
|
|
1429
|
+
const cwd = process.cwd();
|
|
1430
|
+
const defaultGlob = kind === "test" ? "**/*.harness.mjs" : "**/*.eval.mjs";
|
|
1431
|
+
if (kind === "test" && !(0, harness_test_js_1.claudeAvailable)()) {
|
|
1432
|
+
console.log("vigiles test: `claude` CLI not found — skipping harness tests.");
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const files = (0, run_scripts_js_1.discoverScripts)(restArgs, defaultGlob, cwd);
|
|
1436
|
+
if (files.length === 0) {
|
|
1437
|
+
console.log(`No ${defaultGlob} files found.`);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
const trialsFlag = args.find((a) => a.startsWith("--trials="));
|
|
1441
|
+
const env = {};
|
|
1442
|
+
if (trialsFlag)
|
|
1443
|
+
env.VIGILES_TRIALS = trialsFlag.split("=")[1];
|
|
1444
|
+
console.log(`Running ${String(files.length)} ${kind} file(s):\n`);
|
|
1445
|
+
const results = (0, run_scripts_js_1.runScripts)(files, cwd, env);
|
|
1446
|
+
console.log("\n" + (0, run_scripts_js_1.formatScriptSummary)(results));
|
|
1447
|
+
if (results.some((r) => r.code !== 0))
|
|
1448
|
+
process.exit(1);
|
|
1449
|
+
}
|
|
1305
1450
|
function printUsage(command) {
|
|
1306
1451
|
console.log("vigiles — compile typed specs to instruction files");
|
|
1307
1452
|
console.log("");
|
|
@@ -1309,6 +1454,8 @@ function printUsage(command) {
|
|
|
1309
1454
|
console.log(" vigiles init [flags] Setup project (--target=X.md, --strict, --no-gha)");
|
|
1310
1455
|
console.log(" vigiles compile [files...] Compile .spec.ts → .md");
|
|
1311
1456
|
console.log(" vigiles audit [files...] Verify, find gaps, suggest improvements");
|
|
1457
|
+
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
1458
|
+
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N)");
|
|
1312
1459
|
console.log("");
|
|
1313
1460
|
console.log("Examples:");
|
|
1314
1461
|
console.log(" vigiles init Auto-detect project, create specs, wire CI");
|
|
@@ -1328,6 +1475,242 @@ function printUsage(command) {
|
|
|
1328
1475
|
// ---------------------------------------------------------------------------
|
|
1329
1476
|
// Main
|
|
1330
1477
|
// ---------------------------------------------------------------------------
|
|
1478
|
+
/**
|
|
1479
|
+
* Emit GitHub Actions annotations for an audit report. Skipped when --json or
|
|
1480
|
+
* --summary is active — those modes promise clean machine-readable stdout, and
|
|
1481
|
+
* ::error/::warning lines would contaminate output parsed as JSON.
|
|
1482
|
+
*/
|
|
1483
|
+
function annotateAuditForGitHub(report, flags) {
|
|
1484
|
+
const structuredOutput = flags.includes("--json") || flags.includes("--summary");
|
|
1485
|
+
if (!isGitHubActions() || structuredOutput)
|
|
1486
|
+
return;
|
|
1487
|
+
if (report.hashErrors > 0) {
|
|
1488
|
+
ghAnnotate("error", `${String(report.hashErrors)} compiled file(s) with stale hash — run vigiles compile`);
|
|
1489
|
+
}
|
|
1490
|
+
if (report.validationErrors > 0) {
|
|
1491
|
+
ghAnnotate("error", `${String(report.validationErrors)} spec validation failure(s) — see audit output`);
|
|
1492
|
+
}
|
|
1493
|
+
if (report.duplicatePairs > 0) {
|
|
1494
|
+
ghAnnotate("warning", `${String(report.duplicatePairs)} near-duplicate rule pair(s) detected — consider merging`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Run a compiled skill's deterministic gate ladder: execute each step gate in
|
|
1499
|
+
* order (short-circuiting on the first failure), then the result gate. This is
|
|
1500
|
+
* the v0 runtime — it enforces the `vigiles:gate`/`vigiles:result` markers a
|
|
1501
|
+
* compiled SKILL.md carries. It does not yet drive the model through the prose
|
|
1502
|
+
* steps (that needs a live harness).
|
|
1503
|
+
*/
|
|
1504
|
+
function runSkillCommand(target) {
|
|
1505
|
+
if (!target) {
|
|
1506
|
+
console.error("Usage: vigiles run-skill <SKILL.md>");
|
|
1507
|
+
process.exit(2);
|
|
1508
|
+
}
|
|
1509
|
+
const path = (0, node_path_1.resolve)(process.cwd(), target);
|
|
1510
|
+
if (!(0, node_fs_1.existsSync)(path)) {
|
|
1511
|
+
console.error(`Not found: ${target}`);
|
|
1512
|
+
process.exit(2);
|
|
1513
|
+
}
|
|
1514
|
+
const gates = (0, skill_runtime_js_1.parseSkillGates)((0, node_fs_1.readFileSync)(path, "utf-8"));
|
|
1515
|
+
if (gates.steps.length === 0 && !gates.result) {
|
|
1516
|
+
console.log(`No vigiles:gate / vigiles:result markers in ${target}.`);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
console.log(`Running gate ladder for ${target}:\n`);
|
|
1520
|
+
const report = (0, skill_runtime_js_1.runSkillGates)(gates, process.cwd());
|
|
1521
|
+
for (const r of report.results) {
|
|
1522
|
+
const label = r.at === "result" ? "result" : `step ${String(r.at)}`;
|
|
1523
|
+
console.log(` ${r.ok ? "✓" : "✗"} ${label} — ${(0, skill_runtime_js_1.gateLabel)(r.gate)}`);
|
|
1524
|
+
if (!r.ok && r.output) {
|
|
1525
|
+
console.log(r.output
|
|
1526
|
+
.split("\n")
|
|
1527
|
+
.map((l) => ` ${l}`)
|
|
1528
|
+
.join("\n"));
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (report.ok) {
|
|
1532
|
+
console.log("\n✓ All gates passed.");
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
const where = report.blockedAt === "result"
|
|
1536
|
+
? "the result gate"
|
|
1537
|
+
: `step ${String(report.blockedAt)}`;
|
|
1538
|
+
console.log(`\n✗ Blocked at ${where} — fix it before the skill is done.`);
|
|
1539
|
+
process.exit(2);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Stop-hook entrypoint: run the active skill's result gate and decide whether
|
|
1544
|
+
* the agent may stop. Exit 2 (with the reason on stderr) blocks the stop and
|
|
1545
|
+
* feeds the message back to the model; exit 0 allows it and clears the marker.
|
|
1546
|
+
*/
|
|
1547
|
+
function skillHookCommand() {
|
|
1548
|
+
const decision = (0, skill_runtime_js_1.evaluateStopHook)(process.cwd());
|
|
1549
|
+
if (decision.allow) {
|
|
1550
|
+
if (decision.message)
|
|
1551
|
+
console.log(decision.message);
|
|
1552
|
+
(0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
console.error(decision.message);
|
|
1556
|
+
process.exit(2);
|
|
1557
|
+
}
|
|
1558
|
+
/** Mark a skill active so the Stop hook enforces its result gate. */
|
|
1559
|
+
function skillStartCommand(target) {
|
|
1560
|
+
if (!target) {
|
|
1561
|
+
console.error("Usage: vigiles skill-start <SKILL.md>");
|
|
1562
|
+
process.exit(2);
|
|
1563
|
+
}
|
|
1564
|
+
(0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
|
|
1565
|
+
console.log(`Active skill: ${target}`);
|
|
1566
|
+
}
|
|
1567
|
+
/** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
|
|
1568
|
+
function handleSkillCommand(command, restArgs) {
|
|
1569
|
+
switch (command) {
|
|
1570
|
+
case "run-skill":
|
|
1571
|
+
runSkillCommand(restArgs[0]);
|
|
1572
|
+
return true;
|
|
1573
|
+
case "skill-start":
|
|
1574
|
+
skillStartCommand(restArgs[0]);
|
|
1575
|
+
return true;
|
|
1576
|
+
case "skill-done":
|
|
1577
|
+
(0, skill_runtime_js_1.clearActiveSkill)(process.cwd());
|
|
1578
|
+
return true;
|
|
1579
|
+
case "skill-hook":
|
|
1580
|
+
skillHookCommand();
|
|
1581
|
+
return true;
|
|
1582
|
+
case "action-hook":
|
|
1583
|
+
actionHookCommand();
|
|
1584
|
+
return true;
|
|
1585
|
+
case "refs":
|
|
1586
|
+
refsCommand(restArgs[0]);
|
|
1587
|
+
return true;
|
|
1588
|
+
case "refs-hook":
|
|
1589
|
+
refsHookCommand();
|
|
1590
|
+
return true;
|
|
1591
|
+
default:
|
|
1592
|
+
return false;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* PostToolUse-hook entrypoint for action gates. Reads the tool event on stdin,
|
|
1597
|
+
* runs the matching action gates from `.vigiles/action-gates.json`, and blocks
|
|
1598
|
+
* (exit 2 + reason on stderr) if any fails — plan-agnostic, so it works inside
|
|
1599
|
+
* dynamic workflows where there is no static step to attach a gate to.
|
|
1600
|
+
*/
|
|
1601
|
+
function actionHookCommand() {
|
|
1602
|
+
let raw = "";
|
|
1603
|
+
try {
|
|
1604
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
1605
|
+
}
|
|
1606
|
+
catch {
|
|
1607
|
+
/* no stdin */
|
|
1608
|
+
}
|
|
1609
|
+
let event = { tool: "" };
|
|
1610
|
+
try {
|
|
1611
|
+
const j = JSON.parse(raw);
|
|
1612
|
+
event = { tool: j.tool_name ?? "", input: j.tool_input };
|
|
1613
|
+
}
|
|
1614
|
+
catch {
|
|
1615
|
+
/* malformed input → no event, allow */
|
|
1616
|
+
}
|
|
1617
|
+
const decision = (0, action_gate_js_1.evaluateAction)(event, (0, action_gate_js_1.loadActionGates)(process.cwd()), process.cwd());
|
|
1618
|
+
if (!decision.allow) {
|
|
1619
|
+
console.error(decision.message);
|
|
1620
|
+
process.exit(2);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const INSTRUCTION_FILE = /^(SKILL|CLAUDE|AGENTS)\.md$/;
|
|
1624
|
+
function isInstructionFile(file) {
|
|
1625
|
+
return INSTRUCTION_FILE.test((0, node_path_1.basename)(file));
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Inspect an instruction file's symbol references: broken file-qualified refs
|
|
1629
|
+
* (`path.ext#symbol` whose file/symbol is wrong) and code-shaped references not
|
|
1630
|
+
* yet marked. Emits one line per finding via `log`; returns whether any issue
|
|
1631
|
+
* was found. `basePath` is the file's own directory (where paths resolve).
|
|
1632
|
+
*/
|
|
1633
|
+
function reportRefIssues(markdown, basePath, log) {
|
|
1634
|
+
const broken = (0, refs_js_1.verifySymbolRefs)(markdown, basePath);
|
|
1635
|
+
const unmarked = (0, refs_js_1.unmarkedCodeRefs)(markdown);
|
|
1636
|
+
for (const b of broken) {
|
|
1637
|
+
log(` ✗ line ${String(b.line)}: ${b.reason}`);
|
|
1638
|
+
}
|
|
1639
|
+
for (const u of unmarked) {
|
|
1640
|
+
const callee = u.text.replace(/\s*\([^)]*\)\s*$/, "");
|
|
1641
|
+
log(` ✗ line ${String(u.line)}: \`${u.text}\` is an unmarked code reference — ` +
|
|
1642
|
+
`mark it as \`vigiles:symbol path/to/file.ext#${callee}\` or add <!-- vigiles:ignore --> if it is prose`);
|
|
1643
|
+
}
|
|
1644
|
+
return broken.length > 0 || unmarked.length > 0;
|
|
1645
|
+
}
|
|
1646
|
+
/** `vigiles refs <file>` — check a file's symbol references (exit 2 on issues). */
|
|
1647
|
+
function refsCommand(target) {
|
|
1648
|
+
if (!target) {
|
|
1649
|
+
console.error("Usage: vigiles refs <instruction-file.md>");
|
|
1650
|
+
process.exit(2);
|
|
1651
|
+
}
|
|
1652
|
+
const cwd = process.cwd();
|
|
1653
|
+
let markdown;
|
|
1654
|
+
try {
|
|
1655
|
+
markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, target), "utf-8");
|
|
1656
|
+
}
|
|
1657
|
+
catch {
|
|
1658
|
+
console.error(`Cannot read ${target}`);
|
|
1659
|
+
process.exit(2);
|
|
1660
|
+
}
|
|
1661
|
+
const bad = reportRefIssues(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, target)), (m) => {
|
|
1662
|
+
console.log(m);
|
|
1663
|
+
});
|
|
1664
|
+
if (bad)
|
|
1665
|
+
process.exit(2);
|
|
1666
|
+
console.log(`✓ ${target}: all code references are marked and resolve.`);
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* PostToolUse-hook entrypoint: when the agent edits an instruction file, force
|
|
1670
|
+
* every code reference to carry a file-qualified mark (`path.ext#symbol`) and
|
|
1671
|
+
* verify the marked ones against the named file. Exit 2 (reason on stderr)
|
|
1672
|
+
* blocks the edit and feeds the fix back to the agent — the harness makes the
|
|
1673
|
+
* agent mark its references, at write time, with full context. `vigiles:ignore`
|
|
1674
|
+
* opts a prose span out.
|
|
1675
|
+
*/
|
|
1676
|
+
function refsHookCommand() {
|
|
1677
|
+
let raw = "";
|
|
1678
|
+
try {
|
|
1679
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
1680
|
+
}
|
|
1681
|
+
catch {
|
|
1682
|
+
/* no stdin */
|
|
1683
|
+
}
|
|
1684
|
+
let file = "";
|
|
1685
|
+
try {
|
|
1686
|
+
const j = JSON.parse(raw);
|
|
1687
|
+
file = j.tool_input?.file_path ?? "";
|
|
1688
|
+
}
|
|
1689
|
+
catch {
|
|
1690
|
+
/* malformed → nothing to do */
|
|
1691
|
+
}
|
|
1692
|
+
if (!file || !isInstructionFile(file))
|
|
1693
|
+
return;
|
|
1694
|
+
const cwd = process.cwd();
|
|
1695
|
+
const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
|
|
1696
|
+
let markdown;
|
|
1697
|
+
try {
|
|
1698
|
+
markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, file), "utf-8");
|
|
1699
|
+
}
|
|
1700
|
+
catch {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const lines = [];
|
|
1704
|
+
const bad = reportRefIssues(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)), (m) => {
|
|
1705
|
+
lines.push(m);
|
|
1706
|
+
});
|
|
1707
|
+
if (bad) {
|
|
1708
|
+
console.error(`vigiles: fix the code references in ${target}:`);
|
|
1709
|
+
for (const l of lines)
|
|
1710
|
+
console.error(l);
|
|
1711
|
+
process.exit(2);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1331
1714
|
async function main() {
|
|
1332
1715
|
const args = process.argv.slice(2);
|
|
1333
1716
|
const command = args[0];
|
|
@@ -1371,28 +1754,19 @@ async function main() {
|
|
|
1371
1754
|
// audit = verify + discover + guidance count
|
|
1372
1755
|
const flags = args.slice(1).filter((a) => a.startsWith("--"));
|
|
1373
1756
|
const report = await audit(restArgs, flags, config);
|
|
1757
|
+
annotateAuditForGitHub(report, flags);
|
|
1374
1758
|
const exitCode = auditExitCode(report);
|
|
1375
|
-
// Skip GH annotations when --json or --summary is active —
|
|
1376
|
-
// those modes promise clean machine-readable stdout, and
|
|
1377
|
-
// ::error/::warning lines would contaminate the output for
|
|
1378
|
-
// callers parsing it as JSON.
|
|
1379
|
-
const structuredOutput = flags.includes("--json") || flags.includes("--summary");
|
|
1380
|
-
if (isGitHubActions() && !structuredOutput) {
|
|
1381
|
-
if (report.hashErrors > 0) {
|
|
1382
|
-
ghAnnotate("error", `${String(report.hashErrors)} compiled file(s) with stale hash — run vigiles compile`);
|
|
1383
|
-
}
|
|
1384
|
-
if (report.validationErrors > 0) {
|
|
1385
|
-
ghAnnotate("error", `${String(report.validationErrors)} spec validation failure(s) — see audit output`);
|
|
1386
|
-
}
|
|
1387
|
-
if (report.duplicatePairs > 0) {
|
|
1388
|
-
ghAnnotate("warning", `${String(report.duplicatePairs)} near-duplicate rule pair(s) detected — consider merging`);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
1759
|
if (exitCode !== 0) {
|
|
1392
1760
|
process.exit(exitCode);
|
|
1393
1761
|
}
|
|
1394
1762
|
break;
|
|
1395
1763
|
}
|
|
1764
|
+
case "test":
|
|
1765
|
+
handleRunScripts("test", args, restArgs);
|
|
1766
|
+
break;
|
|
1767
|
+
case "eval":
|
|
1768
|
+
handleRunScripts("eval", args, restArgs);
|
|
1769
|
+
break;
|
|
1396
1770
|
// --- Plumbing ---
|
|
1397
1771
|
case "generate-types":
|
|
1398
1772
|
handleGenerateTypes(args, restArgs);
|
|
@@ -1401,7 +1775,8 @@ async function main() {
|
|
|
1401
1775
|
handleGenerateSchema(args, restArgs);
|
|
1402
1776
|
break;
|
|
1403
1777
|
default:
|
|
1404
|
-
|
|
1778
|
+
if (!handleSkillCommand(command, restArgs))
|
|
1779
|
+
printUsage(command);
|
|
1405
1780
|
break;
|
|
1406
1781
|
}
|
|
1407
1782
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ported community skills — proof that the generator form expresses the real,
|
|
3
|
+
* praised "agentic" skills (the deep tail the flat model can't hold), and that
|
|
4
|
+
* they are deterministically testable. Structural ports (not verbatim prose) of:
|
|
5
|
+
*
|
|
6
|
+
* - devonjones/pr-review-loop — the corpus stress test: a bounded round loop
|
|
7
|
+
* (7-round ceiling) with a quality-weighted exit, a per-finding for-each, and
|
|
8
|
+
* a nested bounded CI retry sub-loop.
|
|
9
|
+
* - test-driven-development (superpowers) — the red→green→refactor cycle.
|
|
10
|
+
* - subagent-driven-development (superpowers) — per-task for-each with two
|
|
11
|
+
* nested bounded review loops.
|
|
12
|
+
*
|
|
13
|
+
* These are exercised in community-skills.test.ts with a scripted model.
|
|
14
|
+
*/
|
|
15
|
+
import { type SkillProgram } from "./skill-driver.js";
|
|
16
|
+
/** COLLECT → BATCH → FIX rounds; ceiling 7; exit when no actionable feedback. */
|
|
17
|
+
export declare const prReviewLoop: SkillProgram;
|
|
18
|
+
/** Red → Green → Refactor, once per behavior until done. */
|
|
19
|
+
export declare const tdd: SkillProgram;
|
|
20
|
+
/** Per task: bounded spec review, implement, bounded quality review, gate. */
|
|
21
|
+
export declare const subagentDriven: SkillProgram;
|
|
22
|
+
//# sourceMappingURL=community-skills.d.ts.map
|