vigiles 38.1.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/dialect.js +5 -1
- 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 +549 -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 +283 -137
- package/dist/core/config-schema.d.ts +45 -11
- package/dist/core/config-schema.js +174 -18
- 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-baseline.d.ts +26 -0
- package/dist/core/instruction-baseline.js +45 -1
- package/dist/core/instruction-remedies.d.ts +48 -0
- package/dist/core/instruction-remedies.js +93 -0
- package/dist/core/instruction-weight.d.ts +6 -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 +36 -27
- 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/instruction-weight-ratchet.d.ts +17 -4
- package/dist/instruction-weight-ratchet.js +71 -22
- 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 +1405 -144
- package/package.json +1 -1
- package/skills/adopt-spec/SKILL.md +2 -1
package/dist/cli-main.js
CHANGED
|
@@ -32,6 +32,7 @@ const generate_types_js_1 = require("./core/generate-types.js");
|
|
|
32
32
|
const generate_harness_js_1 = require("./core/generate-harness.js");
|
|
33
33
|
const capability_diff_js_1 = require("./core/capability-diff.js");
|
|
34
34
|
const validate_js_1 = require("./core/validate.js");
|
|
35
|
+
const overrides_js_1 = require("./overrides.js");
|
|
35
36
|
const eval_lock_js_1 = require("./eval-lock.js");
|
|
36
37
|
const eval_entry_js_1 = require("./eval-entry.js");
|
|
37
38
|
const zod_1 = require("zod");
|
|
@@ -69,6 +70,7 @@ const instruction_sources_js_1 = require("./instruction-sources.js");
|
|
|
69
70
|
const rule_catalog_js_1 = require("./core/rule-catalog.js");
|
|
70
71
|
const adoptability_js_1 = require("./adoptability.js");
|
|
71
72
|
const compile_js_1 = require("./core/compile.js");
|
|
73
|
+
const enforce_marker_js_1 = require("./core/enforce-marker.js");
|
|
72
74
|
const proofs_js_1 = require("./core/proofs.js");
|
|
73
75
|
const inline_js_1 = require("./core/inline.js");
|
|
74
76
|
const frontmatter_js_1 = require("./core/frontmatter.js");
|
|
@@ -79,6 +81,7 @@ const action_gate_js_1 = require("./action-gate.js");
|
|
|
79
81
|
const guards_js_1 = require("./core/guards.js");
|
|
80
82
|
const hook_program_js_1 = require("./core/hook-program.js");
|
|
81
83
|
const hook_install_js_1 = require("./hook-install.js");
|
|
84
|
+
const hook_command_js_1 = require("./core/hook-command.js");
|
|
82
85
|
const hook_check_install_js_1 = require("./hook-check-install.js");
|
|
83
86
|
const hook_providers_js_1 = require("./core/hook-providers.js");
|
|
84
87
|
const toml_1 = require("@iarna/toml");
|
|
@@ -98,6 +101,7 @@ const harness_test_js_1 = require("./harness-test.js");
|
|
|
98
101
|
const run_scripts_js_1 = require("./adapters/claude-code/run-scripts.js");
|
|
99
102
|
const coverage_artifact_js_1 = require("./coverage-artifact.js");
|
|
100
103
|
const integrity_js_1 = require("./core/integrity.js");
|
|
104
|
+
const instruction_remedies_js_1 = require("./core/instruction-remedies.js");
|
|
101
105
|
const adopt_js_1 = require("./core/adopt.js");
|
|
102
106
|
const coverage_js_1 = require("./core/coverage.js");
|
|
103
107
|
const orphans_js_1 = require("./core/orphans.js");
|
|
@@ -343,6 +347,27 @@ function printErrors(specFile, errors) {
|
|
|
343
347
|
console.log(`::error file=${specFile}::${err.message}`);
|
|
344
348
|
}
|
|
345
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* The rule count line, split by what compile VERIFIED (#351): "linter-verified"
|
|
352
|
+
* counts only references rendered "**Enforced by:**"; every other `enforce()` was
|
|
353
|
+
* rendered "**Claimed (unverified):**" and gets one line saying why and what
|
|
354
|
+
* turns it into "Enforced by". Never changes the exit code.
|
|
355
|
+
*/
|
|
356
|
+
function printRuleSummary(specFile, ruleCount, verdicts) {
|
|
357
|
+
const unverified = verdicts.flatMap((v) => v.verdict.kind === "unverified"
|
|
358
|
+
? [{ ref: v.ref, reason: v.verdict.reason }]
|
|
359
|
+
: []);
|
|
360
|
+
const verified = verdicts.length - unverified.length;
|
|
361
|
+
const claimed = unverified.length > 0
|
|
362
|
+
? `, ${String(unverified.length)} claimed unverified`
|
|
363
|
+
: "";
|
|
364
|
+
console.log(` ${String(ruleCount)} rules (${String(verified)} linter-verified${claimed})`);
|
|
365
|
+
unverified.forEach((u) => {
|
|
366
|
+
const advice = (0, compile_js_1.unverifiedAdvice)(u.ref, u.reason);
|
|
367
|
+
console.log(` ⚠ Claimed (unverified) — ${advice}`);
|
|
368
|
+
console.log(`::warning file=${specFile}::${advice}`);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
346
371
|
/** Non-blocking advisories — printed, but never fail the compile. */
|
|
347
372
|
function printWarnings(specFile, warnings) {
|
|
348
373
|
for (const w of warnings) {
|
|
@@ -355,10 +380,10 @@ function printWarnings(specFile, warnings) {
|
|
|
355
380
|
// Commands
|
|
356
381
|
// ---------------------------------------------------------------------------
|
|
357
382
|
/** Compile a generator-skill spec from source → SKILL.md. Returns validity. */
|
|
358
|
-
function compileGeneratorSkillToFile(specPath, source) {
|
|
383
|
+
function compileGeneratorSkillToFile(root, specPath, source) {
|
|
359
384
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
360
385
|
const { artifact, errors } = (0, compile_generator_js_1.compileGeneratorSkill)(source, {
|
|
361
|
-
basePath:
|
|
386
|
+
basePath: root,
|
|
362
387
|
specFile: specPath,
|
|
363
388
|
});
|
|
364
389
|
// Written only when the compile is clean: `artifact` is null otherwise, and
|
|
@@ -375,9 +400,9 @@ function compileGeneratorSkillToFile(specPath, source) {
|
|
|
375
400
|
return false;
|
|
376
401
|
}
|
|
377
402
|
/** Compile a ClaudeSpec → its primary + any additional targets. */
|
|
378
|
-
async function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
379
|
-
const basePath =
|
|
380
|
-
const { markdown, errors, warnings,
|
|
403
|
+
async function compileClaudeToFile(root, spec, specPath, config, dialect) {
|
|
404
|
+
const basePath = root;
|
|
405
|
+
const { markdown, errors, warnings, enforceVerdicts, outputs } = await (0, compile_js_1.compileClaude)(spec, {
|
|
381
406
|
basePath,
|
|
382
407
|
specFile: specPath,
|
|
383
408
|
dialect,
|
|
@@ -386,7 +411,7 @@ async function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
|
386
411
|
maxSectionLines: config.maxSectionLines,
|
|
387
412
|
catalogOnly: config.catalogOnly,
|
|
388
413
|
linters: config.linters,
|
|
389
|
-
...compileBudgetOptions(config),
|
|
414
|
+
...compileBudgetOptions(config, specPath.replace(/\.spec\.ts$/, "")),
|
|
390
415
|
});
|
|
391
416
|
const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
|
|
392
417
|
// Budget findings print BEFORE the pass/fail line and never change the exit
|
|
@@ -418,20 +443,12 @@ async function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
|
418
443
|
console.log(` → ${primaryOutput} was NOT written; the previous version is left in place.`);
|
|
419
444
|
return false;
|
|
420
445
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
for (const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const dir = primaryOutput.substring(0, primaryOutput.lastIndexOf("/") + 1);
|
|
428
|
-
const targetPath = dir + t;
|
|
429
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, targetPath), (0, compile_js_1.addHash)(body, specPath));
|
|
430
|
-
outputNames.push(targetPath);
|
|
431
|
-
}
|
|
432
|
-
const linterCount = linterResults.filter((r) => r.exists).length;
|
|
433
|
-
console.log(`\n✓ ${specPath} → ${outputNames.join(", ")} (${formatArtifactSize(markdown)})`);
|
|
434
|
-
console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
|
|
446
|
+
// Each target as compile rendered it for its own path: a secondary in
|
|
447
|
+
// another directory has its own links, so it is never a re-headed copy.
|
|
448
|
+
for (const o of outputs)
|
|
449
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, o.path), o.markdown);
|
|
450
|
+
console.log(`\n✓ ${specPath} → ${outputs.map((o) => o.path).join(", ")} (${formatArtifactSize(markdown)})`);
|
|
451
|
+
printRuleSummary(specPath, Object.keys(spec.rules).length, enforceVerdicts);
|
|
435
452
|
return true;
|
|
436
453
|
}
|
|
437
454
|
/**
|
|
@@ -512,10 +529,10 @@ function formatArtifactSize(markdown) {
|
|
|
512
529
|
return `${size} · ~${String(kTokens)}k tokens est.`;
|
|
513
530
|
}
|
|
514
531
|
/** Compile a declarative SkillSpec → SKILL.md. */
|
|
515
|
-
async function compileSkillToFile(spec, specPath, dialect) {
|
|
532
|
+
async function compileSkillToFile(root, spec, specPath, dialect) {
|
|
516
533
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
517
534
|
const { artifact, errors, warnings } = await (0, compile_js_1.compileSkill)(spec, {
|
|
518
|
-
basePath:
|
|
535
|
+
basePath: root,
|
|
519
536
|
specFile: specPath,
|
|
520
537
|
// The SKILL.md frontmatter profile comes from the resolved harness — a Codex
|
|
521
538
|
// repo gets a minimal (name + description) SKILL.md; CC gets the full set.
|
|
@@ -536,12 +553,15 @@ async function compileSkillToFile(spec, specPath, dialect) {
|
|
|
536
553
|
return false;
|
|
537
554
|
}
|
|
538
555
|
/** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
|
|
539
|
-
async function compileAgentToFile(spec, specPath, dialect) {
|
|
556
|
+
async function compileAgentToFile(root, spec, specPath, config, dialect) {
|
|
540
557
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
558
|
+
// The same verify options an instruction file gets (`compileClaudeToFile`).
|
|
541
559
|
const { artifact, errors, warnings } = await (0, compile_js_1.compileAgent)(spec, {
|
|
542
|
-
basePath:
|
|
560
|
+
basePath: root,
|
|
543
561
|
specFile: specPath,
|
|
544
562
|
dialect,
|
|
563
|
+
catalogOnly: config.catalogOnly,
|
|
564
|
+
linters: config.linters,
|
|
545
565
|
});
|
|
546
566
|
// Written only when the compile is clean: `artifact` is null otherwise, and
|
|
547
567
|
// `writeArtifact` takes nothing else.
|
|
@@ -591,6 +611,7 @@ async function collectAgentNames(excludes) {
|
|
|
591
611
|
return names;
|
|
592
612
|
}
|
|
593
613
|
async function compile(specPaths, config, excludes, opts = {}) {
|
|
614
|
+
const root = excludes.root;
|
|
594
615
|
let allValid = true;
|
|
595
616
|
// Parse the declared harness set ONCE (alias-normalized) and feed both the
|
|
596
617
|
// dialect pick and the mirror from it — no re-parsing, no cwd-sniffing in the
|
|
@@ -598,7 +619,7 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
598
619
|
// ambiguous-detection pick.
|
|
599
620
|
const declaredHarnesses = (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses);
|
|
600
621
|
const selection = (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
601
|
-
root
|
|
622
|
+
root,
|
|
602
623
|
flag: opts.harnessFlag,
|
|
603
624
|
configHarness: declaredHarnesses,
|
|
604
625
|
});
|
|
@@ -610,9 +631,9 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
610
631
|
let knownAgents = null;
|
|
611
632
|
for (const specPath of specPaths) {
|
|
612
633
|
// Generator skills can't be executed to markdown — compile from source.
|
|
613
|
-
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(
|
|
634
|
+
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(root, specPath), "utf-8");
|
|
614
635
|
if (/\bgenSkill\s*\(/.test(source)) {
|
|
615
|
-
if (!compileGeneratorSkillToFile(specPath, source))
|
|
636
|
+
if (!compileGeneratorSkillToFile(root, specPath, source))
|
|
616
637
|
allValid = false;
|
|
617
638
|
continue;
|
|
618
639
|
}
|
|
@@ -633,7 +654,7 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
633
654
|
const specDialect = opts.harnessFlag === undefined
|
|
634
655
|
? ((0, adapter_registry_js_1.adapterForInstructionFile)(targetFile)?.dialect ?? dialect)
|
|
635
656
|
: dialect;
|
|
636
|
-
if (await compileClaudeToFile(spec, specPath, config, specDialect)) {
|
|
657
|
+
if (await compileClaudeToFile(root, spec, specPath, config, specDialect)) {
|
|
637
658
|
writeInstructionMirrors(specPath.replace(/\.spec\.ts$/, ""), declaredHarnesses);
|
|
638
659
|
}
|
|
639
660
|
else {
|
|
@@ -649,11 +670,11 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
649
670
|
for (const w of (0, skill_harness_js_1.skillFrontmatterDropWarnings)(spec, forHarnesses)) {
|
|
650
671
|
console.log(`⚠ ${w}`);
|
|
651
672
|
}
|
|
652
|
-
if (!(await compileSkillToFile(spec, specPath, dialect)))
|
|
673
|
+
if (!(await compileSkillToFile(root, spec, specPath, dialect)))
|
|
653
674
|
allValid = false;
|
|
654
675
|
}
|
|
655
676
|
else if (spec._specType === "agent") {
|
|
656
|
-
if (!(await compileAgentToFile(spec, specPath, dialect)))
|
|
677
|
+
if (!(await compileAgentToFile(root, spec, specPath, config, dialect)))
|
|
657
678
|
allValid = false;
|
|
658
679
|
}
|
|
659
680
|
else if (spec._specType === "railway") {
|
|
@@ -690,14 +711,14 @@ function ghAnnotate(level, message, file, line) {
|
|
|
690
711
|
const loc = locParts.length > 0 ? " " + locParts.join(",") : "";
|
|
691
712
|
console.log(`::${level}${loc}::${message}`);
|
|
692
713
|
}
|
|
693
|
-
function verifyHashes(frame, filePaths, silent = false) {
|
|
714
|
+
function verifyHashes(root, frame, filePaths, silent = false) {
|
|
694
715
|
let errorCount = 0;
|
|
695
716
|
const log = (msg) => {
|
|
696
717
|
if (!silent)
|
|
697
718
|
console.log(msg);
|
|
698
719
|
};
|
|
699
720
|
for (const filePath of filePaths) {
|
|
700
|
-
const fullPath = (0, node_path_1.resolve)(
|
|
721
|
+
const fullPath = (0, node_path_1.resolve)(root, filePath);
|
|
701
722
|
// If the file doesn't exist at all (typo, deleted), that's an error —
|
|
702
723
|
// not a "no hash" informational message. Without this check, a scoped
|
|
703
724
|
// lint like `vigiles lint typo.md` would silently exit clean.
|
|
@@ -727,14 +748,14 @@ function verifyHashes(frame, filePaths, silent = false) {
|
|
|
727
748
|
}
|
|
728
749
|
return { valid: errorCount === 0, errorCount };
|
|
729
750
|
}
|
|
730
|
-
function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
751
|
+
function validateSpecs(root, filePaths, rulesConfig, rules, silent = false) {
|
|
731
752
|
let allValid = true;
|
|
732
753
|
const log = (msg) => {
|
|
733
754
|
if (!silent)
|
|
734
755
|
console.log(msg);
|
|
735
756
|
};
|
|
736
757
|
for (const filePath of filePaths) {
|
|
737
|
-
const fullPath = (0, node_path_1.resolve)(
|
|
758
|
+
const fullPath = (0, node_path_1.resolve)(root, filePath);
|
|
738
759
|
let content;
|
|
739
760
|
try {
|
|
740
761
|
content = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
|
|
@@ -747,16 +768,24 @@ function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
|
747
768
|
const hashMatch = content.match(/<!-- vigiles:sha256:[a-f0-9]+ compiled from (.+) -->/);
|
|
748
769
|
if (hashMatch) {
|
|
749
770
|
// Verify the referenced spec still exists
|
|
750
|
-
const specRef = (0, node_path_1.resolve)(
|
|
771
|
+
const specRef = (0, node_path_1.resolve)(root, hashMatch[1]);
|
|
751
772
|
if (!(0, node_fs_1.existsSync)(specRef)) {
|
|
752
773
|
log(` ✗ [require-instructions-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
|
|
753
774
|
allValid = false;
|
|
754
775
|
}
|
|
755
776
|
continue;
|
|
756
777
|
}
|
|
778
|
+
// The path as discovered (cwd-relative), not `fullPath`: the finding prints
|
|
779
|
+
// it, and its `exclude` snippet must be a root-relative glob.
|
|
757
780
|
const result = (0, validate_js_1.validate)(content, {
|
|
758
|
-
filePath
|
|
759
|
-
rules
|
|
781
|
+
filePath,
|
|
782
|
+
// The two spec-presence rules resolved at THIS file (`overrides`), the
|
|
783
|
+
// rest as configured — validate reads only these two per file.
|
|
784
|
+
rules: {
|
|
785
|
+
...rulesConfig,
|
|
786
|
+
"require-instructions-spec": rules.severityAt("require-instructions-spec", filePath),
|
|
787
|
+
"require-skill-spec": rules.severityAt("require-skill-spec", filePath),
|
|
788
|
+
},
|
|
760
789
|
});
|
|
761
790
|
for (const err of result.errors) {
|
|
762
791
|
log(` ✗ [${err.rule}] ${err.message}`);
|
|
@@ -768,10 +797,10 @@ function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
|
768
797
|
}
|
|
769
798
|
return allValid;
|
|
770
799
|
}
|
|
771
|
-
function check(frame, filePaths, silent = false) {
|
|
772
|
-
const hashes = verifyHashes(frame, filePaths, silent);
|
|
773
|
-
const vConfig = (0, validate_js_1.loadConfig)();
|
|
774
|
-
const specsValid = validateSpecs(filePaths, vConfig.rules, silent);
|
|
800
|
+
function check(root, frame, filePaths, silent = false) {
|
|
801
|
+
const hashes = verifyHashes(root, frame, filePaths, silent);
|
|
802
|
+
const vConfig = (0, validate_js_1.loadConfig)(root);
|
|
803
|
+
const specsValid = validateSpecs(root, filePaths, vConfig.rules, (0, overrides_js_1.ruleResolver)(vConfig.rules, vConfig.overrides), silent);
|
|
775
804
|
return {
|
|
776
805
|
valid: hashes.valid && specsValid,
|
|
777
806
|
hashErrors: hashes.errorCount,
|
|
@@ -787,7 +816,8 @@ function check(frame, filePaths, silent = false) {
|
|
|
787
816
|
* Catches spec bloat — rules that likely say the same thing in different words.
|
|
788
817
|
* Uses information-theoretic distance (gzip-based) — no LLM, fully deterministic.
|
|
789
818
|
*/
|
|
790
|
-
async function findDuplicateRules(excludes, threshold = 0.3, silent = false, scopeFiles) {
|
|
819
|
+
async function findDuplicateRules(excludes, threshold = 0.3, silent = false, scopeFiles, severityAt = () => "warn") {
|
|
820
|
+
const root = excludes.root;
|
|
791
821
|
const log = (msg) => {
|
|
792
822
|
if (!silent)
|
|
793
823
|
console.log(msg);
|
|
@@ -810,28 +840,33 @@ async function findDuplicateRules(excludes, threshold = 0.3, silent = false, sco
|
|
|
810
840
|
for (const f of scopeFiles) {
|
|
811
841
|
let resolved;
|
|
812
842
|
try {
|
|
813
|
-
const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(
|
|
843
|
+
const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(root, f), "utf-8");
|
|
814
844
|
const m = compiledFromRe.exec(content);
|
|
815
845
|
if (m) {
|
|
816
|
-
resolved = (0, node_path_1.resolve)(
|
|
846
|
+
resolved = (0, node_path_1.resolve)(root, m[1].trim());
|
|
817
847
|
}
|
|
818
848
|
}
|
|
819
849
|
catch {
|
|
820
850
|
// File unreadable — fall through to the naming convention
|
|
821
851
|
}
|
|
822
852
|
if (!resolved) {
|
|
823
|
-
resolved = (0, node_path_1.resolve)(
|
|
853
|
+
resolved = (0, node_path_1.resolve)(root, `${f}.spec.ts`);
|
|
824
854
|
}
|
|
825
855
|
wanted.add(resolved);
|
|
826
856
|
}
|
|
827
|
-
return allSpecs.filter((specPath) => wanted.has((0, node_path_1.resolve)(
|
|
857
|
+
return allSpecs.filter((specPath) => wanted.has((0, node_path_1.resolve)(root, specPath)));
|
|
828
858
|
})()
|
|
829
859
|
: allSpecs;
|
|
830
860
|
if (specs.length === 0)
|
|
831
|
-
return { valid: true, pairCount: 0 };
|
|
861
|
+
return { valid: true, pairCount: 0, errorPairs: 0 };
|
|
832
862
|
let totalPairs = 0;
|
|
863
|
+
let errorPairs = 0;
|
|
833
864
|
let specsWithDuplicates = 0;
|
|
834
865
|
for (const specPath of specs) {
|
|
866
|
+
// Per spec: an `overrides` entry may turn `duplicate-rules` off for it.
|
|
867
|
+
const sev = severityAt(specPath);
|
|
868
|
+
if (sev === false)
|
|
869
|
+
continue;
|
|
835
870
|
const spec = await loadSpec(specPath);
|
|
836
871
|
if (!spec || spec._specType !== "claude")
|
|
837
872
|
continue;
|
|
@@ -847,6 +882,8 @@ async function findDuplicateRules(excludes, threshold = 0.3, silent = false, sco
|
|
|
847
882
|
}
|
|
848
883
|
specsWithDuplicates++;
|
|
849
884
|
totalPairs += pairs.length;
|
|
885
|
+
if (sev === "error")
|
|
886
|
+
errorPairs += pairs.length;
|
|
850
887
|
log(` ${specPath}`);
|
|
851
888
|
for (const pair of pairs.slice(0, 5)) {
|
|
852
889
|
log(` ${pair.idA} ↔ ${pair.idB} (distance: ${pair.distance.toFixed(3)})`);
|
|
@@ -857,10 +894,10 @@ async function findDuplicateRules(excludes, threshold = 0.3, silent = false, sco
|
|
|
857
894
|
}
|
|
858
895
|
if (totalPairs === 0) {
|
|
859
896
|
log("No near-duplicate rules detected.");
|
|
860
|
-
return { valid: true, pairCount: 0 };
|
|
897
|
+
return { valid: true, pairCount: 0, errorPairs: 0 };
|
|
861
898
|
}
|
|
862
899
|
log(`\n ${String(totalPairs)} duplicate pair(s) in ${String(specsWithDuplicates)} spec(s). Consider merging or rewording.`);
|
|
863
|
-
return { valid: false, pairCount: totalPairs };
|
|
900
|
+
return { valid: false, pairCount: totalPairs, errorPairs };
|
|
864
901
|
}
|
|
865
902
|
/**
|
|
866
903
|
* Verify the file-qualified symbol references (`path.ext#symbol`) in instruction
|
|
@@ -1005,12 +1042,12 @@ function lintExitCode(report) {
|
|
|
1005
1042
|
report.skillFrontmatterErrors > 0 ||
|
|
1006
1043
|
report.mcpToolErrors > 0 ||
|
|
1007
1044
|
report.hookScriptErrors > 0 ||
|
|
1045
|
+
report.handWrittenHookErrors > 0 ||
|
|
1008
1046
|
report.disallowedToolErrors > 0 ||
|
|
1009
1047
|
report.descriptionOverlapErrors > 0 ||
|
|
1010
1048
|
report.descriptionBudgetErrors > 0 ||
|
|
1011
1049
|
report.frontmatterValidErrors > 0 ||
|
|
1012
1050
|
report.mcpHookErrors > 0 ||
|
|
1013
|
-
report.preferCompiledHookErrors > 0 ||
|
|
1014
1051
|
report.lethalTrifectaErrors > 0 ||
|
|
1015
1052
|
report.skillResourceErrors > 0 ||
|
|
1016
1053
|
report.skillFenceErrors > 0 ||
|
|
@@ -1338,6 +1375,31 @@ function discoverNestedBundles(root, excludes) {
|
|
|
1338
1375
|
}
|
|
1339
1376
|
return out.sort();
|
|
1340
1377
|
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Each finding with the severity `rule` has at its path (`overrides`
|
|
1380
|
+
* applied), the ones an override turns off removed. `pathOf` returns
|
|
1381
|
+
* `undefined` for a finding about the repository as a whole, which keeps the
|
|
1382
|
+
* severity `rules` gives it.
|
|
1383
|
+
*/
|
|
1384
|
+
function graded(rules, rule, found, pathOf) {
|
|
1385
|
+
return found.flatMap((f) => {
|
|
1386
|
+
const sev = rules.severityAt(rule, pathOf(f));
|
|
1387
|
+
return sev === false ? [] : [{ ...f, sev }];
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/** The `{ issues, errors }` a per-surface check returns, from its graded findings. */
|
|
1391
|
+
function tally(found) {
|
|
1392
|
+
return {
|
|
1393
|
+
issues: found.length,
|
|
1394
|
+
errors: found.filter((f) => f.sev === "error").length,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
/** The terminal mark and annotation level for a severity. */
|
|
1398
|
+
function markOf(sev) {
|
|
1399
|
+
return sev === "error"
|
|
1400
|
+
? { mark: "✗", level: "error" }
|
|
1401
|
+
: { mark: "⚠", level: "warning" };
|
|
1402
|
+
}
|
|
1341
1403
|
/**
|
|
1342
1404
|
* Run one per-surface check over EVERY bundle and sum its counters.
|
|
1343
1405
|
*
|
|
@@ -1397,13 +1459,13 @@ function overBundles(fn, run, bundles) {
|
|
|
1397
1459
|
* Cost is bounded: only specs whose compiled target actually EXISTS are loaded,
|
|
1398
1460
|
* so a repo with no specs does no extra work at all.
|
|
1399
1461
|
*/
|
|
1400
|
-
async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
1401
|
-
|
|
1402
|
-
//
|
|
1403
|
-
//
|
|
1404
|
-
//
|
|
1405
|
-
|
|
1406
|
-
if (!
|
|
1462
|
+
async function checkSpecRefs(excludes, config, rules, silent, dialect) {
|
|
1463
|
+
// Both rules are resolved per compiled target below (`overrides`). The scan
|
|
1464
|
+
// is skipped only when neither can fire anywhere. `compile-budget` is
|
|
1465
|
+
// reported here only at "error": at its default it is a compile-time
|
|
1466
|
+
// warning, and `lint` printed nothing about size before the rule had a name
|
|
1467
|
+
// — the default output does not change.
|
|
1468
|
+
if (!rules.mayFire("spec-refs") && !rules.mayFire("compile-budget"))
|
|
1407
1469
|
return { issues: 0, errors: 0, budgetErrors: 0 };
|
|
1408
1470
|
const derived = [];
|
|
1409
1471
|
let knownAgents;
|
|
@@ -1422,7 +1484,12 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
|
1422
1484
|
derived.push({
|
|
1423
1485
|
target,
|
|
1424
1486
|
specPath,
|
|
1425
|
-
findings: await specCompileErrors(spec, specPath,
|
|
1487
|
+
findings: await specCompileErrors(spec, specPath, {
|
|
1488
|
+
root: excludes.root,
|
|
1489
|
+
dialect,
|
|
1490
|
+
config,
|
|
1491
|
+
knownAgents: knownAgents ?? [],
|
|
1492
|
+
}),
|
|
1426
1493
|
});
|
|
1427
1494
|
}
|
|
1428
1495
|
catch {
|
|
@@ -1431,50 +1498,32 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
|
1431
1498
|
continue;
|
|
1432
1499
|
}
|
|
1433
1500
|
}
|
|
1434
|
-
const messages = (pick) => derived.flatMap((d) => pick(d.findings).map((e) =>
|
|
1435
|
-
|
|
1436
|
-
|
|
1501
|
+
const messages = (pick) => derived.flatMap((d) => pick(d.findings).map((e) => ({
|
|
1502
|
+
target: d.target,
|
|
1503
|
+
msg: `${d.target}: ${e.message} (from ${d.specPath})`,
|
|
1504
|
+
})));
|
|
1505
|
+
const found = graded(rules, "spec-refs", messages((f) => f.errors), (m) => m.target);
|
|
1506
|
+
const overBudget = graded(rules, "compile-budget", messages((f) => f.budget), (m) => m.target).filter((m) => m.sev === "error");
|
|
1437
1507
|
if (found.length > 0 && !silent) {
|
|
1438
1508
|
console.log("\nSpec reference check:\n");
|
|
1439
|
-
for (const msg of found) {
|
|
1440
|
-
console.log(` ${sev
|
|
1441
|
-
ghAnnotate(sev
|
|
1509
|
+
for (const { msg, sev } of found) {
|
|
1510
|
+
console.log(` ${markOf(sev).mark} ${msg}`);
|
|
1511
|
+
ghAnnotate(markOf(sev).level, msg);
|
|
1442
1512
|
}
|
|
1443
1513
|
}
|
|
1444
1514
|
if (overBudget.length > 0 && !silent) {
|
|
1445
1515
|
console.log("\nCompile budget check (compile-budget):\n");
|
|
1446
|
-
overBudget.forEach((msg) => {
|
|
1516
|
+
overBudget.forEach(({ msg }) => {
|
|
1447
1517
|
console.log(` ✗ ${msg}`);
|
|
1448
1518
|
ghAnnotate("error", `compile-budget: ${msg}`);
|
|
1449
1519
|
});
|
|
1450
1520
|
}
|
|
1451
|
-
return {
|
|
1452
|
-
issues: found.length,
|
|
1453
|
-
errors: sev === "error" ? found.length : 0,
|
|
1454
|
-
budgetErrors: overBudget.length,
|
|
1455
|
-
};
|
|
1521
|
+
return { ...tally(found), budgetErrors: overBudget.length };
|
|
1456
1522
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
* SAME compilers through this, because a read must never write.
|
|
1462
|
-
*
|
|
1463
|
-
* Why it exists (#190): `checkSpecRefs` used to call `compileClaude` directly and
|
|
1464
|
-
* skip every other spec type, so a SKILL.md / subagent / railway artifact
|
|
1465
|
-
* committed with a since-deleted reference hashed cleanly against its own header
|
|
1466
|
-
* forever — `lint` green, `compile` red. Measured on this repo:
|
|
1467
|
-
* `examples/SKILL.md.spec.ts` names `skills/enforce-rules-format/SKILL.md`, which
|
|
1468
|
-
* does not exist (the skill lives under `.claude/skills/`), and `lint` reported
|
|
1469
|
-
* `hash valid`.
|
|
1470
|
-
*
|
|
1471
|
-
* A switch rather than a per-type "ref accessor": all four compilers already
|
|
1472
|
-
* return the same `errors: CompileError[]`, so there is nothing to normalize —
|
|
1473
|
-
* only the options differ. Exhaustive over `_specType`, so a FIFTH spec type is
|
|
1474
|
-
* a tsc error here instead of a silent skip, which is the failure this closes.
|
|
1475
|
-
*/
|
|
1476
|
-
async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
1477
|
-
const basePath = process.cwd();
|
|
1523
|
+
async function specCompileErrors(spec, specPath, { root, dialect, config, knownAgents }) {
|
|
1524
|
+
const basePath = root;
|
|
1525
|
+
// The compiled file this spec writes — `overrides` resolve at its path.
|
|
1526
|
+
const target = specPath.replace(/\.spec\.ts$/, "");
|
|
1478
1527
|
switch (spec._specType) {
|
|
1479
1528
|
case "claude": {
|
|
1480
1529
|
const r = await (0, compile_js_1.compileClaude)(spec, {
|
|
@@ -1486,7 +1535,7 @@ async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
|
1486
1535
|
maxSectionLines: config?.maxSectionLines,
|
|
1487
1536
|
catalogOnly: config?.catalogOnly,
|
|
1488
1537
|
linters: config?.linters,
|
|
1489
|
-
...compileBudgetOptions(config),
|
|
1538
|
+
...compileBudgetOptions(config, target),
|
|
1490
1539
|
});
|
|
1491
1540
|
// A size finding is `compile-budget`'s, not a dead reference: split by
|
|
1492
1541
|
// IDENTITY against the compiler's own list, never by the type's name.
|
|
@@ -1502,7 +1551,13 @@ async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
|
1502
1551
|
};
|
|
1503
1552
|
case "agent":
|
|
1504
1553
|
return {
|
|
1505
|
-
errors: (await (0, compile_js_1.compileAgent)(spec, {
|
|
1554
|
+
errors: (await (0, compile_js_1.compileAgent)(spec, {
|
|
1555
|
+
basePath,
|
|
1556
|
+
specFile: specPath,
|
|
1557
|
+
dialect,
|
|
1558
|
+
catalogOnly: config?.catalogOnly,
|
|
1559
|
+
linters: config?.linters,
|
|
1560
|
+
})).errors,
|
|
1506
1561
|
budget: [],
|
|
1507
1562
|
};
|
|
1508
1563
|
case "railway":
|
|
@@ -1518,17 +1573,18 @@ async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
|
1518
1573
|
}
|
|
1519
1574
|
}
|
|
1520
1575
|
/**
|
|
1521
|
-
* The `compile-budget` rule, as `compileClaude` options: its severity
|
|
1522
|
-
*
|
|
1523
|
-
* and `lint` (which re-derives)
|
|
1576
|
+
* The `compile-budget` rule, as `compileClaude` options: its severity at the
|
|
1577
|
+
* compiled `target` (`overrides` applied) and any thresholds `rules` set. The
|
|
1578
|
+
* ONE translation, so `compile` (which writes) and `lint` (which re-derives)
|
|
1579
|
+
* can never measure against different numbers.
|
|
1524
1580
|
*/
|
|
1525
|
-
function compileBudgetOptions(config) {
|
|
1581
|
+
function compileBudgetOptions(config, target) {
|
|
1526
1582
|
const rule = config?.rules?.["compile-budget"];
|
|
1527
1583
|
// No config at all (a programmatic caller, a missing file) is the default.
|
|
1528
1584
|
if (rule === undefined)
|
|
1529
1585
|
return { budgetSeverity: "warn" };
|
|
1530
1586
|
return {
|
|
1531
|
-
budgetSeverity: (0,
|
|
1587
|
+
budgetSeverity: (0, overrides_js_1.ruleResolver)(config?.rules ?? validate_js_1.DEFAULT_RULES, config?.overrides).severityAt("compile-budget", target),
|
|
1532
1588
|
...(0, types_js_1.ruleOptions)(rule),
|
|
1533
1589
|
};
|
|
1534
1590
|
}
|
|
@@ -1545,6 +1601,8 @@ function writeArtifact(outputPath, artifact) {
|
|
|
1545
1601
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), artifact);
|
|
1546
1602
|
}
|
|
1547
1603
|
async function runLint(restArgs, flags, excludes, config) {
|
|
1604
|
+
// The project root the CLI resolved once; the excludes are rooted at it.
|
|
1605
|
+
const root = excludes.root;
|
|
1548
1606
|
const summary = flags.includes("--summary");
|
|
1549
1607
|
const json = flags.includes("--json");
|
|
1550
1608
|
const silent = summary || json;
|
|
@@ -1561,7 +1619,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1561
1619
|
(0, node_fs_1.existsSync)(positional[0]) &&
|
|
1562
1620
|
(0, node_fs_1.lstatSync)(positional[0]).isDirectory()
|
|
1563
1621
|
? (0, node_path_1.resolve)(positional[0])
|
|
1564
|
-
:
|
|
1622
|
+
: root;
|
|
1565
1623
|
// Resolve the active harness ONCE so the harness-specific checks below run
|
|
1566
1624
|
// against the right adapter's dialect (tool/event catalogs) and surfaces —
|
|
1567
1625
|
// not a hard-coded Claude Code default. A subagent-surface rule reports n/a
|
|
@@ -1595,8 +1653,9 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1595
1653
|
// ONE frame for the whole run (#281): every path a check prints or annotates is
|
|
1596
1654
|
// relative to it, and config globs resolve from it — whichever bundle is being
|
|
1597
1655
|
// scored. Built here, once, instead of each option re-deriving its own root.
|
|
1598
|
-
const frame = (0, frame_js_1.frameFor)(
|
|
1599
|
-
const
|
|
1656
|
+
const frame = (0, frame_js_1.frameFor)(root, scanRoot);
|
|
1657
|
+
const rules = (0, overrides_js_1.ruleResolver)(config?.rules ?? validate_js_1.DEFAULT_RULES, config?.overrides);
|
|
1658
|
+
const run = { config, rules, silent, adapter, excludes, frame };
|
|
1600
1659
|
if (!silent && nestedBundles.length > 0) {
|
|
1601
1660
|
const rel = nestedBundles.map((b) => (0, node_path_1.relative)(scanRoot, b) || b);
|
|
1602
1661
|
console.log(scoreAll
|
|
@@ -1622,7 +1681,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1622
1681
|
}
|
|
1623
1682
|
}
|
|
1624
1683
|
const hashResult = files.length > 0
|
|
1625
|
-
? check(frame, files, silent)
|
|
1684
|
+
? check(root, frame, files, silent)
|
|
1626
1685
|
: { valid: true, hashErrors: 0, validationErrors: 0 };
|
|
1627
1686
|
// 1b. Verify inline + frontmatter rules in instruction files not managed
|
|
1628
1687
|
// by a spec. See verifyMarkdownModeRules / docs/markdown-mode.md.
|
|
@@ -1637,16 +1696,15 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1637
1696
|
// fail on unrelated duplicates elsewhere in the repo.
|
|
1638
1697
|
if (!silent)
|
|
1639
1698
|
console.log("\nDuplicate rule detection:\n");
|
|
1640
|
-
const dups = await findDuplicateRules(excludes, 0.3, silent, restArgs.length > 0 ? files : undefined);
|
|
1699
|
+
const dups = await findDuplicateRules(excludes, 0.3, silent, restArgs.length > 0 ? files : undefined, (specPath) => rules.severityAt("duplicate-rules", specPath));
|
|
1641
1700
|
// 4. Guidance rule count (strengthen suggestions moved to /strengthen skill)
|
|
1642
1701
|
const guidanceCount = await countGuidanceRules(excludes, silent);
|
|
1643
1702
|
// 5. Integrity check (hand-edit detection via SHA-256 hash)
|
|
1644
|
-
const integritySeverity = config?.rules.integrity ?? "warn";
|
|
1645
1703
|
let integrityErrors = 0;
|
|
1646
|
-
if (
|
|
1704
|
+
if (rules.mayFire("integrity")) {
|
|
1647
1705
|
if (!silent)
|
|
1648
1706
|
console.log("\nIntegrity check:\n");
|
|
1649
|
-
integrityErrors = checkIntegrityForFiles(files,
|
|
1707
|
+
integrityErrors = checkIntegrityForFiles(root, files, rules, silent);
|
|
1650
1708
|
}
|
|
1651
1709
|
// 6. Coverage thresholds (gates CI when severity is "error")
|
|
1652
1710
|
const coverageErrors = await checkCoverageThresholds(excludes, coverage, config, silent);
|
|
@@ -1664,8 +1722,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1664
1722
|
// always-blocking check and deleting the `orphans` block. `warn` now reports
|
|
1665
1723
|
// without touching the exit code and `false`/`"off"` skips the scan, exactly
|
|
1666
1724
|
// like every other rule.
|
|
1667
|
-
const
|
|
1668
|
-
const orphansCfg = orphanSeverity ? config?.orphans : undefined;
|
|
1725
|
+
const orphansCfg = rules.mayFire("orphan-docs") ? config?.orphans : undefined;
|
|
1669
1726
|
if (!silent)
|
|
1670
1727
|
console.log("\nOrphan docs check:\n");
|
|
1671
1728
|
let orphanReport = {
|
|
@@ -1675,12 +1732,10 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1675
1732
|
orphans: [],
|
|
1676
1733
|
};
|
|
1677
1734
|
if (orphansCfg) {
|
|
1678
|
-
|
|
1679
|
-
basePath:
|
|
1735
|
+
const found = (0, orphans_js_1.findOrphanDocs)({
|
|
1736
|
+
basePath: root,
|
|
1680
1737
|
include: orphansCfg.include,
|
|
1681
|
-
exclude
|
|
1682
|
-
// The repo-wide `exclude` is the FLOOR under the rule's own `exclude`
|
|
1683
|
-
// (union, never override): an excluded corpus is neither an orphan
|
|
1738
|
+
// The repo-wide `exclude`: an excluded corpus is neither an orphan
|
|
1684
1739
|
// candidate nor a source of references that keep a doc alive (#192).
|
|
1685
1740
|
repoExclude: excludes.globIgnore,
|
|
1686
1741
|
// Exempt every registered harness's surface files (instruction file,
|
|
@@ -1688,6 +1743,10 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1688
1743
|
// core carries no harness literal (see src/core/orphans.ts).
|
|
1689
1744
|
layouts: adapter_registry_js_1.ADAPTERS.map((a) => a.layout),
|
|
1690
1745
|
});
|
|
1746
|
+
// Per doc: an `overrides` entry with `orphan-docs` off drops it from the
|
|
1747
|
+
// report (it still counts as a doc that references others).
|
|
1748
|
+
const orphanGraded = graded(rules, "orphan-docs", found.orphans.map((doc) => ({ doc })), (o) => o.doc);
|
|
1749
|
+
orphanReport = { ...found, orphans: orphanGraded.map((o) => o.doc) };
|
|
1691
1750
|
if (!silent) {
|
|
1692
1751
|
for (const line of (0, orphans_js_1.formatOrphanReport)(orphanReport).split("\n")) {
|
|
1693
1752
|
console.log(` ${line}`);
|
|
@@ -1702,7 +1761,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1702
1761
|
// hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
|
|
1703
1762
|
// A compiled artifact's refs, re-derived from its spec — the hash says the file
|
|
1704
1763
|
// is unchanged, not that what it names still exists (#173).
|
|
1705
|
-
const specRefs = await checkSpecRefs(excludes, config, silent, adapter.dialect);
|
|
1764
|
+
const specRefs = await checkSpecRefs(excludes, config, rules, silent, adapter.dialect);
|
|
1706
1765
|
const untested = overBundles(checkUntestedSurfaces, run, lintRoots);
|
|
1707
1766
|
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
1708
1767
|
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
@@ -1728,6 +1787,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1728
1787
|
// validate`"; measured false on 2026-09-08 (Claude Code 2.1.263) — see
|
|
1729
1788
|
// docs/rules/hook-script-exists.md and tools/measure-validate-overlap.mjs.
|
|
1730
1789
|
const hookScripts = overBundles(checkHookScriptExists, run, lintRoots);
|
|
1790
|
+
// 7i². Hand-written hooks — every hook line vigiles did not write, by name.
|
|
1791
|
+
const handWrittenHooks = overBundles(checkHandWrittenHooks, run, lintRoots);
|
|
1731
1792
|
// 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
|
|
1732
1793
|
// (the deny-side mirror of subagent-tool-contract; close-typo only).
|
|
1733
1794
|
const disallowedTools = overBundles(checkDisallowedTools, run, lintRoots);
|
|
@@ -1744,9 +1805,6 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1744
1805
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
1745
1806
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
1746
1807
|
const mcpHookTargets = overBundles(checkMcpHookTargets, run, lintRoots);
|
|
1747
|
-
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
1748
|
-
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
1749
|
-
const preferCompiledHooks = overBundles(checkPreferCompiledHooks, run, lintRoots);
|
|
1750
1808
|
// 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
|
|
1751
1809
|
// hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
|
|
1752
1810
|
// prompt-injection exfil path (Rule of Two). Capability SET-intersection.
|
|
@@ -1777,6 +1835,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1777
1835
|
const instructionWeight = (0, instruction_weight_ratchet_js_1.checkInstructionWeightRatchet)({
|
|
1778
1836
|
root: frame.root,
|
|
1779
1837
|
severity: (0, types_js_1.ruleSeverity)(config?.rules?.["instruction-weight"]),
|
|
1838
|
+
max: (0, types_js_1.ruleOptions)(config?.rules?.["instruction-weight"])?.max,
|
|
1780
1839
|
update: flags.includes("--update-baseline"),
|
|
1781
1840
|
silent,
|
|
1782
1841
|
measure: () => lintRoots.map((abs) => [
|
|
@@ -1791,11 +1850,12 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1791
1850
|
// over the repository, like the ratchet: a nested bundle's spec would
|
|
1792
1851
|
// otherwise be reported again by every bundle that encloses it.
|
|
1793
1852
|
const deprecatedKeyFiles = (0, deprecated_key_files_js_1.checkDeprecatedKeyFiles)({
|
|
1794
|
-
severity:
|
|
1795
|
-
|
|
1853
|
+
severity: rules.mayFire("deprecated-key-files") ? "warn" : false,
|
|
1854
|
+
severityAt: (path) => rules.severityAt("deprecated-key-files", path),
|
|
1855
|
+
find: () => specsUsingKeyFiles(root, excludes.globIgnore),
|
|
1796
1856
|
silent,
|
|
1797
1857
|
annotate: (level, message, path) => {
|
|
1798
|
-
ghAnnotate(level, message, frame.repo((0, node_path_1.resolve)(
|
|
1858
|
+
ghAnnotate(level, message, frame.repo((0, node_path_1.resolve)(root, path)));
|
|
1799
1859
|
},
|
|
1800
1860
|
});
|
|
1801
1861
|
// 8. Validate vigiles builder calls inside markdown code blocks — the
|
|
@@ -1818,7 +1878,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1818
1878
|
// globs without `dot`, so `.claude/**` — where real skills and agents live —
|
|
1819
1879
|
// has never been scanned. The rule has never once looked at a deployed
|
|
1820
1880
|
// instruction file; every ref it has ever judged was in prose.
|
|
1821
|
-
const docRefSeverity =
|
|
1881
|
+
const docRefSeverity = rules.mayFire("doc-refs");
|
|
1822
1882
|
if (!silent && docRefSeverity)
|
|
1823
1883
|
console.log("\nMarkdown code block refs:\n");
|
|
1824
1884
|
// 🔴 `config.exclude` REACHES THIS PASS. It did not, and that single omission is
|
|
@@ -1834,8 +1894,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1834
1894
|
// With no way to reach 0 the step was made `continue-on-error: true`, and a lint
|
|
1835
1895
|
// whose exit code is discarded gates nothing — after which hand-written CI steps
|
|
1836
1896
|
// grew to do the gating instead. One unpassed argument, that whole chain.
|
|
1837
|
-
const
|
|
1838
|
-
? (0, doc_refs_js_1.findDocRefs)({ basePath:
|
|
1897
|
+
const docRefFound = docRefSeverity
|
|
1898
|
+
? (0, doc_refs_js_1.findDocRefs)({ basePath: root, ignore: excludes.globIgnore })
|
|
1839
1899
|
: {
|
|
1840
1900
|
filesScanned: 0,
|
|
1841
1901
|
filesIgnored: 0,
|
|
@@ -1845,10 +1905,24 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1845
1905
|
unverified: 0,
|
|
1846
1906
|
placeholders: 0,
|
|
1847
1907
|
};
|
|
1908
|
+
// Per file: an `overrides` entry may turn `doc-refs` off, or set its
|
|
1909
|
+
// severity, for a folder. Printed once per severity present, so a `✗` never
|
|
1910
|
+
// stands next to a finding that does not gate.
|
|
1911
|
+
const docRefErrors = graded(rules, "doc-refs", docRefFound.errors, (e) => e.file);
|
|
1912
|
+
const docRefReport = { ...docRefFound, errors: docRefErrors };
|
|
1848
1913
|
if (!silent && docRefSeverity) {
|
|
1849
|
-
const
|
|
1850
|
-
|
|
1851
|
-
|
|
1914
|
+
const groups = ["error", "warn"]
|
|
1915
|
+
.map((sev) => ({
|
|
1916
|
+
sev,
|
|
1917
|
+
errors: docRefErrors.filter((e) => e.sev === sev),
|
|
1918
|
+
}))
|
|
1919
|
+
.filter((g, i, all) => g.errors.length > 0 ||
|
|
1920
|
+
(i === 0 && all.every((x) => x.errors.length === 0)));
|
|
1921
|
+
for (const g of groups) {
|
|
1922
|
+
const rendered = (0, doc_refs_js_1.formatDocRefReport)({ ...docRefFound, errors: g.errors }, g.sev);
|
|
1923
|
+
for (const line of rendered.split("\n")) {
|
|
1924
|
+
console.log(` ${line}`);
|
|
1925
|
+
}
|
|
1852
1926
|
}
|
|
1853
1927
|
}
|
|
1854
1928
|
// Per-line GitHub annotations for each broken doc ref — each carries file+line,
|
|
@@ -1858,7 +1932,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1858
1932
|
// on the PR. CI-only (isGitHubActions); skipped under --json/--summary.
|
|
1859
1933
|
if (isGitHubActions() && !silent && docRefSeverity) {
|
|
1860
1934
|
for (const e of docRefReport.errors) {
|
|
1861
|
-
ghAnnotate(
|
|
1935
|
+
ghAnnotate(markOf(e.sev).level, `${e.kind}("${e.value}") — ${e.message}`, frame.repo((0, node_path_1.resolve)(root, e.file)), e.line);
|
|
1862
1936
|
}
|
|
1863
1937
|
}
|
|
1864
1938
|
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
@@ -1877,14 +1951,16 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1877
1951
|
specRefErrors: specRefs.errors,
|
|
1878
1952
|
compileBudgetErrors: specRefs.budgetErrors,
|
|
1879
1953
|
duplicatePairs: dups.pairCount,
|
|
1880
|
-
duplicateSeverity:
|
|
1954
|
+
duplicateSeverity: dups.errorPairs > 0 ? "error" : "warn",
|
|
1881
1955
|
coverageEnabled: coverage.enabled,
|
|
1882
1956
|
coverageDocumented: coverage.documented,
|
|
1883
1957
|
strengthenSuggestions: guidanceCount,
|
|
1884
1958
|
integrityErrors,
|
|
1885
1959
|
coverageErrors,
|
|
1886
1960
|
orphanCount: orphanReport.orphans.length,
|
|
1887
|
-
orphanSeverity:
|
|
1961
|
+
orphanSeverity: orphanReport.orphans.some((doc) => rules.severityAt("orphan-docs", doc) === "error")
|
|
1962
|
+
? "error"
|
|
1963
|
+
: "warn",
|
|
1888
1964
|
untestedSurfaces: untested.untested,
|
|
1889
1965
|
untestedErrors: untested.errors,
|
|
1890
1966
|
toolContractIssues: toolContract.issues,
|
|
@@ -1901,6 +1977,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1901
1977
|
mcpToolErrors: mcpToolResolves.errors,
|
|
1902
1978
|
hookScriptIssues: hookScripts.issues,
|
|
1903
1979
|
hookScriptErrors: hookScripts.errors,
|
|
1980
|
+
handWrittenHookIssues: handWrittenHooks.issues,
|
|
1981
|
+
handWrittenHookErrors: handWrittenHooks.errors,
|
|
1904
1982
|
disallowedToolIssues: disallowedTools.issues,
|
|
1905
1983
|
disallowedToolErrors: disallowedTools.errors,
|
|
1906
1984
|
descriptionOverlapIssues: descriptionOverlap.issues,
|
|
@@ -1911,8 +1989,6 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1911
1989
|
frontmatterValidErrors: frontmatterValid.errors,
|
|
1912
1990
|
mcpHookIssues: mcpHookTargets.issues,
|
|
1913
1991
|
mcpHookErrors: mcpHookTargets.errors,
|
|
1914
|
-
preferCompiledHookIssues: preferCompiledHooks.issues,
|
|
1915
|
-
preferCompiledHookErrors: preferCompiledHooks.errors,
|
|
1916
1992
|
lethalTrifectaIssues: lethalTrifecta.issues,
|
|
1917
1993
|
lethalTrifectaErrors: lethalTrifecta.errors,
|
|
1918
1994
|
skillResourceIssues: skillResources.issues,
|
|
@@ -1934,7 +2010,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1934
2010
|
// Only the "error" tier gates. At "warn" the findings are printed and
|
|
1935
2011
|
// annotated, and the exit code is untouched — same contract as every other
|
|
1936
2012
|
// opt-in rule here.
|
|
1937
|
-
docRefErrors:
|
|
2013
|
+
docRefErrors: docRefErrors.filter((e) => e.sev === "error").length,
|
|
1938
2014
|
symbolRefErrors,
|
|
1939
2015
|
mcpRefErrors,
|
|
1940
2016
|
files,
|
|
@@ -2020,10 +2096,9 @@ function collectDocumentedRules(excludes) {
|
|
|
2020
2096
|
});
|
|
2021
2097
|
for (const mdFile of mdFiles) {
|
|
2022
2098
|
const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), mdFile), "utf-8");
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
documented.add(m[1]);
|
|
2099
|
+
// Both compiled spellings (#351): an unverified claim is still documented.
|
|
2100
|
+
for (const m of content.matchAll(new RegExp(enforce_marker_js_1.RULE_MARKER_SOURCE, "g"))) {
|
|
2101
|
+
documented.add(m[2]);
|
|
2027
2102
|
}
|
|
2028
2103
|
}
|
|
2029
2104
|
return documented;
|
|
@@ -2078,10 +2153,12 @@ function discover(excludes, silent = false) {
|
|
|
2078
2153
|
}
|
|
2079
2154
|
const totalPct = totalEnabled > 0 ? Math.round((totalDocumented / totalEnabled) * 100) : 0;
|
|
2080
2155
|
log(`Coverage: ${String(totalDocumented)}/${String(totalEnabled)} rules documented (${String(totalPct)}%)`);
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
}
|
|
2156
|
+
const advice = (0, coverage_js_1.undocumentedRulesAdvice)({
|
|
2157
|
+
enabled: totalEnabled,
|
|
2158
|
+
documented: totalDocumented,
|
|
2159
|
+
});
|
|
2160
|
+
if (advice.length > 0)
|
|
2161
|
+
log(["", ...advice].join("\n"));
|
|
2085
2162
|
return { enabled: totalEnabled, documented: totalDocumented };
|
|
2086
2163
|
}
|
|
2087
2164
|
/** This package's own version (from the installed package.json). */
|
|
@@ -2177,21 +2254,40 @@ function discoverAdoptableSurfaces(cwd) {
|
|
|
2177
2254
|
}
|
|
2178
2255
|
return out;
|
|
2179
2256
|
}
|
|
2180
|
-
/**
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2257
|
+
/** Every hand-written instruction file under `root` — root AND nested
|
|
2258
|
+
* `CLAUDE.md` / `AGENTS.md`, the same files `gatherInstructionFiles` reads —
|
|
2259
|
+
* by the one predicate `lint`'s `require-instructions-spec` asks
|
|
2260
|
+
* (`instructionSpecState`). Measured on a repo with six hand-written files
|
|
2261
|
+
* (root + five nested): `audit` counted 1 (root only) while `lint` warned about
|
|
2262
|
+
* all six. A spec-managed or opted-out file is not listed. */
|
|
2263
|
+
function handWrittenInstructionFiles(root, instructionFile, excludes, rules) {
|
|
2264
|
+
return (gatherInstructionFiles(root, instructionFile, excludes)
|
|
2265
|
+
.filter((f) => (0, integrity_js_1.instructionSpecState)(f.text, (0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, `${f.path}.spec.ts`))) === "hand-written")
|
|
2266
|
+
// A folder an `overrides` entry turned the rule off for keeps its files
|
|
2267
|
+
// hand-written on purpose: that decline is remembered, not re-offered.
|
|
2268
|
+
.filter((f) => rules.severityAt("require-instructions-spec", f.path) !== false)
|
|
2269
|
+
.map((f) => f.path));
|
|
2270
|
+
}
|
|
2271
|
+
/** The hand-written instruction files BELOW the root — the ones `init`'s
|
|
2272
|
+
* root-level `determineTargets` does not see. Bare `init` adopts them too, so a
|
|
2273
|
+
* repo it sets up passes `require-instructions-spec` (in `vigiles:recommended`)
|
|
2274
|
+
* on its first `lint`. Read with the repo's own `exclude`. */
|
|
2275
|
+
function nestedHandWrittenInstructionFiles(cwd) {
|
|
2276
|
+
const config = (0, validate_js_1.loadConfig)();
|
|
2277
|
+
return handWrittenInstructionFiles(cwd, adapter_registry_js_1.defaultAdapter.layout.instructionFile, (0, exclude_js_1.excludeSet)(cwd, config.exclude), (0, overrides_js_1.ruleResolver)(config.rules, config.overrides)).filter((p) => p.includes("/"));
|
|
2278
|
+
}
|
|
2279
|
+
/** The full adoptable-surface list `audit` reports: every hand-written
|
|
2280
|
+
* instruction file (nested included) PLUS every skill/subagent surface without
|
|
2281
|
+
* a spec. Same notion `init` adopts; surfaced in the AuditReport + the terminal
|
|
2282
|
+
* nudge so the report's "Create spec" / "Create all specs" affordances have
|
|
2283
|
+
* their paths. Composition-root only — CC paths are intentional here (like
|
|
2284
|
+
* discoverAdoptableSurfaces). */
|
|
2285
|
+
function discoverAdoptableForAudit(root, { instructionFile, excludes, config, }) {
|
|
2286
|
+
const rules = (0, overrides_js_1.ruleResolver)(config.rules, config.overrides);
|
|
2287
|
+
return [
|
|
2288
|
+
...handWrittenInstructionFiles(root, instructionFile, excludes, rules),
|
|
2289
|
+
...discoverAdoptableSurfaces(root),
|
|
2290
|
+
].filter((p) => !(0, exclude_js_1.excludedBy)(excludes)((0, node_path_1.resolve)(root, p)));
|
|
2195
2291
|
}
|
|
2196
2292
|
/** Lint-config file BASENAMES whose CONTENTS (textual, NEVER executed) reveal
|
|
2197
2293
|
* which rules a repo has configured — read best-effort for the rule-inventory
|
|
@@ -2424,32 +2520,47 @@ function computeRuleRouting(root, instructionFile, excludes) {
|
|
|
2424
2520
|
return undefined;
|
|
2425
2521
|
}
|
|
2426
2522
|
}
|
|
2427
|
-
/** The terminal "adoptable surfaces" nudge
|
|
2428
|
-
*
|
|
2523
|
+
/** The terminal "adoptable surfaces" nudge. Instruction files are listed in
|
|
2524
|
+
* full (up to {@link NUDGE_LIST_CAP}) with the shared adoption remedy — they
|
|
2525
|
+
* have two adopters, `init --target` and the `adopt-spec` skill; skills and
|
|
2526
|
+
* subagents keep the per-surface `init --target` command. "" when nothing to
|
|
2429
2527
|
* adopt (a fully spec-managed repo says nothing). */
|
|
2430
2528
|
function formatAdoptableNudge(surfaces) {
|
|
2431
2529
|
if (surfaces.length === 0)
|
|
2432
2530
|
return "";
|
|
2531
|
+
const instructions = surfaces.filter((s) => surfaceKind(s) === "instruction");
|
|
2532
|
+
const others = surfaces.filter((s) => surfaceKind(s) !== "instruction");
|
|
2433
2533
|
const n = surfaces.length;
|
|
2534
|
+
const capped = (items, line) => {
|
|
2535
|
+
const shown = items.slice(0, NUDGE_LIST_CAP).map(line);
|
|
2536
|
+
const more = items.length - shown.length;
|
|
2537
|
+
return more > 0
|
|
2538
|
+
? [...shown, ` • +${String(more)} more (all in --json)`]
|
|
2539
|
+
: shown;
|
|
2540
|
+
};
|
|
2434
2541
|
// Lead with the VALUE, not the chore: a spec is the pull up from the gate to
|
|
2435
2542
|
// the richer layer, so say what it BUYS (drift-proof references + testable),
|
|
2436
2543
|
// not just "create one". The invitation is where richer-feature adoption is
|
|
2437
2544
|
// won — keep it compelling, never a bare to-do (research/adoption-design.md §1).
|
|
2438
2545
|
const lines = [
|
|
2439
2546
|
`ℹ ${String(n)} surface${n === 1 ? "" : "s"} could be spec-managed — a spec re-verifies its references on every change (a hand-edit can't silently break them) and makes it testable.`,
|
|
2440
|
-
` Adopt now, or one at a time with \`npx vigiles init --target=<path>\`:`,
|
|
2441
2547
|
];
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
lines.push(`
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2548
|
+
if (instructions.length > 0) {
|
|
2549
|
+
const k = instructions.length;
|
|
2550
|
+
lines.push(` ${String(k)} hand-written instruction file${k === 1 ? "" : "s"}:`, ...capped(instructions, (s) => ` • ${s}`), ...(0, instruction_remedies_js_1.adoptInstructionFileRemedy)("<path>")
|
|
2551
|
+
.split("\n")
|
|
2552
|
+
.map((l) => ` ${l}`));
|
|
2553
|
+
}
|
|
2554
|
+
if (others.length > 0) {
|
|
2555
|
+
lines.push(` Skills and subagents, one at a time with \`npx vigiles init --target=<path>\`:`, ...capped(others, (s) => ` • npx vigiles init --target=${s}`));
|
|
2556
|
+
}
|
|
2448
2557
|
// The remembered-decline affordance (non-evil contract): one keystroke to
|
|
2449
2558
|
// silence, stated where it's shown so it's never a surprise nag.
|
|
2450
2559
|
lines.push(` (set "nudge": "dismissed" in .vigilesrc.json to hide this)`);
|
|
2451
2560
|
return lines.join("\n");
|
|
2452
2561
|
}
|
|
2562
|
+
/** How many paths one group of the adoption nudge prints before "+N more". */
|
|
2563
|
+
const NUDGE_LIST_CAP = 10;
|
|
2453
2564
|
/** A small, terse behavioral nudge — the deterministic read can't tell whether a
|
|
2454
2565
|
* skill actually FIRES. "" when there are no model-invocable skills. */
|
|
2455
2566
|
function formatTriggerNudge(triggerableSkills) {
|
|
@@ -3167,7 +3278,11 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3167
3278
|
// file. An explicit --target stays scoped to that one surface.
|
|
3168
3279
|
const targets = targetValue
|
|
3169
3280
|
? instructionTargets
|
|
3170
|
-
: [
|
|
3281
|
+
: [
|
|
3282
|
+
...instructionTargets,
|
|
3283
|
+
...nestedHandWrittenInstructionFiles(cwd).filter((p) => !instructionTargets.includes(p)),
|
|
3284
|
+
...discoverAdoptableSurfaces(cwd),
|
|
3285
|
+
];
|
|
3171
3286
|
// Create specs. An existing hand-written target is faithfully ADOPTED into a
|
|
3172
3287
|
// spec (scaffoldSpec() does the convert), not clobbered with a blank one — so the
|
|
3173
3288
|
// compile below reproduces it (the user reviews the diff). A greenfield target
|
|
@@ -3198,6 +3313,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3198
3313
|
for (const l of typesResult.linters) {
|
|
3199
3314
|
console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
|
|
3200
3315
|
}
|
|
3316
|
+
printUnreadableLinters(typesResult.unreadable);
|
|
3201
3317
|
if (typesResult.scripts.length > 0) {
|
|
3202
3318
|
console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
|
|
3203
3319
|
}
|
|
@@ -3810,6 +3926,7 @@ async function setup(args) {
|
|
|
3810
3926
|
strict,
|
|
3811
3927
|
reportOnly: parsed.reportOnly,
|
|
3812
3928
|
lint: plan.lint,
|
|
3929
|
+
specs: plan.scaffoldSpecs,
|
|
3813
3930
|
written,
|
|
3814
3931
|
});
|
|
3815
3932
|
printSetupSummary({
|
|
@@ -3929,6 +4046,7 @@ function fullSetupGap(plan, detected, harnesses) {
|
|
|
3929
4046
|
const unspecced = pillar1
|
|
3930
4047
|
? [
|
|
3931
4048
|
...determineTargets(detected, undefined, [...harnesses]),
|
|
4049
|
+
...nestedHandWrittenInstructionFiles(cwd),
|
|
3932
4050
|
...discoverAdoptableSurfaces(cwd),
|
|
3933
4051
|
].filter((t, i, all) => all.indexOf(t) === i && !has(`${t}.spec.ts`))
|
|
3934
4052
|
: [];
|
|
@@ -4009,6 +4127,7 @@ function writeProjectConfig(opts) {
|
|
|
4009
4127
|
strict: opts.strict,
|
|
4010
4128
|
reportOnly: opts.reportOnly,
|
|
4011
4129
|
lint: opts.lint,
|
|
4130
|
+
specs: opts.specs,
|
|
4012
4131
|
});
|
|
4013
4132
|
if (!merged)
|
|
4014
4133
|
return;
|
|
@@ -4021,31 +4140,34 @@ function writeProjectConfig(opts) {
|
|
|
4021
4140
|
// ---------------------------------------------------------------------------
|
|
4022
4141
|
// Strengthen: guidance() → enforce() suggestions
|
|
4023
4142
|
// ---------------------------------------------------------------------------
|
|
4024
|
-
function checkIntegrityForFiles(files,
|
|
4143
|
+
function checkIntegrityForFiles(root, files, rules, silent) {
|
|
4025
4144
|
const log = (msg) => {
|
|
4026
4145
|
if (!silent)
|
|
4027
4146
|
console.log(msg);
|
|
4028
4147
|
};
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4148
|
+
const basePath = root;
|
|
4149
|
+
// Per file: an `overrides` entry may turn `integrity` off for a folder.
|
|
4150
|
+
const checked = files.flatMap((filePath) => {
|
|
4151
|
+
const severity = rules.severityAt("integrity", filePath);
|
|
4032
4152
|
const abs = (0, node_path_1.resolve)(basePath, filePath);
|
|
4033
|
-
if (!(0, node_fs_1.existsSync)(abs))
|
|
4034
|
-
|
|
4153
|
+
if (severity === false || !(0, node_fs_1.existsSync)(abs))
|
|
4154
|
+
return [];
|
|
4035
4155
|
const result = (0, integrity_js_1.checkIntegrity)((0, node_fs_1.readFileSync)(abs, "utf-8"));
|
|
4156
|
+
return [{ filePath, severity, result }];
|
|
4157
|
+
});
|
|
4158
|
+
for (const { filePath, severity, result } of checked) {
|
|
4036
4159
|
if (!result.intact) {
|
|
4037
|
-
|
|
4038
|
-
const marker = severity === "error" ? "✗" : "⚠";
|
|
4039
|
-
log(` ${marker} ${filePath} — ${result.reason ?? "tampered"}`);
|
|
4160
|
+
log(` ${markOf(severity).mark} ${filePath} — ${result.reason ?? "tampered"}`);
|
|
4040
4161
|
}
|
|
4041
4162
|
else if (!silent) {
|
|
4042
4163
|
log(` ✓ ${filePath}`);
|
|
4043
4164
|
}
|
|
4044
4165
|
}
|
|
4045
|
-
|
|
4166
|
+
const broken = checked.filter((c) => !c.result.intact);
|
|
4167
|
+
if (broken.length === 0) {
|
|
4046
4168
|
log(" All compiled files intact.");
|
|
4047
4169
|
}
|
|
4048
|
-
return severity === "error"
|
|
4170
|
+
return broken.filter((c) => c.severity === "error").length;
|
|
4049
4171
|
}
|
|
4050
4172
|
/**
|
|
4051
4173
|
* The layout of the harness this repo actually targets — `--harness=`/config/
|
|
@@ -4092,7 +4214,7 @@ const UNTESTED_RULE = {
|
|
|
4092
4214
|
/**
|
|
4093
4215
|
* The `untested-*` rules AS THE DETECTOR TAKES THEM: which kinds this repo
|
|
4094
4216
|
* enabled, and the discovery options merged from whichever of the three rules
|
|
4095
|
-
* carries them (`include` / `
|
|
4217
|
+
* carries them (`include` / `testExtension` are shared).
|
|
4096
4218
|
*
|
|
4097
4219
|
* 🔴 ONE READER, BECAUSE THE SECOND ONE DRIFTED. `vigiles lint` read the config
|
|
4098
4220
|
* here; the PostToolUse nudge (`evalLockNudgeHookCommand`) called the same
|
|
@@ -4103,25 +4225,24 @@ const UNTESTED_RULE = {
|
|
|
4103
4225
|
* while the nudge said "no test or eval covers it". A nudge contradicting the
|
|
4104
4226
|
* linter of the same repo, in the same second, teaches people to ignore both.
|
|
4105
4227
|
*/
|
|
4106
|
-
function untestedRules(config) {
|
|
4107
|
-
const rules = config?.rules;
|
|
4108
|
-
const severity = (kind) => (0, types_js_1.ruleSeverity)(rules?.[UNTESTED_RULE[kind]]);
|
|
4228
|
+
function untestedRules(config, rules) {
|
|
4109
4229
|
const names = Object.values(UNTESTED_RULE);
|
|
4110
4230
|
const opts = names.reduce((acc, rule) => ({
|
|
4111
4231
|
...acc,
|
|
4112
|
-
...(0, types_js_1.ruleOptions)(rules?.[rule]),
|
|
4232
|
+
...(0, types_js_1.ruleOptions)(config?.rules?.[rule]),
|
|
4113
4233
|
}), {});
|
|
4114
|
-
|
|
4234
|
+
// A kind is SCANNED when its rule can fire anywhere — an override may turn
|
|
4235
|
+
// it on for one folder while `rules` has it off.
|
|
4236
|
+
const on = (kind) => rules.mayFire(UNTESTED_RULE[kind]);
|
|
4115
4237
|
return {
|
|
4116
|
-
severity,
|
|
4117
|
-
anyEnabled: names.some((rule) =>
|
|
4238
|
+
severity: (kind, path) => rules.severityAt(UNTESTED_RULE[kind], path),
|
|
4239
|
+
anyEnabled: names.some((rule) => rules.mayFire(rule)),
|
|
4118
4240
|
options: {
|
|
4119
4241
|
skills: on("skill"),
|
|
4120
4242
|
agents: on("agent"),
|
|
4121
4243
|
hooks: on("hook"),
|
|
4122
4244
|
outputStyles: on("output-style"),
|
|
4123
4245
|
include: opts.include,
|
|
4124
|
-
exclude: opts.exclude,
|
|
4125
4246
|
// Without this the `testExtension` documented on TestCoverageOptions was a
|
|
4126
4247
|
// config key nothing read: a TypeScript-shaped repo got `.ts` suggestions
|
|
4127
4248
|
// however the author configured it. Prose isn't policy, in our own CLI.
|
|
@@ -4138,33 +4259,37 @@ function untestedRules(config) {
|
|
|
4138
4259
|
*/
|
|
4139
4260
|
function checkUntestedSurfaces(ctx) {
|
|
4140
4261
|
const { config, silent, adapter, frame, bundle } = ctx;
|
|
4141
|
-
const { severity: sevFor, anyEnabled, options } = untestedRules(config);
|
|
4262
|
+
const { severity: sevFor, anyEnabled, options, } = untestedRules(config, ctx.rules);
|
|
4142
4263
|
if (!anyEnabled)
|
|
4143
4264
|
return { untested: 0, errors: 0 };
|
|
4144
|
-
const
|
|
4265
|
+
const scanned = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
4145
4266
|
...options,
|
|
4146
4267
|
basePath: bundle.abs,
|
|
4147
|
-
// #281: `include
|
|
4268
|
+
// #281: `include` and every reported path are relative to the
|
|
4148
4269
|
// frame root (where `.vigilesrc.json` lives), not to the bundle being scored.
|
|
4149
4270
|
root: frame.root,
|
|
4150
4271
|
layout: adapter.layout,
|
|
4151
|
-
// The
|
|
4152
|
-
// it. Union, never override — a rule option must not re-admit a vendored
|
|
4153
|
-
// corpus the repo excluded (#192). Passed WHOLE so it carries its own root.
|
|
4272
|
+
// The repo-wide `exclude` — passed WHOLE so it carries its own root (#192).
|
|
4154
4273
|
excludes: ctx.excludes,
|
|
4155
4274
|
});
|
|
4275
|
+
// Per surface: an `overrides` entry may turn its kind's rule off for a folder.
|
|
4276
|
+
const untested = scanned.untested.flatMap((s) => {
|
|
4277
|
+
const sev = sevFor(s.kind, s.path);
|
|
4278
|
+
return sev === false ? [] : [{ ...s, sev }];
|
|
4279
|
+
});
|
|
4280
|
+
const report = { ...scanned, untested };
|
|
4156
4281
|
if (!silent) {
|
|
4157
4282
|
console.log("\nUntested surfaces:\n");
|
|
4158
4283
|
for (const line of (0, test_coverage_js_1.formatUntestedReport)(report).split("\n")) {
|
|
4159
4284
|
console.log(` ${line}`);
|
|
4160
4285
|
}
|
|
4161
4286
|
for (const s of report.untested) {
|
|
4162
|
-
ctx.annotate(
|
|
4287
|
+
ctx.annotate(markOf(s.sev).level, `${s.kind} ${s.path} ships without a test or eval`, frame.repo((0, node_path_1.join)(frame.root, s.path)));
|
|
4163
4288
|
}
|
|
4164
4289
|
}
|
|
4165
4290
|
return {
|
|
4166
4291
|
untested: report.untested.length,
|
|
4167
|
-
errors:
|
|
4292
|
+
errors: untested.filter((s) => s.sev === "error").length,
|
|
4168
4293
|
};
|
|
4169
4294
|
}
|
|
4170
4295
|
/**
|
|
@@ -4189,9 +4314,8 @@ function reportNotApplicable(check, surface, adapter, silent) {
|
|
|
4189
4314
|
* `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
|
|
4190
4315
|
*/
|
|
4191
4316
|
function checkSubagentToolContracts(ctx) {
|
|
4192
|
-
const {
|
|
4193
|
-
|
|
4194
|
-
if (!sev)
|
|
4317
|
+
const { silent, adapter } = ctx;
|
|
4318
|
+
if (!ctx.rules.mayFire("subagent-tool-contract"))
|
|
4195
4319
|
return { issues: 0, errors: 0 };
|
|
4196
4320
|
if (!adapter.subagents) {
|
|
4197
4321
|
reportNotApplicable("Subagent tool-contract check", "subagents", adapter, silent);
|
|
@@ -4207,24 +4331,16 @@ function checkSubagentToolContracts(ctx) {
|
|
|
4207
4331
|
catch {
|
|
4208
4332
|
return { issues: 0, errors: 0 };
|
|
4209
4333
|
}
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
if (!printedHeader) {
|
|
4218
|
-
console.log("\nSubagent tool-contract check:\n");
|
|
4219
|
-
printedHeader = true;
|
|
4220
|
-
}
|
|
4221
|
-
for (const issue of agent.toolIssues) {
|
|
4222
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(agent.path)}: ${issue.message}`);
|
|
4223
|
-
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(agent.path));
|
|
4224
|
-
}
|
|
4334
|
+
const found = graded(ctx.rules, "subagent-tool-contract", agents.flatMap((a) => a.toolIssues.map((i) => ({ path: a.path, message: i.message }))), (f) => ctx.bundle.scanned(f.path));
|
|
4335
|
+
if (found.length > 0 && !silent) {
|
|
4336
|
+
console.log("\nSubagent tool-contract check:\n");
|
|
4337
|
+
for (const issue of found) {
|
|
4338
|
+
const at = ctx.bundle.scanned(issue.path);
|
|
4339
|
+
console.log(` ${markOf(issue.sev).mark} ${at}: ${issue.message}`);
|
|
4340
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, at);
|
|
4225
4341
|
}
|
|
4226
4342
|
}
|
|
4227
|
-
return
|
|
4343
|
+
return tally(found);
|
|
4228
4344
|
}
|
|
4229
4345
|
/**
|
|
4230
4346
|
* Apply the `hook-events` rule: flag a hook registered under an event name the
|
|
@@ -4265,30 +4381,30 @@ function checkHookEvents(ctx) {
|
|
|
4265
4381
|
* `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
|
|
4266
4382
|
*/
|
|
4267
4383
|
function checkFrontmatterSchema(ctx) {
|
|
4268
|
-
const {
|
|
4269
|
-
|
|
4270
|
-
if (!sev)
|
|
4384
|
+
const { silent, adapter } = ctx;
|
|
4385
|
+
if (!ctx.rules.mayFire("subagent-frontmatter"))
|
|
4271
4386
|
return { issues: 0, errors: 0 };
|
|
4272
4387
|
if (!adapter.subagents) {
|
|
4273
4388
|
reportNotApplicable("Subagent-frontmatter check", "subagents", adapter, silent);
|
|
4274
4389
|
return { issues: 0, errors: 0 };
|
|
4275
4390
|
}
|
|
4276
|
-
let
|
|
4391
|
+
let raw;
|
|
4277
4392
|
try {
|
|
4278
4393
|
const r = ctx.scan();
|
|
4279
|
-
|
|
4394
|
+
raw = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
|
|
4280
4395
|
}
|
|
4281
4396
|
catch {
|
|
4282
4397
|
return { issues: 0, errors: 0 };
|
|
4283
4398
|
}
|
|
4399
|
+
const found = graded(ctx.rules, "subagent-frontmatter", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4284
4400
|
if (found.length > 0 && !silent) {
|
|
4285
4401
|
console.log("\nFrontmatter-schema check:\n");
|
|
4286
4402
|
for (const issue of found) {
|
|
4287
|
-
console.log(` ${sev
|
|
4288
|
-
ctx.annotate(sev
|
|
4403
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.label(issue.message)}`);
|
|
4404
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4289
4405
|
}
|
|
4290
4406
|
}
|
|
4291
|
-
return
|
|
4407
|
+
return tally(found);
|
|
4292
4408
|
}
|
|
4293
4409
|
/**
|
|
4294
4410
|
* Apply the `skill-frontmatter` rule: RECOMMEND (not require) that a SKILL.md
|
|
@@ -4299,54 +4415,25 @@ function checkFrontmatterSchema(ctx) {
|
|
|
4299
4415
|
* `skillMetaIssues`.
|
|
4300
4416
|
*/
|
|
4301
4417
|
function checkSkillFrontmatter(ctx) {
|
|
4302
|
-
const {
|
|
4303
|
-
|
|
4304
|
-
if (!sev)
|
|
4418
|
+
const { silent } = ctx;
|
|
4419
|
+
if (!ctx.rules.mayFire("skill-frontmatter"))
|
|
4305
4420
|
return { issues: 0, errors: 0 };
|
|
4306
|
-
let
|
|
4421
|
+
let raw;
|
|
4307
4422
|
try {
|
|
4308
|
-
|
|
4423
|
+
raw = ctx.scan().skillMetaIssues;
|
|
4309
4424
|
}
|
|
4310
4425
|
catch {
|
|
4311
4426
|
return { issues: 0, errors: 0 };
|
|
4312
4427
|
}
|
|
4428
|
+
const found = graded(ctx.rules, "skill-frontmatter", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4313
4429
|
if (found.length > 0 && !silent) {
|
|
4314
4430
|
console.log("\nSkill-frontmatter check:\n");
|
|
4315
4431
|
for (const issue of found) {
|
|
4316
|
-
console.log(` ${sev
|
|
4317
|
-
ctx.annotate(sev
|
|
4432
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.label(issue.message)}`);
|
|
4433
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4318
4434
|
}
|
|
4319
4435
|
}
|
|
4320
|
-
return
|
|
4321
|
-
}
|
|
4322
|
-
/**
|
|
4323
|
-
* Apply the `prefer-compiled-hooks` rule: a SINGLE repo-level recommendation
|
|
4324
|
-
* (one finding regardless of hook count) nudging hand-written hooks toward
|
|
4325
|
-
* compiled `vigiles/hook` artifacts. A discovery nudge, not a defect — the shell
|
|
4326
|
-
* lane stays first-class — so it fires once and the message links the guide.
|
|
4327
|
-
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
4328
|
-
*/
|
|
4329
|
-
function checkPreferCompiledHooks(ctx) {
|
|
4330
|
-
const { config, silent } = ctx;
|
|
4331
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
4332
|
-
if (!sev)
|
|
4333
|
-
return { issues: 0, errors: 0 };
|
|
4334
|
-
let count;
|
|
4335
|
-
try {
|
|
4336
|
-
count = ctx.scan().manualHookCount;
|
|
4337
|
-
}
|
|
4338
|
-
catch {
|
|
4339
|
-
return { issues: 0, errors: 0 };
|
|
4340
|
-
}
|
|
4341
|
-
if (count === 0)
|
|
4342
|
-
return { issues: 0, errors: 0 };
|
|
4343
|
-
const message = (0, scan_js_1.preferCompiledHooksMessage)(count);
|
|
4344
|
-
if (!silent) {
|
|
4345
|
-
console.log("\nCompiled-hooks check:\n");
|
|
4346
|
-
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${ctx.label(message)}`);
|
|
4347
|
-
ctx.annotate(sev === "error" ? "error" : "warning", message);
|
|
4348
|
-
}
|
|
4349
|
-
return { issues: 1, errors: sev === "error" ? 1 : 0 };
|
|
4436
|
+
return tally(found);
|
|
4350
4437
|
}
|
|
4351
4438
|
/**
|
|
4352
4439
|
* Apply the `mcp-config` rule: a declared MCP server with neither a `command`
|
|
@@ -4382,17 +4469,16 @@ function checkMcpConfig(ctx) {
|
|
|
4382
4469
|
* "error" gates CI.
|
|
4383
4470
|
*/
|
|
4384
4471
|
function checkDisallowedTools(ctx) {
|
|
4385
|
-
const {
|
|
4386
|
-
|
|
4387
|
-
if (!sev)
|
|
4472
|
+
const { silent, adapter } = ctx;
|
|
4473
|
+
if (!ctx.rules.mayFire("disallowed-tools-contract"))
|
|
4388
4474
|
return { issues: 0, errors: 0 };
|
|
4389
4475
|
if (!adapter.subagents) {
|
|
4390
4476
|
reportNotApplicable("Disallowed-tools check", "subagents", adapter, silent);
|
|
4391
4477
|
return { issues: 0, errors: 0 };
|
|
4392
4478
|
}
|
|
4393
|
-
let
|
|
4479
|
+
let raw;
|
|
4394
4480
|
try {
|
|
4395
|
-
|
|
4481
|
+
raw = ctx.scan().agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({
|
|
4396
4482
|
message: i.message,
|
|
4397
4483
|
path: a.path,
|
|
4398
4484
|
})));
|
|
@@ -4400,14 +4486,15 @@ function checkDisallowedTools(ctx) {
|
|
|
4400
4486
|
catch {
|
|
4401
4487
|
return { issues: 0, errors: 0 };
|
|
4402
4488
|
}
|
|
4489
|
+
const found = graded(ctx.rules, "disallowed-tools-contract", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4403
4490
|
if (found.length > 0 && !silent) {
|
|
4404
4491
|
console.log("\nDisallowed-tools check:\n");
|
|
4405
4492
|
for (const issue of found) {
|
|
4406
|
-
console.log(` ${sev
|
|
4407
|
-
ctx.annotate(sev
|
|
4493
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.bundle.scanned(issue.path)}: ${issue.message}`);
|
|
4494
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4408
4495
|
}
|
|
4409
4496
|
}
|
|
4410
|
-
return
|
|
4497
|
+
return tally(found);
|
|
4411
4498
|
}
|
|
4412
4499
|
/**
|
|
4413
4500
|
* Apply the `frontmatter-valid` rule: a skill/agent `---` block that EXISTS but
|
|
@@ -4418,25 +4505,25 @@ function checkDisallowedTools(ctx) {
|
|
|
4418
4505
|
* (verify before setting "error").
|
|
4419
4506
|
*/
|
|
4420
4507
|
function checkFrontmatterValid(ctx) {
|
|
4421
|
-
const {
|
|
4422
|
-
|
|
4423
|
-
if (!sev)
|
|
4508
|
+
const { silent } = ctx;
|
|
4509
|
+
if (!ctx.rules.mayFire("frontmatter-valid"))
|
|
4424
4510
|
return { issues: 0, errors: 0 };
|
|
4425
|
-
let
|
|
4511
|
+
let raw;
|
|
4426
4512
|
try {
|
|
4427
|
-
|
|
4513
|
+
raw = ctx.scan().malformedFrontmatter;
|
|
4428
4514
|
}
|
|
4429
4515
|
catch {
|
|
4430
4516
|
return { issues: 0, errors: 0 };
|
|
4431
4517
|
}
|
|
4518
|
+
const found = graded(ctx.rules, "frontmatter-valid", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4432
4519
|
if (found.length > 0 && !silent) {
|
|
4433
4520
|
console.log("\nFrontmatter-validity check:\n");
|
|
4434
4521
|
for (const issue of found) {
|
|
4435
|
-
console.log(` ${sev
|
|
4436
|
-
ctx.annotate(sev
|
|
4522
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.label(issue.message)}`);
|
|
4523
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4437
4524
|
}
|
|
4438
4525
|
}
|
|
4439
|
-
return
|
|
4526
|
+
return tally(found);
|
|
4440
4527
|
}
|
|
4441
4528
|
/**
|
|
4442
4529
|
* Apply the `description-overlap` rule: two model-invocable skills with
|
|
@@ -4511,38 +4598,40 @@ function checkDescriptionBudget(ctx) {
|
|
|
4511
4598
|
* unaffected by the collapse.
|
|
4512
4599
|
*/
|
|
4513
4600
|
function checkLethalTrifecta(ctx) {
|
|
4514
|
-
const {
|
|
4515
|
-
|
|
4516
|
-
if (!sev)
|
|
4601
|
+
const { silent } = ctx;
|
|
4602
|
+
if (!ctx.rules.mayFire("lethal-trifecta"))
|
|
4517
4603
|
return { issues: 0, errors: 0 };
|
|
4518
|
-
let
|
|
4604
|
+
let raw;
|
|
4519
4605
|
try {
|
|
4520
|
-
|
|
4606
|
+
raw = ctx.scan().trifectaFindings;
|
|
4521
4607
|
}
|
|
4522
4608
|
catch {
|
|
4523
4609
|
return { issues: 0, errors: 0 };
|
|
4524
4610
|
}
|
|
4611
|
+
const found = graded(ctx.rules, "lethal-trifecta", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4525
4612
|
if (found.length > 0 && !silent) {
|
|
4526
|
-
const mark = sev === "error" ? "✗" : "⚠";
|
|
4527
|
-
const level = sev === "error" ? "error" : "warning";
|
|
4528
4613
|
console.log("\nLethal-trifecta check:\n");
|
|
4529
4614
|
const unfenced = found.filter((t) => t.kind === "skill" && t.finding.fence === "none");
|
|
4530
4615
|
for (const t of found) {
|
|
4531
4616
|
if (unfenced.includes(t))
|
|
4532
4617
|
continue;
|
|
4533
4618
|
const msg = `${t.kind} ${t.name}: ${t.finding.message}`;
|
|
4534
|
-
console.log(` ${mark} ${ctx.bundle.scanned(t.path)}: ${msg}`);
|
|
4535
|
-
ctx.annotate(level, msg, ctx.bundle.scanned(t.path));
|
|
4619
|
+
console.log(` ${markOf(t.sev).mark} ${ctx.bundle.scanned(t.path)}: ${msg}`);
|
|
4620
|
+
ctx.annotate(markOf(t.sev).level, msg, ctx.bundle.scanned(t.path));
|
|
4536
4621
|
}
|
|
4537
4622
|
if (unfenced.length > 0) {
|
|
4538
|
-
|
|
4623
|
+
// One line for the group, at the worst severity any of its skills has.
|
|
4624
|
+
const groupSev = unfenced.some((t) => t.sev === "error")
|
|
4625
|
+
? "error"
|
|
4626
|
+
: "warn";
|
|
4627
|
+
console.log(` ${markOf(groupSev).mark} ${String(unfenced.length)} skill(s) declare no \`disallowed-tools:\` fence, so each ` +
|
|
4539
4628
|
`inherits every tool the session grants and holds all three legs. ` +
|
|
4540
4629
|
`\`allowed-tools:\` pre-approves, it does not restrict. ` +
|
|
4541
4630
|
`Add one \`disallowed-tools:\` line per skill to drop a leg: ` +
|
|
4542
4631
|
unfenced.map((t) => t.name).join(", "));
|
|
4543
4632
|
}
|
|
4544
4633
|
}
|
|
4545
|
-
return
|
|
4634
|
+
return tally(found);
|
|
4546
4635
|
}
|
|
4547
4636
|
/**
|
|
4548
4637
|
* Apply the `skill-resource-resolves` rule: a SKILL.md body referencing a bundled
|
|
@@ -4553,12 +4642,11 @@ function checkLethalTrifecta(ctx) {
|
|
|
4553
4642
|
*/
|
|
4554
4643
|
function checkSkillResourceResolves(ctx) {
|
|
4555
4644
|
const { config, silent } = ctx;
|
|
4556
|
-
|
|
4557
|
-
if (!sev)
|
|
4645
|
+
if (!ctx.rules.mayFire("skill-resource-resolves"))
|
|
4558
4646
|
return { issues: 0, errors: 0 };
|
|
4559
|
-
let
|
|
4647
|
+
let raw;
|
|
4560
4648
|
try {
|
|
4561
|
-
|
|
4649
|
+
raw = ctx.scan({
|
|
4562
4650
|
sharedDirs: config?.sharedDirs,
|
|
4563
4651
|
// sharedDirs live at the frame root — the repo that OWNS the scan target:
|
|
4564
4652
|
// cwd for a scoped subdir or a nested bundle, the target for a foreign lint.
|
|
@@ -4568,6 +4656,7 @@ function checkSkillResourceResolves(ctx) {
|
|
|
4568
4656
|
catch {
|
|
4569
4657
|
return { issues: 0, errors: 0 };
|
|
4570
4658
|
}
|
|
4659
|
+
const found = graded(ctx.rules, "skill-resource-resolves", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4571
4660
|
if (found.length > 0 && !silent) {
|
|
4572
4661
|
console.log("\nSkill-resource check:\n");
|
|
4573
4662
|
for (const s of found) {
|
|
@@ -4577,11 +4666,11 @@ function checkSkillResourceResolves(ctx) {
|
|
|
4577
4666
|
// it was documented only in docs/skills-monorepo.md, so a CI log gave no
|
|
4578
4667
|
// hint and the rule read as broken rather than misconfigured.
|
|
4579
4668
|
` If it resolves from the repo root instead, add its directory to \`sharedDirs\` in .vigilesrc.json.`;
|
|
4580
|
-
console.log(` ${sev
|
|
4581
|
-
ctx.annotate(sev
|
|
4669
|
+
console.log(` ${markOf(s.sev).mark} ${ctx.bundle.scanned(s.path)}: ${msg}`);
|
|
4670
|
+
ctx.annotate(markOf(s.sev).level, msg, ctx.bundle.scanned(s.path));
|
|
4582
4671
|
}
|
|
4583
4672
|
}
|
|
4584
|
-
return
|
|
4673
|
+
return tally(found);
|
|
4585
4674
|
}
|
|
4586
4675
|
/**
|
|
4587
4676
|
* Apply the `skill-missing-fence` rule: a SKILL.md that opens with
|
|
@@ -4591,26 +4680,26 @@ function checkSkillResourceResolves(ctx) {
|
|
|
4591
4680
|
* default; "error" gates CI.
|
|
4592
4681
|
*/
|
|
4593
4682
|
function checkSkillMissingFence(ctx) {
|
|
4594
|
-
const {
|
|
4595
|
-
|
|
4596
|
-
if (!sev)
|
|
4683
|
+
const { silent } = ctx;
|
|
4684
|
+
if (!ctx.rules.mayFire("skill-missing-fence"))
|
|
4597
4685
|
return { issues: 0, errors: 0 };
|
|
4598
|
-
let
|
|
4686
|
+
let raw;
|
|
4599
4687
|
try {
|
|
4600
|
-
|
|
4688
|
+
raw = ctx.scan().skillFenceIssues;
|
|
4601
4689
|
}
|
|
4602
4690
|
catch {
|
|
4603
4691
|
return { issues: 0, errors: 0 };
|
|
4604
4692
|
}
|
|
4693
|
+
const found = graded(ctx.rules, "skill-missing-fence", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4605
4694
|
if (found.length > 0 && !silent) {
|
|
4606
4695
|
console.log("\nSkill-missing-fence check:\n");
|
|
4607
4696
|
for (const s of found) {
|
|
4608
4697
|
const msg = `${s.name}: ${s.finding.message}`;
|
|
4609
|
-
console.log(` ${sev
|
|
4610
|
-
ctx.annotate(sev
|
|
4698
|
+
console.log(` ${markOf(s.sev).mark} ${ctx.bundle.scanned(s.path)}: ${msg}`);
|
|
4699
|
+
ctx.annotate(markOf(s.sev).level, msg, ctx.bundle.scanned(s.path));
|
|
4611
4700
|
}
|
|
4612
4701
|
}
|
|
4613
|
-
return
|
|
4702
|
+
return tally(found);
|
|
4614
4703
|
}
|
|
4615
4704
|
/**
|
|
4616
4705
|
* Apply the `plugin-dir-layout` rule: functional surface dirs (skills/agents/
|
|
@@ -4620,25 +4709,25 @@ function checkSkillMissingFence(ctx) {
|
|
|
4620
4709
|
* gates CI.
|
|
4621
4710
|
*/
|
|
4622
4711
|
function checkPluginDirLayout(ctx) {
|
|
4623
|
-
const {
|
|
4624
|
-
|
|
4625
|
-
if (!sev)
|
|
4712
|
+
const { silent } = ctx;
|
|
4713
|
+
if (!ctx.rules.mayFire("plugin-dir-layout"))
|
|
4626
4714
|
return { issues: 0, errors: 0 };
|
|
4627
|
-
let
|
|
4715
|
+
let raw;
|
|
4628
4716
|
try {
|
|
4629
|
-
|
|
4717
|
+
raw = ctx.scan().pluginLayoutIssues;
|
|
4630
4718
|
}
|
|
4631
4719
|
catch {
|
|
4632
4720
|
return { issues: 0, errors: 0 };
|
|
4633
4721
|
}
|
|
4722
|
+
const found = graded(ctx.rules, "plugin-dir-layout", raw, (f) => ctx.bundle.scanned(f.dir));
|
|
4634
4723
|
if (found.length > 0 && !silent) {
|
|
4635
4724
|
console.log("\nPlugin-dir-layout check:\n");
|
|
4636
4725
|
for (const p of found) {
|
|
4637
|
-
console.log(` ${sev
|
|
4638
|
-
ctx.annotate(sev
|
|
4726
|
+
console.log(` ${markOf(p.sev).mark} ${ctx.label(p.message)}`);
|
|
4727
|
+
ctx.annotate(markOf(p.sev).level, p.message);
|
|
4639
4728
|
}
|
|
4640
4729
|
}
|
|
4641
|
-
return
|
|
4730
|
+
return tally(found);
|
|
4642
4731
|
}
|
|
4643
4732
|
/**
|
|
4644
4733
|
* Apply the `delegation-trifecta` rule: a lethal trifecta that EMERGES across a
|
|
@@ -4649,26 +4738,26 @@ function checkPluginDirLayout(ctx) {
|
|
|
4649
4738
|
* capability the way a surface-specific rule is.
|
|
4650
4739
|
*/
|
|
4651
4740
|
function checkDelegationTrifecta(ctx) {
|
|
4652
|
-
const {
|
|
4653
|
-
|
|
4654
|
-
if (!sev)
|
|
4741
|
+
const { silent } = ctx;
|
|
4742
|
+
if (!ctx.rules.mayFire("delegation-trifecta"))
|
|
4655
4743
|
return { issues: 0, errors: 0 };
|
|
4656
|
-
let
|
|
4744
|
+
let raw;
|
|
4657
4745
|
try {
|
|
4658
|
-
|
|
4746
|
+
raw = ctx.scan().delegationTrifecta;
|
|
4659
4747
|
}
|
|
4660
4748
|
catch {
|
|
4661
4749
|
return { issues: 0, errors: 0 };
|
|
4662
4750
|
}
|
|
4751
|
+
const found = graded(ctx.rules, "delegation-trifecta", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4663
4752
|
if (found.length > 0 && !silent) {
|
|
4664
4753
|
console.log("\nDelegation-trifecta check:\n");
|
|
4665
4754
|
for (const d of found) {
|
|
4666
4755
|
const msg = `${d.finding.name}: ${d.finding.message}`;
|
|
4667
|
-
console.log(` ${sev
|
|
4668
|
-
ctx.annotate(sev
|
|
4756
|
+
console.log(` ${markOf(d.sev).mark} ${ctx.bundle.scanned(d.path)}: ${msg}`);
|
|
4757
|
+
ctx.annotate(markOf(d.sev).level, msg, ctx.bundle.scanned(d.path));
|
|
4669
4758
|
}
|
|
4670
4759
|
}
|
|
4671
|
-
return
|
|
4760
|
+
return tally(found);
|
|
4672
4761
|
}
|
|
4673
4762
|
/**
|
|
4674
4763
|
* Apply the `hook-block-ineffective` rule: a hook that LOOKS like it blocks but
|
|
@@ -4786,43 +4875,57 @@ function checkMcpHookTargets(ctx) {
|
|
|
4786
4875
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
4787
4876
|
}
|
|
4788
4877
|
/**
|
|
4789
|
-
* Apply the `hook-script-exists` rule: a
|
|
4790
|
-
*
|
|
4791
|
-
*
|
|
4792
|
-
*
|
|
4793
|
-
* existence-guarded one-liners, inline commands). Matches Anthropic's own
|
|
4794
|
-
* `claude plugin validate`. Warning by default; "error" gates CI.
|
|
4878
|
+
* Apply the `hook-script-exists` rule: a file a COMPILED hook line needs is
|
|
4879
|
+
* missing — the runtime, the program or its stamp — so the hook cannot run.
|
|
4880
|
+
* The files come from the hook-line plan (`hookLinePlan`), never from reading a
|
|
4881
|
+
* line: a line is compiled only by identity with what `compile` writes.
|
|
4795
4882
|
*/
|
|
4796
4883
|
function checkHookScriptExists(ctx) {
|
|
4884
|
+
return reportHookLines(ctx, "hook-script-exists", "Hook-script existence check", (scan) => [
|
|
4885
|
+
...scan.hooks
|
|
4886
|
+
.filter((h) => h.status === "missing")
|
|
4887
|
+
// `script` is absolute (the root variable resolved) — printed as-is it
|
|
4888
|
+
// was a machine path (#281); `scanned` makes it the bundle's own.
|
|
4889
|
+
.map((h) => (0, scan_js_1.hookScriptMissingMessage)(ctx.bundle.scanned(h.script))),
|
|
4890
|
+
// A line vigiles wrote that is now wrong: its own wiring, so here.
|
|
4891
|
+
...scan.staleHooks.map(scan_js_1.staleHookMessage),
|
|
4892
|
+
]);
|
|
4893
|
+
}
|
|
4894
|
+
/**
|
|
4895
|
+
* Apply the `hand-written-hook` rule: one finding per hook line vigiles did not
|
|
4896
|
+
* write (or wrote and someone edited). vigiles never parses such a line, so it
|
|
4897
|
+
* cannot say what it runs; the finding names it and points at compiled hooks.
|
|
4898
|
+
*/
|
|
4899
|
+
function checkHandWrittenHooks(ctx) {
|
|
4900
|
+
return reportHookLines(ctx, "hand-written-hook", "Hand-written hooks", (scan) => scan.handWrittenHooks.map(scan_js_1.handWrittenHookMessage));
|
|
4901
|
+
}
|
|
4902
|
+
/** Print and count one hook-line rule's messages at its configured severity. */
|
|
4903
|
+
function reportHookLines(ctx, rule, heading, messagesOf) {
|
|
4797
4904
|
const { config, silent, adapter } = ctx;
|
|
4798
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.[
|
|
4905
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.[rule]);
|
|
4799
4906
|
if (!sev)
|
|
4800
4907
|
return { issues: 0, errors: 0 };
|
|
4801
4908
|
if (!adapter.shellHooks) {
|
|
4802
|
-
reportNotApplicable(
|
|
4909
|
+
reportNotApplicable(heading, "shell hooks", adapter, silent);
|
|
4803
4910
|
return { issues: 0, errors: 0 };
|
|
4804
4911
|
}
|
|
4805
|
-
let
|
|
4912
|
+
let messages;
|
|
4806
4913
|
try {
|
|
4807
|
-
|
|
4914
|
+
messages = messagesOf(ctx.scan());
|
|
4808
4915
|
}
|
|
4809
4916
|
catch {
|
|
4810
4917
|
return { issues: 0, errors: 0 };
|
|
4811
4918
|
}
|
|
4812
|
-
if (
|
|
4813
|
-
console.log(
|
|
4814
|
-
for (const
|
|
4815
|
-
// `script` arrives with the plugin-root token EXPANDED, i.e. absolute —
|
|
4816
|
-
// printed as-is it was a machine path (#281). A relative one is the
|
|
4817
|
-
// bundle's own, which is what `scanned` assumes.
|
|
4818
|
-
const msg = `hook script "${ctx.bundle.scanned(h.script)}" is referenced but missing — the hook never runs.`;
|
|
4919
|
+
if (messages.length > 0 && !silent) {
|
|
4920
|
+
console.log(`\n${heading}:\n`);
|
|
4921
|
+
for (const msg of messages) {
|
|
4819
4922
|
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(msg)}`);
|
|
4820
4923
|
ctx.annotate(sev === "error" ? "error" : "warning", msg);
|
|
4821
4924
|
}
|
|
4822
4925
|
}
|
|
4823
4926
|
return {
|
|
4824
|
-
issues:
|
|
4825
|
-
errors: sev === "error" ?
|
|
4927
|
+
issues: messages.length,
|
|
4928
|
+
errors: sev === "error" ? messages.length : 0,
|
|
4826
4929
|
};
|
|
4827
4930
|
}
|
|
4828
4931
|
/**
|
|
@@ -4833,31 +4936,31 @@ function checkHookScriptExists(ctx) {
|
|
|
4833
4936
|
* plugin-namespaced form skipped). Warning by default; "error" gates CI.
|
|
4834
4937
|
*/
|
|
4835
4938
|
function checkMcpToolResolves(ctx) {
|
|
4836
|
-
const {
|
|
4837
|
-
|
|
4838
|
-
if (!sev)
|
|
4939
|
+
const { silent, adapter } = ctx;
|
|
4940
|
+
if (!ctx.rules.mayFire("mcp-tool-resolves"))
|
|
4839
4941
|
return { issues: 0, errors: 0 };
|
|
4840
4942
|
if (!adapter.subagents) {
|
|
4841
4943
|
reportNotApplicable("MCP tool-resolution check", "subagents", adapter, silent);
|
|
4842
4944
|
return { issues: 0, errors: 0 };
|
|
4843
4945
|
}
|
|
4844
|
-
let
|
|
4946
|
+
let raw;
|
|
4845
4947
|
try {
|
|
4846
|
-
|
|
4948
|
+
raw = ctx
|
|
4847
4949
|
.scan()
|
|
4848
4950
|
.agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
4849
4951
|
}
|
|
4850
4952
|
catch {
|
|
4851
4953
|
return { issues: 0, errors: 0 };
|
|
4852
4954
|
}
|
|
4955
|
+
const found = graded(ctx.rules, "mcp-tool-resolves", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4853
4956
|
if (found.length > 0 && !silent) {
|
|
4854
4957
|
console.log("\nMCP tool-resolution check:\n");
|
|
4855
4958
|
for (const issue of found) {
|
|
4856
|
-
console.log(` ${sev
|
|
4857
|
-
ctx.annotate(sev
|
|
4959
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.bundle.scanned(issue.path)}: ${issue.message}`);
|
|
4960
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4858
4961
|
}
|
|
4859
4962
|
}
|
|
4860
|
-
return
|
|
4963
|
+
return tally(found);
|
|
4861
4964
|
}
|
|
4862
4965
|
/**
|
|
4863
4966
|
* Apply the configured coverage thresholds. Returns the number of failing
|
|
@@ -5081,6 +5184,17 @@ async function handleGenerate(restArgs, args) {
|
|
|
5081
5184
|
process.exit(2);
|
|
5082
5185
|
}
|
|
5083
5186
|
}
|
|
5187
|
+
/**
|
|
5188
|
+
* A linter whose config could not be read is missing from the generated types;
|
|
5189
|
+
* say so on stderr (and as a CI annotation) instead of a silent short list.
|
|
5190
|
+
*/
|
|
5191
|
+
function printUnreadableLinters(unreadable) {
|
|
5192
|
+
unreadable.forEach(({ linter, reason }) => {
|
|
5193
|
+
const msg = `could not read the ${linter} config: ${reason} (its rules are not listed)`;
|
|
5194
|
+
console.error(` ⚠ ${msg}`);
|
|
5195
|
+
console.error(`::warning::${msg}`);
|
|
5196
|
+
});
|
|
5197
|
+
}
|
|
5084
5198
|
function handleGenerateTypes(args, restArgs) {
|
|
5085
5199
|
const checkOnly = args.includes("--check");
|
|
5086
5200
|
const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
|
|
@@ -5096,6 +5210,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
5096
5210
|
for (const l of result.linters) {
|
|
5097
5211
|
console.log(` ${l.linter}: ${String(l.rules.length)} enabled rules (via ${l.via})`);
|
|
5098
5212
|
}
|
|
5213
|
+
printUnreadableLinters(result.unreadable);
|
|
5099
5214
|
if (result.scripts.length > 0) {
|
|
5100
5215
|
console.log(` npm scripts: ${String(result.scripts.length)}`);
|
|
5101
5216
|
}
|
|
@@ -6381,7 +6496,8 @@ function evalLockNudgeHookCommand() {
|
|
|
6381
6496
|
// into a failed edit: the nudge not firing is the cheaper failure. The verbs
|
|
6382
6497
|
// throw on the same config; what is CHECKED is identical.
|
|
6383
6498
|
const config = (0, validate_js_1.loadConfig)(cwd, { onInvalid: "warn" });
|
|
6384
|
-
const
|
|
6499
|
+
const nudgeRules = (0, overrides_js_1.ruleResolver)(config.rules, config.overrides);
|
|
6500
|
+
const { options } = untestedRules(config, nudgeRules);
|
|
6385
6501
|
// 🔴 THE SAME LAYOUT `vigiles lint` RESOLVES, for the same reason as the config
|
|
6386
6502
|
// above. This used to pass `basePath` alone, so the detector fell back to the
|
|
6387
6503
|
// Claude Code layout: in a Codex repo whose surfaces live at
|
|
@@ -6396,9 +6512,8 @@ function evalLockNudgeHookCommand() {
|
|
|
6396
6512
|
// heard nothing at all, while a repo that already tests got reminded. That is
|
|
6397
6513
|
// backwards, and `untested-skill` already stated the missing half correctly;
|
|
6398
6514
|
// it just lived in `vigiles lint`, which someone has to run by hand.
|
|
6399
|
-
//
|
|
6400
|
-
//
|
|
6401
|
-
// vendored skill the linter would never list.
|
|
6515
|
+
// The repo-wide exclude `vigiles lint` applies (#192) — the nudge must not
|
|
6516
|
+
// report a vendored skill the linter would never list.
|
|
6402
6517
|
const excludes = (0, exclude_js_1.excludeSet)(cwd, config.exclude);
|
|
6403
6518
|
// 🔴 THE BUNDLE THAT HOLDS THE EDIT, found the way `lint` finds it (#281, D6).
|
|
6404
6519
|
// The nudge scanned the root bundle only and matched by SUFFIX, so an edit to
|
|
@@ -6415,13 +6530,18 @@ function evalLockNudgeHookCommand() {
|
|
|
6415
6530
|
return r !== "" && !r.startsWith("..") && !(0, node_path_1.isAbsolute)(r);
|
|
6416
6531
|
})
|
|
6417
6532
|
: undefined) ?? cwd;
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6533
|
+
// The same per-path answer `lint` gives: no untested nudge for a skill whose
|
|
6534
|
+
// folder an `overrides` entry turned `untested-skill` off for.
|
|
6535
|
+
const untestedHere = nudgeRules.severityAt("untested-skill", frame.repo(edited)) !== false;
|
|
6536
|
+
const msg = (untestedHere
|
|
6537
|
+
? (0, test_coverage_js_1.skillTestNudge)(frame.repo(edited), {
|
|
6538
|
+
...options,
|
|
6539
|
+
basePath: home,
|
|
6540
|
+
root: cwd,
|
|
6541
|
+
layout,
|
|
6542
|
+
excludes,
|
|
6543
|
+
})
|
|
6544
|
+
: undefined) ?? (0, eval_lock_js_1.evalLockNudge)(target, (0, node_path_1.resolve)(cwd, eval_lock_js_1.DEFAULT_LOCK_DIR));
|
|
6425
6545
|
if (!msg)
|
|
6426
6546
|
return;
|
|
6427
6547
|
process.stdout.write(JSON.stringify({
|
|
@@ -6461,7 +6581,10 @@ async function refsHookCommand() {
|
|
|
6461
6581
|
// from the process's directory is how a disabled rule comes back to life.
|
|
6462
6582
|
const cwd = runtimeRoot(eventRoot(raw));
|
|
6463
6583
|
// A hook rail — see `evalLockNudgeHookCommand` for why it warns, not throws.
|
|
6464
|
-
|
|
6584
|
+
// Resolved at the edited file's own path, so an `overrides` entry that turns
|
|
6585
|
+
// `unmarked-refs` off for a folder is honoured at edit time too.
|
|
6586
|
+
const refsConfig = (0, validate_js_1.loadConfig)(cwd, { onInvalid: "warn" });
|
|
6587
|
+
const severity = (0, overrides_js_1.ruleResolver)(refsConfig.rules, refsConfig.overrides).severityAt("unmarked-refs", (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)));
|
|
6465
6588
|
if (severity === false)
|
|
6466
6589
|
return;
|
|
6467
6590
|
const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
|
|
@@ -6541,7 +6664,7 @@ async function compileProviders() {
|
|
|
6541
6664
|
* the string the user typed — `x.hook.ts` and `./x.hook.ts` are the same wiring),
|
|
6542
6665
|
* so recompiling updates in place and never clobbers the user's own hooks.
|
|
6543
6666
|
*/
|
|
6544
|
-
async function installHookFile(file, adapter, registeredProviders
|
|
6667
|
+
async function installHookFile(file, adapter, registeredProviders, runtime) {
|
|
6545
6668
|
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
|
|
6546
6669
|
const program = await (0, hook_runtime_js_1.loadHookProgram)(file);
|
|
6547
6670
|
// Emit the CANONICAL path so two spellings of the same file produce the same
|
|
@@ -6560,16 +6683,17 @@ async function installHookFile(file, adapter, registeredProviders = []) {
|
|
|
6560
6683
|
// cache, thirteen times, on every tool call. The `|| exit N` ending is what
|
|
6561
6684
|
// the shell does when that file cannot start at all. `compileHookProgram`
|
|
6562
6685
|
// adds it (see `hookRuntimeMissingExit`), so it is not written here.
|
|
6563
|
-
gateCommand:
|
|
6564
|
-
(0, hook_install_js_1.hookGateRef)(ref, adapter.layout.projectRootTokens),
|
|
6686
|
+
gateCommand: (0, hook_command_js_1.runProgramCommand)((0, hook_command_js_1.hookRuntimeRef)(adapter.layout.projectRootTokens, runtime), (0, hook_command_js_1.hookGateRef)(ref, adapter.layout.projectRootTokens)),
|
|
6565
6687
|
dialect: adapter.dialect,
|
|
6566
6688
|
hookProtocol: adapter.hookProtocol,
|
|
6567
6689
|
settings: adapter.layout.settings,
|
|
6568
6690
|
registeredProviders,
|
|
6569
6691
|
});
|
|
6570
|
-
// Tamper-evident stamp beside the source (one dir → basename is unique).
|
|
6692
|
+
// Tamper-evident stamp beside the source (one dir → basename is unique). The
|
|
6693
|
+
// role is recorded with it: `lint` rebuilds the exact line compile writes
|
|
6694
|
+
// from it (`hookLinePlan`), so a wired line is read by identity, not parsed.
|
|
6571
6695
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)((0, hook_runtime_js_1.hookStampPath)(file)), { recursive: true });
|
|
6572
|
-
(0, node_fs_1.writeFileSync)((0, hook_runtime_js_1.hookStampPath)(file), JSON.stringify({ file, stamp: compiled.stamp }, null, 2) + "\n");
|
|
6696
|
+
(0, node_fs_1.writeFileSync)((0, hook_runtime_js_1.hookStampPath)(file), JSON.stringify({ file, stamp: compiled.stamp, role: (0, hook_program_js_1.dispatchKind)(program) }, null, 2) + "\n");
|
|
6573
6697
|
// Merge into the harness's native config, idempotently.
|
|
6574
6698
|
//
|
|
6575
6699
|
// 🔴 THREE FORMAT BRANCHES BECAME ZERO, and they were three because the ONE
|
|
@@ -6645,7 +6769,14 @@ async function installHookFile(file, adapter, registeredProviders = []) {
|
|
|
6645
6769
|
`yet automatic).`;
|
|
6646
6770
|
}
|
|
6647
6771
|
}
|
|
6648
|
-
|
|
6772
|
+
// 🔴 `compiled.warnings` WAS COMPUTED AND NEVER PRINTED until 2026-10-11
|
|
6773
|
+
// (#356): a degraded role/event fit reached the author's terminal by no
|
|
6774
|
+
// path at all — a nudge with no delivery, inside the check for exactly that.
|
|
6775
|
+
return {
|
|
6776
|
+
role,
|
|
6777
|
+
settingsPath: adapter.layout.settingsPath,
|
|
6778
|
+
warnings: [...(compiled.warnings ?? []), ...(warning ? [warning] : [])],
|
|
6779
|
+
};
|
|
6649
6780
|
}
|
|
6650
6781
|
/**
|
|
6651
6782
|
* Compile + install every hook (explicit paths, else discovered under
|
|
@@ -6679,15 +6810,29 @@ async function installHooks(hookFiles, harnessFlag, configHarness, validateProvi
|
|
|
6679
6810
|
}
|
|
6680
6811
|
if (hookFiles.length === 0)
|
|
6681
6812
|
return true; // providers validated; nothing to wire
|
|
6813
|
+
// WHICH runtime the wiring starts — the installed package for a consumer, the
|
|
6814
|
+
// package's own built bin when this IS vigiles (see `hookRuntimeFor`).
|
|
6815
|
+
const runtime = (0, hook_install_js_1.readHookRuntime)(process.cwd());
|
|
6816
|
+
// 🔴 SAY IT NOW, because nothing will later. A nudge whose runtime cannot
|
|
6817
|
+
// start ends in `|| exit 0` by design, so from then on it is indistinguishable
|
|
6818
|
+
// from a nudge with nothing to say. Measured 2026-10-11 on this repository:
|
|
6819
|
+
// dead for three weeks, every gate green. Not an error — the file may arrive
|
|
6820
|
+
// with the next install or build — but never silent.
|
|
6821
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), runtime.path))) {
|
|
6822
|
+
console.warn(`⚠ the hook runtime ${runtime.path} is not on disk — every hook compiled ` +
|
|
6823
|
+
`here fails to load until it is: a reminder is skipped without a word, a ` +
|
|
6824
|
+
`gate refuses every command. Run ${runtime.cure}.`);
|
|
6825
|
+
}
|
|
6682
6826
|
let ok = true;
|
|
6683
6827
|
for (const file of hookFiles) {
|
|
6684
6828
|
try {
|
|
6685
6829
|
// Fan out: the same compiled hook lands in each enabled harness's config.
|
|
6686
6830
|
for (const adapter of adapters) {
|
|
6687
|
-
const r = await installHookFile(file, adapter, registeredProviders);
|
|
6831
|
+
const r = await installHookFile(file, adapter, registeredProviders, runtime);
|
|
6688
6832
|
console.log(`✓ ${file} → ${r.settingsPath} (role: ${r.role}, harness: ${adapter.name})`);
|
|
6689
|
-
|
|
6690
|
-
console.warn(`⚠ ${
|
|
6833
|
+
r.warnings.forEach((w) => {
|
|
6834
|
+
console.warn(`⚠ ${w}`);
|
|
6835
|
+
});
|
|
6691
6836
|
}
|
|
6692
6837
|
}
|
|
6693
6838
|
catch (e) {
|
|
@@ -7090,6 +7235,30 @@ async function askOnce(q) {
|
|
|
7090
7235
|
rl.close();
|
|
7091
7236
|
}
|
|
7092
7237
|
}
|
|
7238
|
+
/**
|
|
7239
|
+
* 🔴 THE PROJECT ROOT, resolved ONCE (#371) — the only place the CLI asks where
|
|
7240
|
+
* it was started. Every command then runs from the root, so `vigiles lint` in
|
|
7241
|
+
* `src/` reads the root's `.vigilesrc.json`, its overrides and its compiled
|
|
7242
|
+
* files exactly as it does at the root, and passes the root on explicitly. A
|
|
7243
|
+
* path the user typed means "from where I typed it", so path arguments are
|
|
7244
|
+
* re-expressed from the root first (`PATH_ARGUMENTS` says which arguments are
|
|
7245
|
+
* paths). `hook-runtime` is exempt: its cwd and argv come from the harness.
|
|
7246
|
+
*/
|
|
7247
|
+
function enterProjectRoot() {
|
|
7248
|
+
const typed = process.argv.slice(2);
|
|
7249
|
+
const [command = "", ...rest] = typed;
|
|
7250
|
+
const invocationDir = process.cwd();
|
|
7251
|
+
if (!(0, cli_flag_check_js_1.runsFromProjectRoot)(command))
|
|
7252
|
+
return { command, projectRoot: invocationDir, args: typed };
|
|
7253
|
+
const projectRoot = (0, frame_js_1.findProjectRoot)(invocationDir, node_fs_1.existsSync);
|
|
7254
|
+
const args = [
|
|
7255
|
+
command,
|
|
7256
|
+
...(0, cli_flag_check_js_1.rebasePathArgs)(command, rest, (p) => (0, frame_js_1.rebaseArg)(p, invocationDir, projectRoot)),
|
|
7257
|
+
];
|
|
7258
|
+
if (projectRoot !== invocationDir)
|
|
7259
|
+
process.chdir(projectRoot);
|
|
7260
|
+
return { command, projectRoot, args };
|
|
7261
|
+
}
|
|
7093
7262
|
/**
|
|
7094
7263
|
* Persist the audit consent (`audit.measure`) into `.vigilesrc.json` without
|
|
7095
7264
|
* clobbering existing keys — the "ask once, remember" sticky choice. Best-effort:
|
|
@@ -7119,8 +7288,7 @@ function rememberAuditMeasure(value) {
|
|
|
7119
7288
|
}
|
|
7120
7289
|
}
|
|
7121
7290
|
async function main() {
|
|
7122
|
-
const args =
|
|
7123
|
-
const command = args[0];
|
|
7291
|
+
const { command, projectRoot, args } = enterProjectRoot();
|
|
7124
7292
|
// `--version` / `-v` / `version` prints the version number, not the help
|
|
7125
7293
|
// banner — so `npx vigiles --version` reports e.g. `3.0.0`.
|
|
7126
7294
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -7155,11 +7323,11 @@ async function main() {
|
|
|
7155
7323
|
const restArgs = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
7156
7324
|
// Shared flags (--max-rules, --catalog-only) override the loaded config so
|
|
7157
7325
|
// every GitHub Action input maps to a real CLI flag. See src/cli-flags.ts.
|
|
7158
|
-
const config = (0, cli_flags_js_1.applyConfigFlags)((0, validate_js_1.loadConfig)(), args);
|
|
7326
|
+
const config = (0, cli_flags_js_1.applyConfigFlags)((0, validate_js_1.loadConfig)(projectRoot), args);
|
|
7159
7327
|
// `.vigilesrc.json#exclude`, parsed ONCE here and threaded to every walk that
|
|
7160
7328
|
// polices the repo (src/exclude.ts). Built where the config is loaded so a
|
|
7161
7329
|
// command cannot re-derive it differently — the drift #192 measured.
|
|
7162
|
-
const excludes = (0, exclude_js_1.excludeSet)(
|
|
7330
|
+
const excludes = (0, exclude_js_1.excludeSet)(projectRoot, config.exclude);
|
|
7163
7331
|
switch (command) {
|
|
7164
7332
|
// --- Primary commands ---
|
|
7165
7333
|
case "init": {
|
|
@@ -7192,7 +7360,7 @@ async function main() {
|
|
|
7192
7360
|
// is refused below, out loud, with the command that fixes it.
|
|
7193
7361
|
const found = restArgs.length > 0
|
|
7194
7362
|
? (0, hook_install_js_1.partitionHookArgs)(restArgs.filter((f) => !f.endsWith(".spec.ts")))
|
|
7195
|
-
: (0, hook_install_js_1.discoveredHookArgs)(
|
|
7363
|
+
: (0, hook_install_js_1.discoveredHookArgs)(projectRoot);
|
|
7196
7364
|
const hooks = [...found.hooks];
|
|
7197
7365
|
const unclaimed = found.unclaimed;
|
|
7198
7366
|
if (specs.length === 0 &&
|
|
@@ -7227,8 +7395,8 @@ async function main() {
|
|
|
7227
7395
|
// committed baseline — shown at the moment of change. Report-only:
|
|
7228
7396
|
// `lint` is the gate (rule `instruction-weight`).
|
|
7229
7397
|
if (specs.length > 0) {
|
|
7230
|
-
const weightLine = (0, instruction_weight_ratchet_js_1.compileWeightLine)(
|
|
7231
|
-
root:
|
|
7398
|
+
const weightLine = (0, instruction_weight_ratchet_js_1.compileWeightLine)(projectRoot, (0, scan_js_1.measureInstructionWeight)(projectRoot, (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
7399
|
+
root: projectRoot,
|
|
7232
7400
|
flag: harnessFlag,
|
|
7233
7401
|
configHarness: (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses),
|
|
7234
7402
|
}).adapter));
|
|
@@ -7431,7 +7599,7 @@ async function main() {
|
|
|
7431
7599
|
}
|
|
7432
7600
|
// The same single owner of "relative to which directory" `lint` uses
|
|
7433
7601
|
// (#281): cwd, unless the audited dir is somebody else's repository.
|
|
7434
|
-
const frame = (0, frame_js_1.frameFor)(
|
|
7602
|
+
const frame = (0, frame_js_1.frameFor)(projectRoot, root);
|
|
7435
7603
|
const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
|
|
7436
7604
|
deprecatedKeyFiles: specsUsingKeyFiles(root, excludes.globIgnore),
|
|
7437
7605
|
sharedDirs: config.sharedDirs,
|
|
@@ -7477,9 +7645,11 @@ async function main() {
|
|
|
7477
7645
|
// --target=skills/x/SKILL.md` named a file that does not exist from where
|
|
7478
7646
|
// you run it, and offered to adopt a skill `exclude` had removed.
|
|
7479
7647
|
const auditedBundle = frame.bundle(root);
|
|
7480
|
-
const adoptableSurfaces = discoverAdoptableForAudit(root,
|
|
7481
|
-
|
|
7482
|
-
|
|
7648
|
+
const adoptableSurfaces = discoverAdoptableForAudit(root, {
|
|
7649
|
+
instructionFile: adapter.layout.instructionFile,
|
|
7650
|
+
excludes,
|
|
7651
|
+
config,
|
|
7652
|
+
}).map((p) => auditedBundle.scanned(p));
|
|
7483
7653
|
// Read the local flight recorder ONCE — feeds both the JSON report
|
|
7484
7654
|
// (structured summary, the product boundary) and the terminal render.
|
|
7485
7655
|
const ledgerRecords = (0, observe_js_1.readObservations)(root);
|
|
@@ -7487,7 +7657,7 @@ async function main() {
|
|
|
7487
7657
|
// report is built (it's a pure read off the scan) because the report's
|
|
7488
7658
|
// `Evaluated` ring needs to know whether this run will ever ask the
|
|
7489
7659
|
// skill-firing question — see `firingMeasured` below.
|
|
7490
|
-
const isForeign = root !==
|
|
7660
|
+
const isForeign = root !== projectRoot;
|
|
7491
7661
|
// ONE read of "can we reach a model, and on whose bill", from the driving
|
|
7492
7662
|
// adapter. It replaces a name check OR-ed with one harness's env vars at
|
|
7493
7663
|
// four sites below, and the two hard-coded "authenticate the `claude` CLI"
|
|
@@ -7757,8 +7927,8 @@ async function main() {
|
|
|
7757
7927
|
// CI upload / a custom location). The dir is created if missing.
|
|
7758
7928
|
const outFlag = args.find((a) => a.startsWith("--out="));
|
|
7759
7929
|
const outDir = outFlag
|
|
7760
|
-
? (0, node_path_1.resolve)(
|
|
7761
|
-
:
|
|
7930
|
+
? (0, node_path_1.resolve)(projectRoot, outFlag.slice("--out=".length))
|
|
7931
|
+
: projectRoot;
|
|
7762
7932
|
const openReport = !args.includes("--no-open");
|
|
7763
7933
|
const wroteReports = [];
|
|
7764
7934
|
// The versioned JSON artifact — the upload/CI boundary (a hosted dashboard
|
|
@@ -7804,14 +7974,14 @@ async function main() {
|
|
|
7804
7974
|
// `--out` pointing elsewhere yields nothing to write rather than a
|
|
7805
7975
|
// dead `../../..` entry.
|
|
7806
7976
|
const rel = wroteReports
|
|
7807
|
-
.map((f) => (0, repo_path_js_1.repoRelative)(
|
|
7977
|
+
.map((f) => (0, repo_path_js_1.repoRelative)(projectRoot, (0, node_path_1.resolve)(outDir, f), {
|
|
7808
7978
|
relative: node_path_1.relative,
|
|
7809
7979
|
resolve: node_path_1.resolve,
|
|
7810
7980
|
isAbsolute: node_path_1.isAbsolute,
|
|
7811
7981
|
sep: node_path_1.sep,
|
|
7812
7982
|
}))
|
|
7813
7983
|
.filter((p) => p !== null);
|
|
7814
|
-
ensureReportGitignored(
|
|
7984
|
+
ensureReportGitignored(projectRoot, rel);
|
|
7815
7985
|
}
|
|
7816
7986
|
// A shareable deep-link for a public GitHub repo: the in-browser demo
|
|
7817
7987
|
// re-runs the audit LIVE for whoever opens it, so a local result is
|