vigiles 38.2.0 → 39.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/adapters/claude-code/event-capability.js +24 -8
  2. package/dist/adapters/claude-code/hook-protocol.js +2 -1
  3. package/dist/adapters/codex/hook-protocol.js +2 -1
  4. package/dist/cli-flag-check.d.ts +36 -0
  5. package/dist/cli-flag-check.js +72 -1
  6. package/dist/cli-main.d.ts +2 -2
  7. package/dist/cli-main.js +548 -379
  8. package/dist/core/bash-effects.d.ts +0 -57
  9. package/dist/core/bash-effects.js +0 -147
  10. package/dist/core/compile-generator.d.ts +6 -6
  11. package/dist/core/compile-generator.js +4 -4
  12. package/dist/core/compile.d.ts +72 -8
  13. package/dist/core/compile.js +276 -133
  14. package/dist/core/config-schema.d.ts +40 -10
  15. package/dist/core/config-schema.js +139 -16
  16. package/dist/core/coverage.d.ts +16 -0
  17. package/dist/core/coverage.js +22 -0
  18. package/dist/core/doc-refs.d.ts +3 -2
  19. package/dist/core/doc-refs.js +2 -2
  20. package/dist/core/enforce-marker.d.ts +28 -0
  21. package/dist/core/enforce-marker.js +34 -0
  22. package/dist/core/eslint-inventory.d.ts +96 -0
  23. package/dist/core/eslint-inventory.js +251 -0
  24. package/dist/core/event-capability.d.ts +5 -1
  25. package/dist/core/frame.d.ts +43 -2
  26. package/dist/core/frame.js +85 -2
  27. package/dist/core/generate-schema.d.ts +3 -2
  28. package/dist/core/generate-schema.js +2 -2
  29. package/dist/core/generate-types.d.ts +11 -2
  30. package/dist/core/generate-types.js +12 -5
  31. package/dist/core/hook-command.d.ts +172 -0
  32. package/dist/core/hook-command.js +277 -0
  33. package/dist/core/hook-delivery.d.ts +133 -0
  34. package/dist/core/hook-delivery.js +150 -0
  35. package/dist/core/hook-paths.d.ts +14 -0
  36. package/dist/core/hook-paths.js +22 -0
  37. package/dist/core/hook-program.d.ts +4 -4
  38. package/dist/core/hook-program.js +13 -25
  39. package/dist/core/hook-state.d.ts +1 -1
  40. package/dist/core/hook-state.js +1 -1
  41. package/dist/core/instruction-remedies.d.ts +35 -0
  42. package/dist/core/instruction-remedies.js +70 -1
  43. package/dist/core/integrity-header.d.ts +54 -0
  44. package/dist/core/integrity-header.js +90 -0
  45. package/dist/core/integrity.d.ts +1 -25
  46. package/dist/core/integrity.js +16 -74
  47. package/dist/core/linter-adapter.d.ts +27 -3
  48. package/dist/core/linters.d.ts +8 -1
  49. package/dist/core/linters.js +116 -81
  50. package/dist/core/orphans.d.ts +3 -5
  51. package/dist/core/orphans.js +9 -10
  52. package/dist/core/presets.d.ts +19 -7
  53. package/dist/core/presets.js +19 -6
  54. package/dist/core/rule-catalog.d.ts +11 -12
  55. package/dist/core/rule-catalog.js +25 -94
  56. package/dist/core/rule-meta.d.ts +12 -0
  57. package/dist/core/rule-meta.js +47 -10
  58. package/dist/core/source-refs.d.ts +3 -23
  59. package/dist/core/source-refs.js +3 -27
  60. package/dist/core/test-utils.d.ts +13 -0
  61. package/dist/core/test-utils.js +40 -1
  62. package/dist/core/types.d.ts +18 -26
  63. package/dist/core/validate.d.ts +1 -2
  64. package/dist/core/validate.js +60 -47
  65. package/dist/coverage-evidence.js +2 -3
  66. package/dist/deprecated-key-files.d.ts +6 -0
  67. package/dist/deprecated-key-files.js +7 -6
  68. package/dist/exclude.d.ts +3 -1
  69. package/dist/exclude.js +9 -33
  70. package/dist/harness-assert.d.ts +18 -0
  71. package/dist/harness-assert.js +38 -0
  72. package/dist/hook-check-install.js +15 -6
  73. package/dist/hook-check.d.ts +1 -3
  74. package/dist/hook-check.js +3 -5
  75. package/dist/hook-install.d.ts +7 -41
  76. package/dist/hook-install.js +29 -96
  77. package/dist/hook-runtime.js +2 -1
  78. package/dist/load-hook.d.ts +3 -2
  79. package/dist/load-hook.js +28 -6
  80. package/dist/overrides.d.ts +25 -0
  81. package/dist/overrides.js +51 -0
  82. package/dist/plugin-loader.d.ts +23 -0
  83. package/dist/plugin-loader.js +42 -12
  84. package/dist/root-globs.d.ts +9 -0
  85. package/dist/root-globs.js +48 -0
  86. package/dist/rule-routing.js +6 -3
  87. package/dist/run-hook.d.ts +26 -2
  88. package/dist/run-hook.js +48 -12
  89. package/dist/scan-core.d.ts +96 -20
  90. package/dist/scan-core.js +171 -176
  91. package/dist/scan-files.d.ts +0 -8
  92. package/dist/scan-files.js +78 -20
  93. package/dist/scan.d.ts +12 -12
  94. package/dist/scan.js +33 -10
  95. package/dist/setup-plan.d.ts +15 -9
  96. package/dist/setup-plan.js +26 -18
  97. package/dist/test-coverage.d.ts +3 -8
  98. package/dist/test-coverage.js +6 -6
  99. package/dist/test.d.ts +1 -1
  100. package/dist/vigilesrc.schema.json +1329 -118
  101. package/package.json +1 -1
  102. package/skills/adopt-spec/SKILL.md +2 -1
@@ -15,6 +15,7 @@ exports.validateCommandRef = validateCommandRef;
15
15
  exports.validateSymbolRef = validateSymbolRef;
16
16
  exports.validateDirRef = validateDirRef;
17
17
  exports.validateGlobRef = validateGlobRef;
18
+ exports.unverifiedAdvice = unverifiedAdvice;
18
19
  exports.compileClaude = compileClaude;
19
20
  exports.compileSkill = compileSkill;
20
21
  exports.compileAgent = compileAgent;
@@ -36,7 +37,9 @@ const markdown_js_1 = require("./markdown.js");
36
37
  const symbols_js_1 = require("./symbols.js");
37
38
  const skill_normalize_js_1 = require("./skill-normalize.js");
38
39
  const keyfiles_js_1 = require("./keyfiles.js");
40
+ const frame_js_1 = require("./frame.js");
39
41
  const linters_js_1 = require("./linters.js");
42
+ const enforce_marker_js_1 = require("./enforce-marker.js");
40
43
  const tool_contract_js_1 = require("./tool-contract.js");
41
44
  const effects_js_1 = require("./effects.js");
42
45
  const instruction_remedies_js_1 = require("./instruction-remedies.js");
@@ -274,7 +277,18 @@ async function validateOneRef(r, basePath) {
274
277
  return validateRefs(r.body, basePath);
275
278
  }
276
279
  }
277
- function renderFragment(fragment) {
280
+ /** The repo-relative path a spec compiles to: the spec's own name without `.spec.ts`. */
281
+ function compiledPathOf(specFile) {
282
+ return specFile.endsWith(".spec.ts")
283
+ ? specFile.slice(0, -".spec.ts".length)
284
+ : specFile;
285
+ }
286
+ /**
287
+ * Render one fragment into the compiled file at `out` (repo-relative). A link
288
+ * resolves from the file it is written into, so every path rendered as a link
289
+ * goes through {@link linkFrom}.
290
+ */
291
+ function renderFragment(fragment, out) {
278
292
  if (typeof fragment === "string")
279
293
  return fragment;
280
294
  switch (fragment._ref) {
@@ -283,7 +297,7 @@ function renderFragment(fragment) {
283
297
  case "cmd":
284
298
  return `\`${fragment.command}\``;
285
299
  case "skill":
286
- return `[${(0, node_path_1.basename)((0, node_path_1.dirname)(fragment.path))}](${fragment.path})`;
300
+ return `[${(0, node_path_1.basename)((0, node_path_1.dirname)(fragment.path))}](${(0, frame_js_1.linkFrom)(out, fragment.path)})`;
287
301
  case "symbol":
288
302
  return `\`vigiles:symbol ${fragment.file}#${fragment.symbol}\``;
289
303
  case "dir":
@@ -291,20 +305,61 @@ function renderFragment(fragment) {
291
305
  case "glob":
292
306
  return `\`${fragment.pattern}\``;
293
307
  case "effect": {
294
- const inner = fragment.body.map(renderFragment).join("").trim();
308
+ const inner = renderBody(fragment.body, out).trim();
295
309
  return `\n<!-- vigiles:effect -->\n\n${inner}\n\n<!-- /vigiles:effect -->\n`;
296
310
  }
297
311
  default:
298
312
  return (0, hash_js_1.assertNever)(fragment);
299
313
  }
300
314
  }
301
- // ---------------------------------------------------------------------------
302
- // Compile CLAUDE.md spec → markdown
303
- // ---------------------------------------------------------------------------
304
- /** `[Details](path#anchor)` for a rule's long text. */
305
- function detailsLink(details) {
315
+ /** The verdict a linter check result supports. */
316
+ function verdictFromResult(result) {
317
+ if (!result.exists)
318
+ return { kind: "unverified", reason: "not-found" };
319
+ switch (result.enabled) {
320
+ case "enabled":
321
+ return { kind: "verified" };
322
+ case "disabled":
323
+ return { kind: "unverified", reason: "disabled" };
324
+ case "unconfigured":
325
+ return { kind: "unverified", reason: "not-configured" };
326
+ case "unknown":
327
+ return { kind: "unverified", reason: "enabled-unknown" };
328
+ default:
329
+ return (0, hash_js_1.assertNever)(result.enabled);
330
+ }
331
+ }
332
+ /**
333
+ * The sentence the compile prints for an unverified reference: what happened
334
+ * and the one thing that turns it into "Enforced by".
335
+ */
336
+ function unverifiedAdvice(ref, reason) {
337
+ const linter = (0, linters_js_1.extractLinterName)(ref);
338
+ switch (reason) {
339
+ case "verify-false":
340
+ return `${ref}: not checked (\`verify: false\`). Remove the option once the rule resolves, so a typo or a deleted rule fails the compile.`;
341
+ case "verification-off":
342
+ return `${ref}: not checked (\`verifyLinters: false\`).`;
343
+ case "linter-off":
344
+ return `${ref}: not checked (verification is off for ${linter}).`;
345
+ case "config-not-read":
346
+ return `${ref}: the rule exists; catalog-only mode did not read whether your config turns it on.`;
347
+ case "not-configured":
348
+ return `${ref}: the rule exists but no block of your ${linter} config sets it, so it runs nowhere. Set it in the config, or make this a guidance() rule.`;
349
+ case "enabled-unknown":
350
+ return `${ref}: the rule exists; vigiles could not read whether your ${linter} config turns it on.`;
351
+ case "not-found":
352
+ return `${ref}: the rule was not found.`;
353
+ case "disabled":
354
+ return `${ref}: the rule is turned off in your ${linter} config.`;
355
+ default:
356
+ return (0, hash_js_1.assertNever)(reason);
357
+ }
358
+ }
359
+ /** `[Details](path#anchor)` for a rule's long text, linked from `out`. */
360
+ function detailsLink(details, out) {
306
361
  const anchor = details.anchor === undefined ? "" : `#${details.anchor}`;
307
- return `[Details](${details.file.path}${anchor})`;
362
+ return `[Details](${(0, frame_js_1.linkFrom)(out, details.file.path)}${anchor})`;
308
363
  }
309
364
  /**
310
365
  * The first sentence of `text`: up to the first `.`, `!` or `?` that closes a
@@ -319,8 +374,8 @@ function firstSentence(text) {
319
374
  return m ? m[0] : text.trim();
320
375
  }
321
376
  /** The `**Why:**` line(s) of an enforce rule under its render policy. */
322
- function enforceWhy(rule) {
323
- const link = rule.details ? ` ${detailsLink(rule.details)}` : "";
377
+ function enforceWhy(rule, out) {
378
+ const link = rule.details ? ` ${detailsLink(rule.details, out)}` : "";
324
379
  const render = rule.render ?? "full";
325
380
  switch (render) {
326
381
  case "full":
@@ -328,25 +383,29 @@ function enforceWhy(rule) {
328
383
  case "line":
329
384
  return [`**Why:** ${firstSentence(rule.why)}${link}`];
330
385
  case "id":
331
- return rule.details ? [detailsLink(rule.details)] : [];
386
+ return rule.details ? [detailsLink(rule.details, out)] : [];
332
387
  default:
333
388
  return (0, hash_js_1.assertNever)(render);
334
389
  }
335
390
  }
336
- function compileRule(id, rule) {
391
+ function compileRule(id, rule, out, verdict) {
337
392
  const title = id
338
393
  .replace(/[-_]/g, " ")
339
394
  .replace(/\b\w/g, (c) => c.toUpperCase());
340
395
  switch (rule._kind) {
341
- case "enforce":
396
+ case "enforce": {
397
+ // "Enforced by" only for a VERIFIED reference (#351); no verdict = not
398
+ // verified, so the default cannot print the promise by omission.
399
+ const marker = verdict?.kind === "verified" ? enforce_marker_js_1.ENFORCED_MARKER : enforce_marker_js_1.CLAIMED_MARKER;
342
400
  return [
343
401
  `### ${title}`,
344
402
  "",
345
- `**Enforced by:** \`${rule.linterRule}\``,
346
- ...enforceWhy(rule),
403
+ `${marker} \`${rule.linterRule}\``,
404
+ ...enforceWhy(rule, out),
347
405
  ].join("\n");
406
+ }
348
407
  case "guidance": {
349
- const link = rule.details ? ` ${detailsLink(rule.details)}` : "";
408
+ const link = rule.details ? ` ${detailsLink(rule.details, out)}` : "";
350
409
  return [
351
410
  `### ${title}`,
352
411
  "",
@@ -540,10 +599,22 @@ const RESERVED_SECTION_KEYS = new Set([
540
599
  "key_files",
541
600
  "rules",
542
601
  ]);
543
- async function compileSectionsSection(spec, basePath, maxSectionLines) {
602
+ /**
603
+ * The prose sections as they read in the file at `out` (repo-relative): a
604
+ * fragment's links resolve from that file's directory, so each target of a
605
+ * multi-target spec renders its own.
606
+ */
607
+ function renderSections(spec, out) {
608
+ return Object.entries(spec.sections ?? {}).map(([name, content]) => {
609
+ const heading = name.charAt(0).toUpperCase() + name.slice(1);
610
+ const text = typeof content === "string" ? content : renderBody(content, out);
611
+ return `## ${heading}\n\n${text.trim()}`;
612
+ });
613
+ }
614
+ /** The prose sections' errors, measured on their rendering at `out`. */
615
+ async function compileSectionsSection(spec, basePath, out, maxSectionLines) {
544
616
  if (!spec.sections)
545
- return { lines: [], errors: [] };
546
- const lines = [];
617
+ return [];
547
618
  const errors = [];
548
619
  for (const [name, content] of Object.entries(spec.sections)) {
549
620
  // #4: reject reserved section keys that clash with structured fields
@@ -553,19 +624,16 @@ async function compileSectionsSection(spec, basePath, maxSectionLines) {
553
624
  message: `Section key "${name}" is reserved — use the dedicated \`${name}\` field on the spec instead.`,
554
625
  });
555
626
  }
556
- const heading = name.charAt(0).toUpperCase() + name.slice(1);
557
627
  if (typeof content === "string") {
558
628
  errors.push(...validateSectionContent(name, content, maxSectionLines));
559
- lines.push(`## ${heading}\n\n${content.trim()}`);
560
629
  }
561
630
  else {
562
631
  errors.push(...(await validateRefs(content, basePath)));
563
- const rendered = content.map(renderFragment).join("");
632
+ const rendered = renderBody(content, out);
564
633
  errors.push(...validateSectionContent(name, rendered, maxSectionLines));
565
- lines.push(`## ${heading}\n\n${rendered.trim()}`);
566
634
  }
567
635
  }
568
- return { lines, errors };
636
+ return errors;
569
637
  }
570
638
  function compileKeyFilesSection(spec, basePath) {
571
639
  // eslint-disable-next-line @typescript-eslint/no-deprecated -- keyFiles is deprecated, not removed: it still compiles until the next major
@@ -602,21 +670,21 @@ function compileCommandsSection(spec, basePath) {
602
670
  return { lines: [lines.join("\n")], errors };
603
671
  }
604
672
  /**
605
- * Determine if a rule should be verified, checking three levels:
673
+ * Why a rule is NOT verified at all, checking three levels, or null to verify:
606
674
  * 1. Per-rule: enforce("...", "...", { verify: false })
607
675
  * 2. Global: options.verifyLinters === false
608
676
  * 3. Per-linter: options.linterModes[linterName] === false
609
677
  */
610
- function shouldVerifyRule(rule, options) {
678
+ function skipVerificationReason(rule, options) {
611
679
  if (!rule.verify)
612
- return false;
680
+ return "verify-false";
613
681
  if (options.verifyLinters === false)
614
- return false;
682
+ return "verification-off";
615
683
  const linterName = (0, linters_js_1.extractLinterName)(rule.linterRule);
616
684
  const linterMode = options.linterModes?.[linterName];
617
685
  if (linterMode === false)
618
- return false;
619
- return true;
686
+ return "linter-off";
687
+ return null;
620
688
  }
621
689
  /**
622
690
  * A rule's `details` must land somewhere: the file exists (the same check as
@@ -641,58 +709,123 @@ function validateRuleDetails(details, basePath) {
641
709
  },
642
710
  ];
643
711
  }
712
+ /** The compile error a checked reference earns, if any. */
713
+ function enforceError(ref, result) {
714
+ if (!result.exists) {
715
+ return {
716
+ type: "invalid-rule",
717
+ message: result.error ?? `Rule "${ref}" not found in ${result.linter}`,
718
+ path: ref,
719
+ };
720
+ }
721
+ if (result.enabled === "disabled") {
722
+ return {
723
+ type: "invalid-rule",
724
+ message: `Rule "${result.rule}" exists but is disabled in ${result.linter} config`,
725
+ path: ref,
726
+ };
727
+ }
728
+ return undefined;
729
+ }
730
+ /** Verify one `enforce()` reference: its verdict, check result and any error. */
731
+ function verifyEnforce(rule, basePath, options) {
732
+ const skipped = skipVerificationReason(rule, options);
733
+ if (skipped !== null) {
734
+ return { verdict: { kind: "unverified", reason: skipped } };
735
+ }
736
+ const linterMode = options.linterModes?.[(0, linters_js_1.extractLinterName)(rule.linterRule)];
737
+ const catalogOnly = options.catalogOnly === true || linterMode === "catalog-only";
738
+ const result = (0, linters_js_1.checkLinterRule)(rule.linterRule, basePath, {
739
+ catalogOnly,
740
+ linters: options.linters,
741
+ });
742
+ const verdict = verdictFromResult(result);
743
+ // catalog-only never reads the config, so "unknown" there has a known cause.
744
+ const explained = catalogOnly &&
745
+ verdict.kind === "unverified" &&
746
+ verdict.reason === "enabled-unknown"
747
+ ? { kind: "unverified", reason: "config-not-read" }
748
+ : verdict;
749
+ return {
750
+ verdict: explained,
751
+ result,
752
+ error: enforceError(rule.linterRule, result),
753
+ };
754
+ }
644
755
  function compileRulesSection(spec, basePath, options) {
645
756
  const ruleEntries = Object.entries(spec.rules);
646
757
  if (ruleEntries.length === 0) {
647
- return { lines: [], errors: [], linterResults: [] };
648
- }
649
- const ruleLines = ["## Rules"];
650
- const errors = [];
651
- const linterResults = [];
652
- for (const [id, rule] of ruleEntries) {
653
- ruleLines.push("");
654
- ruleLines.push(compileRule(id, rule));
655
- if (rule._kind === "enforce") {
656
- const shouldVerify = shouldVerifyRule(rule, options);
657
- if (!shouldVerify)
658
- continue;
659
- const linterName = (0, linters_js_1.extractLinterName)(rule.linterRule);
660
- const linterMode = options.linterModes?.[linterName];
661
- const catalogOnly = options.catalogOnly || linterMode === "catalog-only";
662
- const result = (0, linters_js_1.checkLinterRule)(rule.linterRule, basePath, {
663
- catalogOnly,
664
- linters: options.linters,
665
- });
666
- linterResults.push(result);
667
- if (!result.exists) {
668
- errors.push({
669
- type: "invalid-rule",
670
- message: result.error ??
671
- `Rule "${rule.linterRule}" not found in ${result.linter}`,
672
- path: rule.linterRule,
673
- });
674
- }
675
- else if (result.enabled === "disabled") {
676
- errors.push({
677
- type: "invalid-rule",
678
- message: `Rule "${result.rule}" exists but is disabled in ${result.linter} config`,
679
- path: rule.linterRule,
680
- });
681
- }
682
- }
758
+ return {
759
+ render: () => [],
760
+ errors: [],
761
+ linterResults: [],
762
+ enforceVerdicts: [],
763
+ };
683
764
  }
684
- const detailsErrors = ruleEntries.flatMap(([, rule]) => rule._kind !== "guard" && rule.details
685
- ? validateRuleDetails(rule.details, basePath)
686
- : []);
765
+ // Verify FIRST, render second: the marker an `enforce()` gets is the
766
+ // verdict's, so rendering cannot run ahead of verification (#351). Verified
767
+ // once; rendered once per output file, since links depend on where it sits.
768
+ const verified = ruleEntries.map(([id, rule]) => {
769
+ if (rule._kind !== "enforce")
770
+ return { id, rule };
771
+ const check = verifyEnforce(rule, basePath, options);
772
+ return { id, rule, check: { ref: rule.linterRule, ...check } };
773
+ });
774
+ const checks = verified.flatMap((r) => (r.check ? [r.check] : []));
687
775
  return {
688
- lines: [ruleLines.join("\n")],
689
- errors: [...errors, ...detailsErrors],
690
- linterResults,
776
+ render: (out) => [
777
+ [
778
+ "## Rules",
779
+ ...verified.flatMap((r) => [
780
+ "",
781
+ compileRule(r.id, r.rule, out, r.check?.verdict),
782
+ ]),
783
+ ].join("\n"),
784
+ ],
785
+ errors: [
786
+ ...checks.flatMap((c) => (c.error ? [c.error] : [])),
787
+ ...ruleEntries.flatMap(([, rule]) => rule._kind !== "guard" && rule.details
788
+ ? validateRuleDetails(rule.details, basePath)
789
+ : []),
790
+ ],
791
+ linterResults: checks.flatMap((c) => (c.result ? [c.result] : [])),
792
+ enforceVerdicts: checks.map((c) => ({ ref: c.ref, verdict: c.verdict })),
691
793
  };
692
794
  }
693
795
  // ---------------------------------------------------------------------------
694
796
  // compileClaude
695
797
  // ---------------------------------------------------------------------------
798
+ /** The spec must be named after its primary target: `<target>.spec.ts`. */
799
+ function specNameErrors(specFile, target) {
800
+ if (!specFile.endsWith(".spec.ts")) {
801
+ return [
802
+ {
803
+ type: "spec-name-mismatch",
804
+ message: `Spec file "${specFile}" must end with .spec.ts`,
805
+ },
806
+ ];
807
+ }
808
+ if ((0, node_path_1.basename)(specFile, ".spec.ts") === target)
809
+ return [];
810
+ return [
811
+ {
812
+ type: "spec-name-mismatch",
813
+ message: `Spec file "${specFile}" doesn't match target "${target}". Expected "${target}.spec.ts".`,
814
+ },
815
+ ];
816
+ }
817
+ /** More rules than `maxRules` allows is an error. */
818
+ function ruleCountErrors(spec, maxRules) {
819
+ const ruleCount = Object.keys(spec.rules).length;
820
+ if (!maxRules || ruleCount <= maxRules)
821
+ return [];
822
+ return [
823
+ {
824
+ type: "invalid-rule",
825
+ message: `${String(ruleCount)} rules exceeds maxRules limit of ${String(maxRules)}. Split into subdirectory specs.`,
826
+ },
827
+ ];
828
+ }
696
829
  /**
697
830
  * Compile a ClaudeSpec into markdown.
698
831
  *
@@ -705,46 +838,33 @@ function compileRulesSection(spec, basePath, options) {
705
838
  // (no frontmatter), so Codex/OpenCode reuse it unchanged — only the h1 target
706
839
  // differs, which already comes from the injected dialect. Nothing here is
707
840
  // CC-only, so there is no per-dialect branch to gate.
708
- async function compileClaude(spec, options = {}) {
841
+ async function compileClaude(spec, options) {
709
842
  const targets = spec.target ?? options.dialect?.instructionTargets[0] ?? DEFAULT_TARGET;
710
843
  const target = Array.isArray(targets) ? targets[0] : targets;
711
- const basePath = options.basePath ?? process.cwd();
844
+ const basePath = options.basePath;
712
845
  const specFile = options.specFile ?? `${target}.spec.ts`;
713
- const errors = [];
714
- const sections = [`# ${target}`];
715
- // Verify spec file naming matches the primary target
716
- if (!specFile.endsWith(".spec.ts")) {
717
- errors.push({
718
- type: "spec-name-mismatch",
719
- message: `Spec file "${specFile}" must end with .spec.ts`,
720
- });
721
- }
722
- else {
723
- const baseName = (0, node_path_1.basename)(specFile, ".spec.ts");
724
- if (baseName !== target) {
725
- errors.push({
726
- type: "spec-name-mismatch",
727
- message: `Spec file "${specFile}" doesn't match target "${target}". Expected "${target}.spec.ts".`,
728
- });
729
- }
730
- }
731
- // maxRules check
732
- const ruleCount = Object.keys(spec.rules).length;
733
- if (options.maxRules && ruleCount > options.maxRules) {
734
- errors.push({
735
- type: "invalid-rule",
736
- message: `${String(ruleCount)} rules exceeds maxRules limit of ${String(options.maxRules)}. Split into subdirectory specs.`,
737
- });
738
- }
846
+ const errors = [
847
+ ...specNameErrors(specFile, target),
848
+ ...ruleCountErrors(spec, options.maxRules),
849
+ ];
739
850
  // Per-spec maxSectionLines takes precedence, then compile options
740
851
  const maxSectionLines = spec.maxSectionLines ?? options.maxSectionLines;
741
- const prose = await compileSectionsSection(spec, basePath, maxSectionLines);
852
+ const out = compiledPathOf(specFile);
853
+ const proseErrors = await compileSectionsSection(spec, basePath, out, maxSectionLines);
742
854
  const keyFiles = compileKeyFilesSection(spec, basePath);
743
855
  const commands = compileCommandsSection(spec, basePath);
744
856
  const rules = compileRulesSection(spec, basePath, options);
745
- sections.push(...prose.lines, ...keyFiles.lines, ...commands.lines, ...rules.lines);
746
- errors.push(...prose.errors, ...keyFiles.errors, ...commands.errors, ...rules.errors);
747
- const body = sections.join("\n\n") + "\n";
857
+ errors.push(...proseErrors, ...keyFiles.errors, ...commands.errors, ...rules.errors);
858
+ // The file at `at`, headed `heading`. Every link in it is written from
859
+ // `at`'s own directory, so each target is rendered for its own path.
860
+ const bodyAt = (heading, at) => [
861
+ `# ${heading}`,
862
+ ...renderSections(spec, at),
863
+ ...keyFiles.lines,
864
+ ...commands.lines,
865
+ ...rules.render(at),
866
+ ].join("\n\n") + "\n";
867
+ const body = bodyAt(target, out);
748
868
  const tokens = estimateTokens(body);
749
869
  // Per-spec maxTokens takes precedence, then compile options. An explicit
750
870
  // ceiling is an error at any `compile-budget` severity, as it always was:
@@ -764,7 +884,7 @@ async function compileClaude(spec, options = {}) {
764
884
  : [
765
885
  ...checkContentBudgets({
766
886
  spec,
767
- renderedRules: rules.lines.join("\n\n"),
887
+ renderedRules: rules.render(out).join("\n\n"),
768
888
  }, specFile, resolveBudgetLimits(options, severity)),
769
889
  ];
770
890
  return {
@@ -775,17 +895,24 @@ async function compileClaude(spec, options = {}) {
775
895
  warnings: severity === "warn" ? budgetFindings : [],
776
896
  budgetFindings,
777
897
  linterResults: rules.linterResults,
898
+ enforceVerdicts: rules.enforceVerdicts,
778
899
  tokens,
779
900
  targets: allTargets,
901
+ outputs: allTargets.map((t, i) => {
902
+ if (i === 0)
903
+ return { path: out, markdown };
904
+ const path = (0, frame_js_1.targetBeside)(out, t);
905
+ return { path, markdown: addHash(bodyAt(t, path), specFile) };
906
+ }),
780
907
  };
781
908
  }
782
909
  // ---------------------------------------------------------------------------
783
910
  // Compile SKILL.md spec → markdown
784
911
  // ---------------------------------------------------------------------------
785
- function renderBody(body) {
912
+ function renderBody(body, out) {
786
913
  if (typeof body === "string")
787
914
  return body;
788
- return body.map(renderFragment).join("");
915
+ return body.map((f) => renderFragment(f, out)).join("");
789
916
  }
790
917
  /** Derive the `argument-hint` frontmatter value from typed inputs. */
791
918
  function renderArgumentHint(inputs) {
@@ -826,17 +953,17 @@ function renderGate(gate, retry) {
826
953
  };
827
954
  }
828
955
  /** Render the `## Steps` checklist with a gate per step. */
829
- function renderSteps(steps) {
830
- const out = ["## Steps", ""];
956
+ function renderSteps(steps, out) {
957
+ const lines = ["## Steps", ""];
831
958
  steps.forEach((s, idx) => {
832
- out.push(`### Step ${String(idx + 1)}`, "");
833
- out.push(renderBody(s.do).trim(), "");
959
+ lines.push(`### Step ${String(idx + 1)}`, "");
960
+ lines.push(renderBody(s.do, out).trim(), "");
834
961
  if (s.gate) {
835
962
  const g = renderGate(s.gate, s.retry);
836
- out.push(g.prose, "", g.marker, "");
963
+ lines.push(g.prose, "", g.marker, "");
837
964
  }
838
965
  });
839
- return out.join("\n").trimEnd();
966
+ return lines.join("\n").trimEnd();
840
967
  }
841
968
  /** Render the `## Result` postcondition gate. */
842
969
  function renderResult(result) {
@@ -982,15 +1109,15 @@ function renderSkillFrontmatter(spec, keys = dialect_js_1.RENDERABLE_SKILL_FRONT
982
1109
  * the gated Steps, then the Result. body + steps compose — a skill can carry
983
1110
  * both a rich reference body and a verified procedure.
984
1111
  */
985
- function renderSkillSections(spec) {
1112
+ function renderSkillSections(spec, out) {
986
1113
  const sections = [];
987
1114
  if (spec.inputs && spec.inputs.length > 0) {
988
1115
  sections.push(renderArguments(spec.inputs));
989
1116
  }
990
1117
  if (spec.body !== undefined)
991
- sections.push(renderBody(spec.body).trim());
1118
+ sections.push(renderBody(spec.body, out).trim());
992
1119
  if (spec.steps && spec.steps.length > 0) {
993
- sections.push(renderSteps(spec.steps));
1120
+ sections.push(renderSteps(spec.steps, out));
994
1121
  }
995
1122
  if (spec.postcondition)
996
1123
  sections.push(renderResult(spec.postcondition));
@@ -1040,7 +1167,7 @@ function checkInlineCode(markdown, max) {
1040
1167
  /**
1041
1168
  * Compile a SkillSpec into SKILL.md markdown with YAML frontmatter.
1042
1169
  */
1043
- async function compileSkill(spec, options = {}) {
1170
+ async function compileSkill(spec, options) {
1044
1171
  // 🔴 NORMALISE FIRST, before anything reads the spec. `compileSkill` accepts a
1045
1172
  // `SkillSpec` STRUCTURALLY, so a caller can hand us `{ _specType: "skill", …,
1046
1173
  // result: cmd(…) }` without ever touching `experimental_skill()`. Both readers
@@ -1049,7 +1176,7 @@ async function compileSkill(spec, options = {}) {
1049
1176
  // section AND its reference verification, silently. Doing it here rather than
1050
1177
  // in the readers means a reader added later cannot reintroduce the gap.
1051
1178
  spec = (0, skill_normalize_js_1.foldLegacyPostcondition)(spec);
1052
- const basePath = options.basePath ?? process.cwd();
1179
+ const basePath = options.basePath;
1053
1180
  const specFile = options.specFile ?? "SKILL.md.spec.ts";
1054
1181
  const frontmatterKeys = options.dialect?.skillFrontmatterKeys ?? dialect_js_1.RENDERABLE_SKILL_FRONTMATTER_KEYS;
1055
1182
  const errors = [];
@@ -1115,7 +1242,7 @@ async function compileSkill(spec, options = {}) {
1115
1242
  errors.push({ type: "unknown-tool", message: issue.message });
1116
1243
  }
1117
1244
  }
1118
- const sections = renderSkillSections(spec);
1245
+ const sections = renderSkillSections(spec, compiledPathOf(specFile));
1119
1246
  // Over-long inline code blocks are WARNINGS, not errors — they don't block
1120
1247
  // compilation (so adoption always compiles), just nudge toward file().
1121
1248
  const warnings = checkInlineCode(sections, spec.maxInlineCodeLines ?? DEFAULT_MAX_INLINE_CODE_LINES);
@@ -1201,7 +1328,7 @@ function purityMarker(purity) {
1201
1328
  return `<!-- vigiles:purity:${level} -->`;
1202
1329
  }
1203
1330
  /** Render the subagent's named `##` system-prompt sections (verified like CLAUDE.md). */
1204
- async function renderAgentSections(sections, basePath) {
1331
+ async function renderAgentSections(sections, basePath, out) {
1205
1332
  const lines = [];
1206
1333
  const errors = [];
1207
1334
  for (const [name, content] of Object.entries(sections)) {
@@ -1218,7 +1345,7 @@ async function renderAgentSections(sections, basePath) {
1218
1345
  }
1219
1346
  else {
1220
1347
  errors.push(...(await validateRefs(content, basePath)));
1221
- const rendered = content.map(renderFragment).join("");
1348
+ const rendered = renderBody(content, out);
1222
1349
  errors.push(...validateSectionContent(name, rendered));
1223
1350
  lines.push(`## ${heading}\n\n${rendered.trim()}`);
1224
1351
  }
@@ -1259,13 +1386,26 @@ function renderOutputContract(contract) {
1259
1386
  "```",
1260
1387
  ].join("\n");
1261
1388
  }
1262
- /** Render the rules a subagent must follow as a `## Rules` section. */
1263
- function renderAgentRules(rules) {
1264
- const parts = ["## Rules", ""];
1265
- for (const [id, rule] of Object.entries(rules)) {
1266
- parts.push(compileRule(id, rule), "");
1267
- }
1268
- return parts.join("\n").trim();
1389
+ /**
1390
+ * Render the rules a subagent must follow as a `## Rules` section. Each
1391
+ * `enforce()` is verified the same way `compileClaude` verifies it, so the
1392
+ * marker is the verdict's and a missing or switched-off rule is an error here
1393
+ * too (#351).
1394
+ */
1395
+ function renderAgentRules(rules, basePath, out, verify) {
1396
+ const rendered = Object.entries(rules).map(([id, rule]) => {
1397
+ if (rule._kind !== "enforce")
1398
+ return { text: compileRule(id, rule, out) };
1399
+ const check = verifyEnforce(rule, basePath, verify);
1400
+ return {
1401
+ text: compileRule(id, rule, out, check.verdict),
1402
+ error: check.error,
1403
+ };
1404
+ });
1405
+ return {
1406
+ text: ["## Rules", ...rendered.flatMap((r) => ["", r.text])].join("\n"),
1407
+ errors: rendered.flatMap((r) => ("error" in r && r.error ? [r.error] : [])),
1408
+ };
1269
1409
  }
1270
1410
  /**
1271
1411
  * Compile an AgentSpec into a subagent markdown file with YAML frontmatter.
@@ -1273,7 +1413,7 @@ function renderAgentRules(rules) {
1273
1413
  * carries (`vigiles:symbol`, file/cmd refs) are the same ones `lint` re-checks.
1274
1414
  */
1275
1415
  async function compileAgent(spec, options) {
1276
- const basePath = options.basePath ?? process.cwd();
1416
+ const basePath = options.basePath;
1277
1417
  const specFile = options.specFile ?? "agent.md.spec.ts";
1278
1418
  const dialect = options.dialect;
1279
1419
  const errors = [];
@@ -1309,16 +1449,19 @@ async function compileAgent(spec, options) {
1309
1449
  if (Array.isArray(spec.body)) {
1310
1450
  errors.push(...(await validateRefs(spec.body, basePath)));
1311
1451
  }
1452
+ const out = compiledPathOf(specFile);
1312
1453
  const sections = [];
1313
1454
  if (spec.body !== undefined)
1314
- sections.push(renderBody(spec.body).trim());
1455
+ sections.push(renderBody(spec.body, out).trim());
1315
1456
  if (spec.sections) {
1316
- const result = await renderAgentSections(spec.sections, basePath);
1457
+ const result = await renderAgentSections(spec.sections, basePath, out);
1317
1458
  sections.push(...result.lines);
1318
1459
  errors.push(...result.errors);
1319
1460
  }
1320
1461
  if (spec.rules && Object.keys(spec.rules).length > 0) {
1321
- sections.push(renderAgentRules(spec.rules));
1462
+ const agentRules = renderAgentRules(spec.rules, basePath, out, options);
1463
+ sections.push(agentRules.text);
1464
+ errors.push(...agentRules.errors);
1322
1465
  }
1323
1466
  if (spec.output)
1324
1467
  sections.push(renderOutputContract(spec.output));