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.
- package/dist/adapters/claude-code/event-capability.js +24 -8
- package/dist/adapters/claude-code/hook-protocol.js +2 -1
- package/dist/adapters/codex/hook-protocol.js +2 -1
- package/dist/cli-flag-check.d.ts +36 -0
- package/dist/cli-flag-check.js +72 -1
- package/dist/cli-main.d.ts +2 -2
- package/dist/cli-main.js +548 -379
- package/dist/core/bash-effects.d.ts +0 -57
- package/dist/core/bash-effects.js +0 -147
- package/dist/core/compile-generator.d.ts +6 -6
- package/dist/core/compile-generator.js +4 -4
- package/dist/core/compile.d.ts +72 -8
- package/dist/core/compile.js +276 -133
- package/dist/core/config-schema.d.ts +40 -10
- package/dist/core/config-schema.js +139 -16
- package/dist/core/coverage.d.ts +16 -0
- package/dist/core/coverage.js +22 -0
- package/dist/core/doc-refs.d.ts +3 -2
- package/dist/core/doc-refs.js +2 -2
- package/dist/core/enforce-marker.d.ts +28 -0
- package/dist/core/enforce-marker.js +34 -0
- package/dist/core/eslint-inventory.d.ts +96 -0
- package/dist/core/eslint-inventory.js +251 -0
- package/dist/core/event-capability.d.ts +5 -1
- package/dist/core/frame.d.ts +43 -2
- package/dist/core/frame.js +85 -2
- package/dist/core/generate-schema.d.ts +3 -2
- package/dist/core/generate-schema.js +2 -2
- package/dist/core/generate-types.d.ts +11 -2
- package/dist/core/generate-types.js +12 -5
- package/dist/core/hook-command.d.ts +172 -0
- package/dist/core/hook-command.js +277 -0
- package/dist/core/hook-delivery.d.ts +133 -0
- package/dist/core/hook-delivery.js +150 -0
- package/dist/core/hook-paths.d.ts +14 -0
- package/dist/core/hook-paths.js +22 -0
- package/dist/core/hook-program.d.ts +4 -4
- package/dist/core/hook-program.js +13 -25
- package/dist/core/hook-state.d.ts +1 -1
- package/dist/core/hook-state.js +1 -1
- package/dist/core/instruction-remedies.d.ts +35 -0
- package/dist/core/instruction-remedies.js +70 -1
- package/dist/core/integrity-header.d.ts +54 -0
- package/dist/core/integrity-header.js +90 -0
- package/dist/core/integrity.d.ts +1 -25
- package/dist/core/integrity.js +16 -74
- package/dist/core/linter-adapter.d.ts +27 -3
- package/dist/core/linters.d.ts +8 -1
- package/dist/core/linters.js +116 -81
- package/dist/core/orphans.d.ts +3 -5
- package/dist/core/orphans.js +9 -10
- package/dist/core/presets.d.ts +19 -7
- package/dist/core/presets.js +19 -6
- package/dist/core/rule-catalog.d.ts +11 -12
- package/dist/core/rule-catalog.js +25 -94
- package/dist/core/rule-meta.d.ts +12 -0
- package/dist/core/rule-meta.js +47 -10
- package/dist/core/source-refs.d.ts +3 -23
- package/dist/core/source-refs.js +3 -27
- package/dist/core/test-utils.d.ts +13 -0
- package/dist/core/test-utils.js +40 -1
- package/dist/core/types.d.ts +18 -26
- package/dist/core/validate.d.ts +1 -2
- package/dist/core/validate.js +60 -47
- package/dist/coverage-evidence.js +2 -3
- package/dist/deprecated-key-files.d.ts +6 -0
- package/dist/deprecated-key-files.js +7 -6
- package/dist/exclude.d.ts +3 -1
- package/dist/exclude.js +9 -33
- package/dist/harness-assert.d.ts +18 -0
- package/dist/harness-assert.js +38 -0
- package/dist/hook-check-install.js +15 -6
- package/dist/hook-check.d.ts +1 -3
- package/dist/hook-check.js +3 -5
- package/dist/hook-install.d.ts +7 -41
- package/dist/hook-install.js +29 -96
- package/dist/hook-runtime.js +2 -1
- package/dist/load-hook.d.ts +3 -2
- package/dist/load-hook.js +28 -6
- package/dist/overrides.d.ts +25 -0
- package/dist/overrides.js +51 -0
- package/dist/plugin-loader.d.ts +23 -0
- package/dist/plugin-loader.js +42 -12
- package/dist/root-globs.d.ts +9 -0
- package/dist/root-globs.js +48 -0
- package/dist/rule-routing.js +6 -3
- package/dist/run-hook.d.ts +26 -2
- package/dist/run-hook.js +48 -12
- package/dist/scan-core.d.ts +96 -20
- package/dist/scan-core.js +171 -176
- package/dist/scan-files.d.ts +0 -8
- package/dist/scan-files.js +78 -20
- package/dist/scan.d.ts +12 -12
- package/dist/scan.js +33 -10
- package/dist/setup-plan.d.ts +15 -9
- package/dist/setup-plan.js +26 -18
- package/dist/test-coverage.d.ts +3 -8
- package/dist/test-coverage.js +6 -6
- package/dist/test.d.ts +1 -1
- package/dist/vigilesrc.schema.json +1329 -118
- package/package.json +1 -1
- package/skills/adopt-spec/SKILL.md +2 -1
package/dist/core/compile.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
619
|
-
return
|
|
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 {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
685
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
|
844
|
+
const basePath = options.basePath;
|
|
712
845
|
const specFile = options.specFile ?? `${target}.spec.ts`;
|
|
713
|
-
const errors = [
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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.
|
|
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
|
|
956
|
+
function renderSteps(steps, out) {
|
|
957
|
+
const lines = ["## Steps", ""];
|
|
831
958
|
steps.forEach((s, idx) => {
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
963
|
+
lines.push(g.prose, "", g.marker, "");
|
|
837
964
|
}
|
|
838
965
|
});
|
|
839
|
-
return
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
|
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
|
-
|
|
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));
|