vigiles 2.1.1 → 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/dist/cli.js CHANGED
@@ -21,6 +21,10 @@ 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");
25
29
  const integrity_js_1 = require("./integrity.js");
26
30
  const coverage_js_1 = require("./coverage.js");
@@ -102,9 +106,84 @@ function printErrors(specFile, errors) {
102
106
  // ---------------------------------------------------------------------------
103
107
  // Commands
104
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
+ }
105
177
  async function compile(specPaths, config) {
106
178
  let allValid = true;
107
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
+ }
108
187
  const spec = await loadSpec(specPath);
109
188
  if (!spec) {
110
189
  console.log(`\n✗ ${specPath} — failed to load`);
@@ -112,62 +191,13 @@ async function compile(specPaths, config) {
112
191
  allValid = false;
113
192
  continue;
114
193
  }
115
- const basePath = process.cwd();
116
194
  if (spec._specType === "claude") {
117
- const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
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);
195
+ if (!compileClaudeToFile(spec, specPath, config))
150
196
  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
197
  }
155
198
  else if (spec._specType === "skill") {
156
- const outputPath = specPath.replace(/\.spec\.ts$/, "");
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);
199
+ if (!compileSkillToFile(spec, specPath))
168
200
  allValid = false;
169
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, outputPath), markdown);
170
- }
171
201
  }
172
202
  }
173
203
  return allValid;
@@ -363,6 +393,44 @@ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
363
393
  log(`\n ${String(totalPairs)} duplicate pair(s) in ${String(specsWithDuplicates)} spec(s). Consider merging or rewording.`);
364
394
  return { valid: false, pairCount: totalPairs };
365
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
+ }
366
434
  /** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
367
435
  function auditExitCode(report) {
368
436
  if (report.hashErrors > 0 ||
@@ -370,7 +438,8 @@ function auditExitCode(report) {
370
438
  report.inlineErrors > 0 ||
371
439
  report.frontmatterErrors > 0 ||
372
440
  report.integrityErrors > 0 ||
373
- report.coverageErrors > 0)
441
+ report.coverageErrors > 0 ||
442
+ report.symbolRefErrors > 0)
374
443
  return 2;
375
444
  if (report.duplicatePairs > 0 ||
376
445
  report.orphanCount > 0 ||
@@ -406,6 +475,35 @@ function verifyOneRule(rule, filePath, silent, linterOptions) {
406
475
  log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
407
476
  return true;
408
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.
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
+ }
409
507
  function verifyInlineRules(filePath, silent, linterOptions) {
410
508
  const log = (msg) => {
411
509
  if (!silent)
@@ -418,8 +516,11 @@ function verifyInlineRules(filePath, silent, linterOptions) {
418
516
  catch {
419
517
  return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
420
518
  }
421
- const { rules, errors: parseErrors } = (0, inline_js_1.parseInlineRules)(content);
422
- if (rules.length === 0 && parseErrors.length === 0) {
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) {
423
524
  return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
424
525
  }
425
526
  let errorCount = 0;
@@ -435,10 +536,11 @@ function verifyInlineRules(filePath, silent, linterOptions) {
435
536
  if (!verifyOneRule(rule, filePath, silent, linterOptions))
436
537
  errorCount++;
437
538
  }
539
+ errorCount += verifyMarkdownRefs(files, commands, filePath, silent);
438
540
  return {
439
541
  ok: errorCount === 0,
440
542
  errorCount,
441
- ruleCount: rules.length,
543
+ ruleCount: rules.length + files.length + commands.length,
442
544
  ruleNames: rules.map((r) => r.linterRule),
443
545
  };
444
546
  }
@@ -460,9 +562,12 @@ function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
460
562
  catch {
461
563
  return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
462
564
  }
463
- const { rules: allRules, errors: parseErrors } = (0, frontmatter_js_1.parseFrontmatterRules)(content);
565
+ const { rules: allRules, files, commands, errors: parseErrors, } = (0, frontmatter_js_1.parseFrontmatterRules)(content);
464
566
  const rules = allRules.filter((r) => !exclude.has(r.linterRule));
465
- if (rules.length === 0 && parseErrors.length === 0) {
567
+ if (rules.length === 0 &&
568
+ files.length === 0 &&
569
+ commands.length === 0 &&
570
+ parseErrors.length === 0) {
466
571
  return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
467
572
  }
468
573
  let errorCount = 0;
@@ -478,10 +583,11 @@ function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
478
583
  if (!verifyOneRule(rule, filePath, silent, linterOptions))
479
584
  errorCount++;
480
585
  }
586
+ errorCount += verifyMarkdownRefs(files, commands, filePath, silent);
481
587
  return {
482
588
  ok: errorCount === 0,
483
589
  errorCount,
484
- ruleCount: rules.length,
590
+ ruleCount: rules.length + files.length + commands.length,
485
591
  ruleNames: rules.map((r) => r.linterRule),
486
592
  };
487
593
  }
@@ -620,6 +726,8 @@ async function audit(restArgs, flags, config) {
620
726
  console.log(` ${line}`);
621
727
  }
622
728
  }
729
+ // 9. Verify code-shaped symbol references live (see src/refs.ts).
730
+ const symbolRefErrors = verifyMarkdownSymbols(files, silent);
623
731
  const report = {
624
732
  hashErrors: hashResult.hashErrors,
625
733
  validationErrors: hashResult.validationErrors,
@@ -635,6 +743,7 @@ async function audit(restArgs, flags, config) {
635
743
  coverageErrors,
636
744
  orphanCount: orphanReport.orphans.length,
637
745
  docRefErrors: docRefReport.errors.length,
746
+ symbolRefErrors,
638
747
  files,
639
748
  };
640
749
  if (summary) {
@@ -662,6 +771,8 @@ function printAuditSummary(report) {
662
771
  parts.push(`${String(report.orphanCount)} orphan docs`);
663
772
  if (report.docRefErrors > 0)
664
773
  parts.push(`${String(report.docRefErrors)} broken doc refs`);
774
+ if (report.symbolRefErrors > 0)
775
+ parts.push(`${String(report.symbolRefErrors)} broken symbol refs`);
665
776
  const undocumented = report.coverageEnabled - report.coverageDocumented;
666
777
  if (undocumented > 0)
667
778
  parts.push(`${String(undocumented)} undocumented rules`);
@@ -1328,6 +1439,242 @@ function printUsage(command) {
1328
1439
  // ---------------------------------------------------------------------------
1329
1440
  // Main
1330
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
+ }
1331
1678
  async function main() {
1332
1679
  const args = process.argv.slice(2);
1333
1680
  const command = args[0];
@@ -1371,23 +1718,8 @@ async function main() {
1371
1718
  // audit = verify + discover + guidance count
1372
1719
  const flags = args.slice(1).filter((a) => a.startsWith("--"));
1373
1720
  const report = await audit(restArgs, flags, config);
1721
+ annotateAuditForGitHub(report, flags);
1374
1722
  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
1723
  if (exitCode !== 0) {
1392
1724
  process.exit(exitCode);
1393
1725
  }
@@ -1401,7 +1733,8 @@ async function main() {
1401
1733
  handleGenerateSchema(args, restArgs);
1402
1734
  break;
1403
1735
  default:
1404
- printUsage(command);
1736
+ if (!handleSkillCommand(command, restArgs))
1737
+ printUsage(command);
1405
1738
  break;
1406
1739
  }
1407
1740
  }
@@ -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