vigiles 38.2.0 → 39.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code/event-capability.js +24 -8
- package/dist/adapters/claude-code/hook-protocol.js +2 -1
- package/dist/adapters/codex/hook-protocol.js +2 -1
- package/dist/cli-flag-check.d.ts +36 -0
- package/dist/cli-flag-check.js +72 -1
- package/dist/cli-main.d.ts +2 -2
- package/dist/cli-main.js +548 -379
- package/dist/core/bash-effects.d.ts +0 -57
- package/dist/core/bash-effects.js +0 -147
- package/dist/core/compile-generator.d.ts +6 -6
- package/dist/core/compile-generator.js +4 -4
- package/dist/core/compile.d.ts +72 -8
- package/dist/core/compile.js +276 -133
- package/dist/core/config-schema.d.ts +40 -10
- package/dist/core/config-schema.js +139 -16
- package/dist/core/coverage.d.ts +16 -0
- package/dist/core/coverage.js +22 -0
- package/dist/core/doc-refs.d.ts +3 -2
- package/dist/core/doc-refs.js +2 -2
- package/dist/core/enforce-marker.d.ts +28 -0
- package/dist/core/enforce-marker.js +34 -0
- package/dist/core/eslint-inventory.d.ts +96 -0
- package/dist/core/eslint-inventory.js +251 -0
- package/dist/core/event-capability.d.ts +5 -1
- package/dist/core/frame.d.ts +43 -2
- package/dist/core/frame.js +85 -2
- package/dist/core/generate-schema.d.ts +3 -2
- package/dist/core/generate-schema.js +2 -2
- package/dist/core/generate-types.d.ts +11 -2
- package/dist/core/generate-types.js +12 -5
- package/dist/core/hook-command.d.ts +172 -0
- package/dist/core/hook-command.js +277 -0
- package/dist/core/hook-delivery.d.ts +133 -0
- package/dist/core/hook-delivery.js +150 -0
- package/dist/core/hook-paths.d.ts +14 -0
- package/dist/core/hook-paths.js +22 -0
- package/dist/core/hook-program.d.ts +4 -4
- package/dist/core/hook-program.js +13 -25
- package/dist/core/hook-state.d.ts +1 -1
- package/dist/core/hook-state.js +1 -1
- package/dist/core/instruction-remedies.d.ts +35 -0
- package/dist/core/instruction-remedies.js +70 -1
- package/dist/core/integrity-header.d.ts +54 -0
- package/dist/core/integrity-header.js +90 -0
- package/dist/core/integrity.d.ts +1 -25
- package/dist/core/integrity.js +16 -74
- package/dist/core/linter-adapter.d.ts +27 -3
- package/dist/core/linters.d.ts +8 -1
- package/dist/core/linters.js +116 -81
- package/dist/core/orphans.d.ts +3 -5
- package/dist/core/orphans.js +9 -10
- package/dist/core/presets.d.ts +19 -7
- package/dist/core/presets.js +19 -6
- package/dist/core/rule-catalog.d.ts +11 -12
- package/dist/core/rule-catalog.js +25 -94
- package/dist/core/rule-meta.d.ts +12 -0
- package/dist/core/rule-meta.js +47 -10
- package/dist/core/source-refs.d.ts +3 -23
- package/dist/core/source-refs.js +3 -27
- package/dist/core/test-utils.d.ts +13 -0
- package/dist/core/test-utils.js +40 -1
- package/dist/core/types.d.ts +18 -26
- package/dist/core/validate.d.ts +1 -2
- package/dist/core/validate.js +60 -47
- package/dist/coverage-evidence.js +2 -3
- package/dist/deprecated-key-files.d.ts +6 -0
- package/dist/deprecated-key-files.js +7 -6
- package/dist/exclude.d.ts +3 -1
- package/dist/exclude.js +9 -33
- package/dist/harness-assert.d.ts +18 -0
- package/dist/harness-assert.js +38 -0
- package/dist/hook-check-install.js +15 -6
- package/dist/hook-check.d.ts +1 -3
- package/dist/hook-check.js +3 -5
- package/dist/hook-install.d.ts +7 -41
- package/dist/hook-install.js +29 -96
- package/dist/hook-runtime.js +2 -1
- package/dist/load-hook.d.ts +3 -2
- package/dist/load-hook.js +28 -6
- package/dist/overrides.d.ts +25 -0
- package/dist/overrides.js +51 -0
- package/dist/plugin-loader.d.ts +23 -0
- package/dist/plugin-loader.js +42 -12
- package/dist/root-globs.d.ts +9 -0
- package/dist/root-globs.js +48 -0
- package/dist/rule-routing.js +6 -3
- package/dist/run-hook.d.ts +26 -2
- package/dist/run-hook.js +48 -12
- package/dist/scan-core.d.ts +96 -20
- package/dist/scan-core.js +171 -176
- package/dist/scan-files.d.ts +0 -8
- package/dist/scan-files.js +78 -20
- package/dist/scan.d.ts +12 -12
- package/dist/scan.js +33 -10
- package/dist/setup-plan.d.ts +15 -9
- package/dist/setup-plan.js +26 -18
- package/dist/test-coverage.d.ts +3 -8
- package/dist/test-coverage.js +6 -6
- package/dist/test.d.ts +1 -1
- package/dist/vigilesrc.schema.json +1329 -118
- package/package.json +1 -1
- package/skills/adopt-spec/SKILL.md +2 -1
package/dist/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.
|
|
@@ -1792,11 +1850,12 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1792
1850
|
// over the repository, like the ratchet: a nested bundle's spec would
|
|
1793
1851
|
// otherwise be reported again by every bundle that encloses it.
|
|
1794
1852
|
const deprecatedKeyFiles = (0, deprecated_key_files_js_1.checkDeprecatedKeyFiles)({
|
|
1795
|
-
severity:
|
|
1796
|
-
|
|
1853
|
+
severity: rules.mayFire("deprecated-key-files") ? "warn" : false,
|
|
1854
|
+
severityAt: (path) => rules.severityAt("deprecated-key-files", path),
|
|
1855
|
+
find: () => specsUsingKeyFiles(root, excludes.globIgnore),
|
|
1797
1856
|
silent,
|
|
1798
1857
|
annotate: (level, message, path) => {
|
|
1799
|
-
ghAnnotate(level, message, frame.repo((0, node_path_1.resolve)(
|
|
1858
|
+
ghAnnotate(level, message, frame.repo((0, node_path_1.resolve)(root, path)));
|
|
1800
1859
|
},
|
|
1801
1860
|
});
|
|
1802
1861
|
// 8. Validate vigiles builder calls inside markdown code blocks — the
|
|
@@ -1819,7 +1878,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1819
1878
|
// globs without `dot`, so `.claude/**` — where real skills and agents live —
|
|
1820
1879
|
// has never been scanned. The rule has never once looked at a deployed
|
|
1821
1880
|
// instruction file; every ref it has ever judged was in prose.
|
|
1822
|
-
const docRefSeverity =
|
|
1881
|
+
const docRefSeverity = rules.mayFire("doc-refs");
|
|
1823
1882
|
if (!silent && docRefSeverity)
|
|
1824
1883
|
console.log("\nMarkdown code block refs:\n");
|
|
1825
1884
|
// 🔴 `config.exclude` REACHES THIS PASS. It did not, and that single omission is
|
|
@@ -1835,8 +1894,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1835
1894
|
// With no way to reach 0 the step was made `continue-on-error: true`, and a lint
|
|
1836
1895
|
// whose exit code is discarded gates nothing — after which hand-written CI steps
|
|
1837
1896
|
// grew to do the gating instead. One unpassed argument, that whole chain.
|
|
1838
|
-
const
|
|
1839
|
-
? (0, doc_refs_js_1.findDocRefs)({ basePath:
|
|
1897
|
+
const docRefFound = docRefSeverity
|
|
1898
|
+
? (0, doc_refs_js_1.findDocRefs)({ basePath: root, ignore: excludes.globIgnore })
|
|
1840
1899
|
: {
|
|
1841
1900
|
filesScanned: 0,
|
|
1842
1901
|
filesIgnored: 0,
|
|
@@ -1846,10 +1905,24 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1846
1905
|
unverified: 0,
|
|
1847
1906
|
placeholders: 0,
|
|
1848
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 };
|
|
1849
1913
|
if (!silent && docRefSeverity) {
|
|
1850
|
-
const
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
+
}
|
|
1853
1926
|
}
|
|
1854
1927
|
}
|
|
1855
1928
|
// Per-line GitHub annotations for each broken doc ref — each carries file+line,
|
|
@@ -1859,7 +1932,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1859
1932
|
// on the PR. CI-only (isGitHubActions); skipped under --json/--summary.
|
|
1860
1933
|
if (isGitHubActions() && !silent && docRefSeverity) {
|
|
1861
1934
|
for (const e of docRefReport.errors) {
|
|
1862
|
-
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);
|
|
1863
1936
|
}
|
|
1864
1937
|
}
|
|
1865
1938
|
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
@@ -1878,14 +1951,16 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1878
1951
|
specRefErrors: specRefs.errors,
|
|
1879
1952
|
compileBudgetErrors: specRefs.budgetErrors,
|
|
1880
1953
|
duplicatePairs: dups.pairCount,
|
|
1881
|
-
duplicateSeverity:
|
|
1954
|
+
duplicateSeverity: dups.errorPairs > 0 ? "error" : "warn",
|
|
1882
1955
|
coverageEnabled: coverage.enabled,
|
|
1883
1956
|
coverageDocumented: coverage.documented,
|
|
1884
1957
|
strengthenSuggestions: guidanceCount,
|
|
1885
1958
|
integrityErrors,
|
|
1886
1959
|
coverageErrors,
|
|
1887
1960
|
orphanCount: orphanReport.orphans.length,
|
|
1888
|
-
orphanSeverity:
|
|
1961
|
+
orphanSeverity: orphanReport.orphans.some((doc) => rules.severityAt("orphan-docs", doc) === "error")
|
|
1962
|
+
? "error"
|
|
1963
|
+
: "warn",
|
|
1889
1964
|
untestedSurfaces: untested.untested,
|
|
1890
1965
|
untestedErrors: untested.errors,
|
|
1891
1966
|
toolContractIssues: toolContract.issues,
|
|
@@ -1902,6 +1977,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1902
1977
|
mcpToolErrors: mcpToolResolves.errors,
|
|
1903
1978
|
hookScriptIssues: hookScripts.issues,
|
|
1904
1979
|
hookScriptErrors: hookScripts.errors,
|
|
1980
|
+
handWrittenHookIssues: handWrittenHooks.issues,
|
|
1981
|
+
handWrittenHookErrors: handWrittenHooks.errors,
|
|
1905
1982
|
disallowedToolIssues: disallowedTools.issues,
|
|
1906
1983
|
disallowedToolErrors: disallowedTools.errors,
|
|
1907
1984
|
descriptionOverlapIssues: descriptionOverlap.issues,
|
|
@@ -1912,8 +1989,6 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1912
1989
|
frontmatterValidErrors: frontmatterValid.errors,
|
|
1913
1990
|
mcpHookIssues: mcpHookTargets.issues,
|
|
1914
1991
|
mcpHookErrors: mcpHookTargets.errors,
|
|
1915
|
-
preferCompiledHookIssues: preferCompiledHooks.issues,
|
|
1916
|
-
preferCompiledHookErrors: preferCompiledHooks.errors,
|
|
1917
1992
|
lethalTrifectaIssues: lethalTrifecta.issues,
|
|
1918
1993
|
lethalTrifectaErrors: lethalTrifecta.errors,
|
|
1919
1994
|
skillResourceIssues: skillResources.issues,
|
|
@@ -1935,7 +2010,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1935
2010
|
// Only the "error" tier gates. At "warn" the findings are printed and
|
|
1936
2011
|
// annotated, and the exit code is untouched — same contract as every other
|
|
1937
2012
|
// opt-in rule here.
|
|
1938
|
-
docRefErrors:
|
|
2013
|
+
docRefErrors: docRefErrors.filter((e) => e.sev === "error").length,
|
|
1939
2014
|
symbolRefErrors,
|
|
1940
2015
|
mcpRefErrors,
|
|
1941
2016
|
files,
|
|
@@ -2021,10 +2096,9 @@ function collectDocumentedRules(excludes) {
|
|
|
2021
2096
|
});
|
|
2022
2097
|
for (const mdFile of mdFiles) {
|
|
2023
2098
|
const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), mdFile), "utf-8");
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
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]);
|
|
2028
2102
|
}
|
|
2029
2103
|
}
|
|
2030
2104
|
return documented;
|
|
@@ -2079,10 +2153,12 @@ function discover(excludes, silent = false) {
|
|
|
2079
2153
|
}
|
|
2080
2154
|
const totalPct = totalEnabled > 0 ? Math.round((totalDocumented / totalEnabled) * 100) : 0;
|
|
2081
2155
|
log(`Coverage: ${String(totalDocumented)}/${String(totalEnabled)} rules documented (${String(totalPct)}%)`);
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
}
|
|
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"));
|
|
2086
2162
|
return { enabled: totalEnabled, documented: totalDocumented };
|
|
2087
2163
|
}
|
|
2088
2164
|
/** This package's own version (from the installed package.json). */
|
|
@@ -2178,21 +2254,40 @@ function discoverAdoptableSurfaces(cwd) {
|
|
|
2178
2254
|
}
|
|
2179
2255
|
return out;
|
|
2180
2256
|
}
|
|
2181
|
-
/**
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2185
|
-
*
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
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)));
|
|
2196
2291
|
}
|
|
2197
2292
|
/** Lint-config file BASENAMES whose CONTENTS (textual, NEVER executed) reveal
|
|
2198
2293
|
* which rules a repo has configured — read best-effort for the rule-inventory
|
|
@@ -2425,32 +2520,47 @@ function computeRuleRouting(root, instructionFile, excludes) {
|
|
|
2425
2520
|
return undefined;
|
|
2426
2521
|
}
|
|
2427
2522
|
}
|
|
2428
|
-
/** The terminal "adoptable surfaces" nudge
|
|
2429
|
-
*
|
|
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
|
|
2430
2527
|
* adopt (a fully spec-managed repo says nothing). */
|
|
2431
2528
|
function formatAdoptableNudge(surfaces) {
|
|
2432
2529
|
if (surfaces.length === 0)
|
|
2433
2530
|
return "";
|
|
2531
|
+
const instructions = surfaces.filter((s) => surfaceKind(s) === "instruction");
|
|
2532
|
+
const others = surfaces.filter((s) => surfaceKind(s) !== "instruction");
|
|
2434
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
|
+
};
|
|
2435
2541
|
// Lead with the VALUE, not the chore: a spec is the pull up from the gate to
|
|
2436
2542
|
// the richer layer, so say what it BUYS (drift-proof references + testable),
|
|
2437
2543
|
// not just "create one". The invitation is where richer-feature adoption is
|
|
2438
2544
|
// won — keep it compelling, never a bare to-do (research/adoption-design.md §1).
|
|
2439
2545
|
const lines = [
|
|
2440
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.`,
|
|
2441
|
-
` Adopt now, or one at a time with \`npx vigiles init --target=<path>\`:`,
|
|
2442
2547
|
];
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
lines.push(`
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
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
|
+
}
|
|
2449
2557
|
// The remembered-decline affordance (non-evil contract): one keystroke to
|
|
2450
2558
|
// silence, stated where it's shown so it's never a surprise nag.
|
|
2451
2559
|
lines.push(` (set "nudge": "dismissed" in .vigilesrc.json to hide this)`);
|
|
2452
2560
|
return lines.join("\n");
|
|
2453
2561
|
}
|
|
2562
|
+
/** How many paths one group of the adoption nudge prints before "+N more". */
|
|
2563
|
+
const NUDGE_LIST_CAP = 10;
|
|
2454
2564
|
/** A small, terse behavioral nudge — the deterministic read can't tell whether a
|
|
2455
2565
|
* skill actually FIRES. "" when there are no model-invocable skills. */
|
|
2456
2566
|
function formatTriggerNudge(triggerableSkills) {
|
|
@@ -3168,7 +3278,11 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3168
3278
|
// file. An explicit --target stays scoped to that one surface.
|
|
3169
3279
|
const targets = targetValue
|
|
3170
3280
|
? instructionTargets
|
|
3171
|
-
: [
|
|
3281
|
+
: [
|
|
3282
|
+
...instructionTargets,
|
|
3283
|
+
...nestedHandWrittenInstructionFiles(cwd).filter((p) => !instructionTargets.includes(p)),
|
|
3284
|
+
...discoverAdoptableSurfaces(cwd),
|
|
3285
|
+
];
|
|
3172
3286
|
// Create specs. An existing hand-written target is faithfully ADOPTED into a
|
|
3173
3287
|
// spec (scaffoldSpec() does the convert), not clobbered with a blank one — so the
|
|
3174
3288
|
// compile below reproduces it (the user reviews the diff). A greenfield target
|
|
@@ -3199,6 +3313,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3199
3313
|
for (const l of typesResult.linters) {
|
|
3200
3314
|
console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
|
|
3201
3315
|
}
|
|
3316
|
+
printUnreadableLinters(typesResult.unreadable);
|
|
3202
3317
|
if (typesResult.scripts.length > 0) {
|
|
3203
3318
|
console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
|
|
3204
3319
|
}
|
|
@@ -3811,6 +3926,7 @@ async function setup(args) {
|
|
|
3811
3926
|
strict,
|
|
3812
3927
|
reportOnly: parsed.reportOnly,
|
|
3813
3928
|
lint: plan.lint,
|
|
3929
|
+
specs: plan.scaffoldSpecs,
|
|
3814
3930
|
written,
|
|
3815
3931
|
});
|
|
3816
3932
|
printSetupSummary({
|
|
@@ -3930,6 +4046,7 @@ function fullSetupGap(plan, detected, harnesses) {
|
|
|
3930
4046
|
const unspecced = pillar1
|
|
3931
4047
|
? [
|
|
3932
4048
|
...determineTargets(detected, undefined, [...harnesses]),
|
|
4049
|
+
...nestedHandWrittenInstructionFiles(cwd),
|
|
3933
4050
|
...discoverAdoptableSurfaces(cwd),
|
|
3934
4051
|
].filter((t, i, all) => all.indexOf(t) === i && !has(`${t}.spec.ts`))
|
|
3935
4052
|
: [];
|
|
@@ -4010,6 +4127,7 @@ function writeProjectConfig(opts) {
|
|
|
4010
4127
|
strict: opts.strict,
|
|
4011
4128
|
reportOnly: opts.reportOnly,
|
|
4012
4129
|
lint: opts.lint,
|
|
4130
|
+
specs: opts.specs,
|
|
4013
4131
|
});
|
|
4014
4132
|
if (!merged)
|
|
4015
4133
|
return;
|
|
@@ -4022,31 +4140,34 @@ function writeProjectConfig(opts) {
|
|
|
4022
4140
|
// ---------------------------------------------------------------------------
|
|
4023
4141
|
// Strengthen: guidance() → enforce() suggestions
|
|
4024
4142
|
// ---------------------------------------------------------------------------
|
|
4025
|
-
function checkIntegrityForFiles(files,
|
|
4143
|
+
function checkIntegrityForFiles(root, files, rules, silent) {
|
|
4026
4144
|
const log = (msg) => {
|
|
4027
4145
|
if (!silent)
|
|
4028
4146
|
console.log(msg);
|
|
4029
4147
|
};
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
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);
|
|
4033
4152
|
const abs = (0, node_path_1.resolve)(basePath, filePath);
|
|
4034
|
-
if (!(0, node_fs_1.existsSync)(abs))
|
|
4035
|
-
|
|
4153
|
+
if (severity === false || !(0, node_fs_1.existsSync)(abs))
|
|
4154
|
+
return [];
|
|
4036
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) {
|
|
4037
4159
|
if (!result.intact) {
|
|
4038
|
-
|
|
4039
|
-
const marker = severity === "error" ? "✗" : "⚠";
|
|
4040
|
-
log(` ${marker} ${filePath} — ${result.reason ?? "tampered"}`);
|
|
4160
|
+
log(` ${markOf(severity).mark} ${filePath} — ${result.reason ?? "tampered"}`);
|
|
4041
4161
|
}
|
|
4042
4162
|
else if (!silent) {
|
|
4043
4163
|
log(` ✓ ${filePath}`);
|
|
4044
4164
|
}
|
|
4045
4165
|
}
|
|
4046
|
-
|
|
4166
|
+
const broken = checked.filter((c) => !c.result.intact);
|
|
4167
|
+
if (broken.length === 0) {
|
|
4047
4168
|
log(" All compiled files intact.");
|
|
4048
4169
|
}
|
|
4049
|
-
return severity === "error"
|
|
4170
|
+
return broken.filter((c) => c.severity === "error").length;
|
|
4050
4171
|
}
|
|
4051
4172
|
/**
|
|
4052
4173
|
* The layout of the harness this repo actually targets — `--harness=`/config/
|
|
@@ -4093,7 +4214,7 @@ const UNTESTED_RULE = {
|
|
|
4093
4214
|
/**
|
|
4094
4215
|
* The `untested-*` rules AS THE DETECTOR TAKES THEM: which kinds this repo
|
|
4095
4216
|
* enabled, and the discovery options merged from whichever of the three rules
|
|
4096
|
-
* carries them (`include` / `
|
|
4217
|
+
* carries them (`include` / `testExtension` are shared).
|
|
4097
4218
|
*
|
|
4098
4219
|
* 🔴 ONE READER, BECAUSE THE SECOND ONE DRIFTED. `vigiles lint` read the config
|
|
4099
4220
|
* here; the PostToolUse nudge (`evalLockNudgeHookCommand`) called the same
|
|
@@ -4104,25 +4225,24 @@ const UNTESTED_RULE = {
|
|
|
4104
4225
|
* while the nudge said "no test or eval covers it". A nudge contradicting the
|
|
4105
4226
|
* linter of the same repo, in the same second, teaches people to ignore both.
|
|
4106
4227
|
*/
|
|
4107
|
-
function untestedRules(config) {
|
|
4108
|
-
const rules = config?.rules;
|
|
4109
|
-
const severity = (kind) => (0, types_js_1.ruleSeverity)(rules?.[UNTESTED_RULE[kind]]);
|
|
4228
|
+
function untestedRules(config, rules) {
|
|
4110
4229
|
const names = Object.values(UNTESTED_RULE);
|
|
4111
4230
|
const opts = names.reduce((acc, rule) => ({
|
|
4112
4231
|
...acc,
|
|
4113
|
-
...(0, types_js_1.ruleOptions)(rules?.[rule]),
|
|
4232
|
+
...(0, types_js_1.ruleOptions)(config?.rules?.[rule]),
|
|
4114
4233
|
}), {});
|
|
4115
|
-
|
|
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]);
|
|
4116
4237
|
return {
|
|
4117
|
-
severity,
|
|
4118
|
-
anyEnabled: names.some((rule) =>
|
|
4238
|
+
severity: (kind, path) => rules.severityAt(UNTESTED_RULE[kind], path),
|
|
4239
|
+
anyEnabled: names.some((rule) => rules.mayFire(rule)),
|
|
4119
4240
|
options: {
|
|
4120
4241
|
skills: on("skill"),
|
|
4121
4242
|
agents: on("agent"),
|
|
4122
4243
|
hooks: on("hook"),
|
|
4123
4244
|
outputStyles: on("output-style"),
|
|
4124
4245
|
include: opts.include,
|
|
4125
|
-
exclude: opts.exclude,
|
|
4126
4246
|
// Without this the `testExtension` documented on TestCoverageOptions was a
|
|
4127
4247
|
// config key nothing read: a TypeScript-shaped repo got `.ts` suggestions
|
|
4128
4248
|
// however the author configured it. Prose isn't policy, in our own CLI.
|
|
@@ -4139,33 +4259,37 @@ function untestedRules(config) {
|
|
|
4139
4259
|
*/
|
|
4140
4260
|
function checkUntestedSurfaces(ctx) {
|
|
4141
4261
|
const { config, silent, adapter, frame, bundle } = ctx;
|
|
4142
|
-
const { severity: sevFor, anyEnabled, options } = untestedRules(config);
|
|
4262
|
+
const { severity: sevFor, anyEnabled, options, } = untestedRules(config, ctx.rules);
|
|
4143
4263
|
if (!anyEnabled)
|
|
4144
4264
|
return { untested: 0, errors: 0 };
|
|
4145
|
-
const
|
|
4265
|
+
const scanned = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
4146
4266
|
...options,
|
|
4147
4267
|
basePath: bundle.abs,
|
|
4148
|
-
// #281: `include
|
|
4268
|
+
// #281: `include` and every reported path are relative to the
|
|
4149
4269
|
// frame root (where `.vigilesrc.json` lives), not to the bundle being scored.
|
|
4150
4270
|
root: frame.root,
|
|
4151
4271
|
layout: adapter.layout,
|
|
4152
|
-
// The
|
|
4153
|
-
// it. Union, never override — a rule option must not re-admit a vendored
|
|
4154
|
-
// 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).
|
|
4155
4273
|
excludes: ctx.excludes,
|
|
4156
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 };
|
|
4157
4281
|
if (!silent) {
|
|
4158
4282
|
console.log("\nUntested surfaces:\n");
|
|
4159
4283
|
for (const line of (0, test_coverage_js_1.formatUntestedReport)(report).split("\n")) {
|
|
4160
4284
|
console.log(` ${line}`);
|
|
4161
4285
|
}
|
|
4162
4286
|
for (const s of report.untested) {
|
|
4163
|
-
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)));
|
|
4164
4288
|
}
|
|
4165
4289
|
}
|
|
4166
4290
|
return {
|
|
4167
4291
|
untested: report.untested.length,
|
|
4168
|
-
errors:
|
|
4292
|
+
errors: untested.filter((s) => s.sev === "error").length,
|
|
4169
4293
|
};
|
|
4170
4294
|
}
|
|
4171
4295
|
/**
|
|
@@ -4190,9 +4314,8 @@ function reportNotApplicable(check, surface, adapter, silent) {
|
|
|
4190
4314
|
* `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
|
|
4191
4315
|
*/
|
|
4192
4316
|
function checkSubagentToolContracts(ctx) {
|
|
4193
|
-
const {
|
|
4194
|
-
|
|
4195
|
-
if (!sev)
|
|
4317
|
+
const { silent, adapter } = ctx;
|
|
4318
|
+
if (!ctx.rules.mayFire("subagent-tool-contract"))
|
|
4196
4319
|
return { issues: 0, errors: 0 };
|
|
4197
4320
|
if (!adapter.subagents) {
|
|
4198
4321
|
reportNotApplicable("Subagent tool-contract check", "subagents", adapter, silent);
|
|
@@ -4208,24 +4331,16 @@ function checkSubagentToolContracts(ctx) {
|
|
|
4208
4331
|
catch {
|
|
4209
4332
|
return { issues: 0, errors: 0 };
|
|
4210
4333
|
}
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
if (!printedHeader) {
|
|
4219
|
-
console.log("\nSubagent tool-contract check:\n");
|
|
4220
|
-
printedHeader = true;
|
|
4221
|
-
}
|
|
4222
|
-
for (const issue of agent.toolIssues) {
|
|
4223
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(agent.path)}: ${issue.message}`);
|
|
4224
|
-
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(agent.path));
|
|
4225
|
-
}
|
|
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);
|
|
4226
4341
|
}
|
|
4227
4342
|
}
|
|
4228
|
-
return
|
|
4343
|
+
return tally(found);
|
|
4229
4344
|
}
|
|
4230
4345
|
/**
|
|
4231
4346
|
* Apply the `hook-events` rule: flag a hook registered under an event name the
|
|
@@ -4266,30 +4381,30 @@ function checkHookEvents(ctx) {
|
|
|
4266
4381
|
* `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
|
|
4267
4382
|
*/
|
|
4268
4383
|
function checkFrontmatterSchema(ctx) {
|
|
4269
|
-
const {
|
|
4270
|
-
|
|
4271
|
-
if (!sev)
|
|
4384
|
+
const { silent, adapter } = ctx;
|
|
4385
|
+
if (!ctx.rules.mayFire("subagent-frontmatter"))
|
|
4272
4386
|
return { issues: 0, errors: 0 };
|
|
4273
4387
|
if (!adapter.subagents) {
|
|
4274
4388
|
reportNotApplicable("Subagent-frontmatter check", "subagents", adapter, silent);
|
|
4275
4389
|
return { issues: 0, errors: 0 };
|
|
4276
4390
|
}
|
|
4277
|
-
let
|
|
4391
|
+
let raw;
|
|
4278
4392
|
try {
|
|
4279
4393
|
const r = ctx.scan();
|
|
4280
|
-
|
|
4394
|
+
raw = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
|
|
4281
4395
|
}
|
|
4282
4396
|
catch {
|
|
4283
4397
|
return { issues: 0, errors: 0 };
|
|
4284
4398
|
}
|
|
4399
|
+
const found = graded(ctx.rules, "subagent-frontmatter", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4285
4400
|
if (found.length > 0 && !silent) {
|
|
4286
4401
|
console.log("\nFrontmatter-schema check:\n");
|
|
4287
4402
|
for (const issue of found) {
|
|
4288
|
-
console.log(` ${sev
|
|
4289
|
-
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));
|
|
4290
4405
|
}
|
|
4291
4406
|
}
|
|
4292
|
-
return
|
|
4407
|
+
return tally(found);
|
|
4293
4408
|
}
|
|
4294
4409
|
/**
|
|
4295
4410
|
* Apply the `skill-frontmatter` rule: RECOMMEND (not require) that a SKILL.md
|
|
@@ -4300,54 +4415,25 @@ function checkFrontmatterSchema(ctx) {
|
|
|
4300
4415
|
* `skillMetaIssues`.
|
|
4301
4416
|
*/
|
|
4302
4417
|
function checkSkillFrontmatter(ctx) {
|
|
4303
|
-
const {
|
|
4304
|
-
|
|
4305
|
-
if (!sev)
|
|
4418
|
+
const { silent } = ctx;
|
|
4419
|
+
if (!ctx.rules.mayFire("skill-frontmatter"))
|
|
4306
4420
|
return { issues: 0, errors: 0 };
|
|
4307
|
-
let
|
|
4421
|
+
let raw;
|
|
4308
4422
|
try {
|
|
4309
|
-
|
|
4423
|
+
raw = ctx.scan().skillMetaIssues;
|
|
4310
4424
|
}
|
|
4311
4425
|
catch {
|
|
4312
4426
|
return { issues: 0, errors: 0 };
|
|
4313
4427
|
}
|
|
4428
|
+
const found = graded(ctx.rules, "skill-frontmatter", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4314
4429
|
if (found.length > 0 && !silent) {
|
|
4315
4430
|
console.log("\nSkill-frontmatter check:\n");
|
|
4316
4431
|
for (const issue of found) {
|
|
4317
|
-
console.log(` ${sev
|
|
4318
|
-
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));
|
|
4319
4434
|
}
|
|
4320
4435
|
}
|
|
4321
|
-
return
|
|
4322
|
-
}
|
|
4323
|
-
/**
|
|
4324
|
-
* Apply the `prefer-compiled-hooks` rule: a SINGLE repo-level recommendation
|
|
4325
|
-
* (one finding regardless of hook count) nudging hand-written hooks toward
|
|
4326
|
-
* compiled `vigiles/hook` artifacts. A discovery nudge, not a defect — the shell
|
|
4327
|
-
* lane stays first-class — so it fires once and the message links the guide.
|
|
4328
|
-
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
4329
|
-
*/
|
|
4330
|
-
function checkPreferCompiledHooks(ctx) {
|
|
4331
|
-
const { config, silent } = ctx;
|
|
4332
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
4333
|
-
if (!sev)
|
|
4334
|
-
return { issues: 0, errors: 0 };
|
|
4335
|
-
let count;
|
|
4336
|
-
try {
|
|
4337
|
-
count = ctx.scan().manualHookCount;
|
|
4338
|
-
}
|
|
4339
|
-
catch {
|
|
4340
|
-
return { issues: 0, errors: 0 };
|
|
4341
|
-
}
|
|
4342
|
-
if (count === 0)
|
|
4343
|
-
return { issues: 0, errors: 0 };
|
|
4344
|
-
const message = (0, scan_js_1.preferCompiledHooksMessage)(count);
|
|
4345
|
-
if (!silent) {
|
|
4346
|
-
console.log("\nCompiled-hooks check:\n");
|
|
4347
|
-
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${ctx.label(message)}`);
|
|
4348
|
-
ctx.annotate(sev === "error" ? "error" : "warning", message);
|
|
4349
|
-
}
|
|
4350
|
-
return { issues: 1, errors: sev === "error" ? 1 : 0 };
|
|
4436
|
+
return tally(found);
|
|
4351
4437
|
}
|
|
4352
4438
|
/**
|
|
4353
4439
|
* Apply the `mcp-config` rule: a declared MCP server with neither a `command`
|
|
@@ -4383,17 +4469,16 @@ function checkMcpConfig(ctx) {
|
|
|
4383
4469
|
* "error" gates CI.
|
|
4384
4470
|
*/
|
|
4385
4471
|
function checkDisallowedTools(ctx) {
|
|
4386
|
-
const {
|
|
4387
|
-
|
|
4388
|
-
if (!sev)
|
|
4472
|
+
const { silent, adapter } = ctx;
|
|
4473
|
+
if (!ctx.rules.mayFire("disallowed-tools-contract"))
|
|
4389
4474
|
return { issues: 0, errors: 0 };
|
|
4390
4475
|
if (!adapter.subagents) {
|
|
4391
4476
|
reportNotApplicable("Disallowed-tools check", "subagents", adapter, silent);
|
|
4392
4477
|
return { issues: 0, errors: 0 };
|
|
4393
4478
|
}
|
|
4394
|
-
let
|
|
4479
|
+
let raw;
|
|
4395
4480
|
try {
|
|
4396
|
-
|
|
4481
|
+
raw = ctx.scan().agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({
|
|
4397
4482
|
message: i.message,
|
|
4398
4483
|
path: a.path,
|
|
4399
4484
|
})));
|
|
@@ -4401,14 +4486,15 @@ function checkDisallowedTools(ctx) {
|
|
|
4401
4486
|
catch {
|
|
4402
4487
|
return { issues: 0, errors: 0 };
|
|
4403
4488
|
}
|
|
4489
|
+
const found = graded(ctx.rules, "disallowed-tools-contract", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4404
4490
|
if (found.length > 0 && !silent) {
|
|
4405
4491
|
console.log("\nDisallowed-tools check:\n");
|
|
4406
4492
|
for (const issue of found) {
|
|
4407
|
-
console.log(` ${sev
|
|
4408
|
-
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));
|
|
4409
4495
|
}
|
|
4410
4496
|
}
|
|
4411
|
-
return
|
|
4497
|
+
return tally(found);
|
|
4412
4498
|
}
|
|
4413
4499
|
/**
|
|
4414
4500
|
* Apply the `frontmatter-valid` rule: a skill/agent `---` block that EXISTS but
|
|
@@ -4419,25 +4505,25 @@ function checkDisallowedTools(ctx) {
|
|
|
4419
4505
|
* (verify before setting "error").
|
|
4420
4506
|
*/
|
|
4421
4507
|
function checkFrontmatterValid(ctx) {
|
|
4422
|
-
const {
|
|
4423
|
-
|
|
4424
|
-
if (!sev)
|
|
4508
|
+
const { silent } = ctx;
|
|
4509
|
+
if (!ctx.rules.mayFire("frontmatter-valid"))
|
|
4425
4510
|
return { issues: 0, errors: 0 };
|
|
4426
|
-
let
|
|
4511
|
+
let raw;
|
|
4427
4512
|
try {
|
|
4428
|
-
|
|
4513
|
+
raw = ctx.scan().malformedFrontmatter;
|
|
4429
4514
|
}
|
|
4430
4515
|
catch {
|
|
4431
4516
|
return { issues: 0, errors: 0 };
|
|
4432
4517
|
}
|
|
4518
|
+
const found = graded(ctx.rules, "frontmatter-valid", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4433
4519
|
if (found.length > 0 && !silent) {
|
|
4434
4520
|
console.log("\nFrontmatter-validity check:\n");
|
|
4435
4521
|
for (const issue of found) {
|
|
4436
|
-
console.log(` ${sev
|
|
4437
|
-
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));
|
|
4438
4524
|
}
|
|
4439
4525
|
}
|
|
4440
|
-
return
|
|
4526
|
+
return tally(found);
|
|
4441
4527
|
}
|
|
4442
4528
|
/**
|
|
4443
4529
|
* Apply the `description-overlap` rule: two model-invocable skills with
|
|
@@ -4512,38 +4598,40 @@ function checkDescriptionBudget(ctx) {
|
|
|
4512
4598
|
* unaffected by the collapse.
|
|
4513
4599
|
*/
|
|
4514
4600
|
function checkLethalTrifecta(ctx) {
|
|
4515
|
-
const {
|
|
4516
|
-
|
|
4517
|
-
if (!sev)
|
|
4601
|
+
const { silent } = ctx;
|
|
4602
|
+
if (!ctx.rules.mayFire("lethal-trifecta"))
|
|
4518
4603
|
return { issues: 0, errors: 0 };
|
|
4519
|
-
let
|
|
4604
|
+
let raw;
|
|
4520
4605
|
try {
|
|
4521
|
-
|
|
4606
|
+
raw = ctx.scan().trifectaFindings;
|
|
4522
4607
|
}
|
|
4523
4608
|
catch {
|
|
4524
4609
|
return { issues: 0, errors: 0 };
|
|
4525
4610
|
}
|
|
4611
|
+
const found = graded(ctx.rules, "lethal-trifecta", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4526
4612
|
if (found.length > 0 && !silent) {
|
|
4527
|
-
const mark = sev === "error" ? "✗" : "⚠";
|
|
4528
|
-
const level = sev === "error" ? "error" : "warning";
|
|
4529
4613
|
console.log("\nLethal-trifecta check:\n");
|
|
4530
4614
|
const unfenced = found.filter((t) => t.kind === "skill" && t.finding.fence === "none");
|
|
4531
4615
|
for (const t of found) {
|
|
4532
4616
|
if (unfenced.includes(t))
|
|
4533
4617
|
continue;
|
|
4534
4618
|
const msg = `${t.kind} ${t.name}: ${t.finding.message}`;
|
|
4535
|
-
console.log(` ${mark} ${ctx.bundle.scanned(t.path)}: ${msg}`);
|
|
4536
|
-
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));
|
|
4537
4621
|
}
|
|
4538
4622
|
if (unfenced.length > 0) {
|
|
4539
|
-
|
|
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 ` +
|
|
4540
4628
|
`inherits every tool the session grants and holds all three legs. ` +
|
|
4541
4629
|
`\`allowed-tools:\` pre-approves, it does not restrict. ` +
|
|
4542
4630
|
`Add one \`disallowed-tools:\` line per skill to drop a leg: ` +
|
|
4543
4631
|
unfenced.map((t) => t.name).join(", "));
|
|
4544
4632
|
}
|
|
4545
4633
|
}
|
|
4546
|
-
return
|
|
4634
|
+
return tally(found);
|
|
4547
4635
|
}
|
|
4548
4636
|
/**
|
|
4549
4637
|
* Apply the `skill-resource-resolves` rule: a SKILL.md body referencing a bundled
|
|
@@ -4554,12 +4642,11 @@ function checkLethalTrifecta(ctx) {
|
|
|
4554
4642
|
*/
|
|
4555
4643
|
function checkSkillResourceResolves(ctx) {
|
|
4556
4644
|
const { config, silent } = ctx;
|
|
4557
|
-
|
|
4558
|
-
if (!sev)
|
|
4645
|
+
if (!ctx.rules.mayFire("skill-resource-resolves"))
|
|
4559
4646
|
return { issues: 0, errors: 0 };
|
|
4560
|
-
let
|
|
4647
|
+
let raw;
|
|
4561
4648
|
try {
|
|
4562
|
-
|
|
4649
|
+
raw = ctx.scan({
|
|
4563
4650
|
sharedDirs: config?.sharedDirs,
|
|
4564
4651
|
// sharedDirs live at the frame root — the repo that OWNS the scan target:
|
|
4565
4652
|
// cwd for a scoped subdir or a nested bundle, the target for a foreign lint.
|
|
@@ -4569,6 +4656,7 @@ function checkSkillResourceResolves(ctx) {
|
|
|
4569
4656
|
catch {
|
|
4570
4657
|
return { issues: 0, errors: 0 };
|
|
4571
4658
|
}
|
|
4659
|
+
const found = graded(ctx.rules, "skill-resource-resolves", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4572
4660
|
if (found.length > 0 && !silent) {
|
|
4573
4661
|
console.log("\nSkill-resource check:\n");
|
|
4574
4662
|
for (const s of found) {
|
|
@@ -4578,11 +4666,11 @@ function checkSkillResourceResolves(ctx) {
|
|
|
4578
4666
|
// it was documented only in docs/skills-monorepo.md, so a CI log gave no
|
|
4579
4667
|
// hint and the rule read as broken rather than misconfigured.
|
|
4580
4668
|
` If it resolves from the repo root instead, add its directory to \`sharedDirs\` in .vigilesrc.json.`;
|
|
4581
|
-
console.log(` ${sev
|
|
4582
|
-
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));
|
|
4583
4671
|
}
|
|
4584
4672
|
}
|
|
4585
|
-
return
|
|
4673
|
+
return tally(found);
|
|
4586
4674
|
}
|
|
4587
4675
|
/**
|
|
4588
4676
|
* Apply the `skill-missing-fence` rule: a SKILL.md that opens with
|
|
@@ -4592,26 +4680,26 @@ function checkSkillResourceResolves(ctx) {
|
|
|
4592
4680
|
* default; "error" gates CI.
|
|
4593
4681
|
*/
|
|
4594
4682
|
function checkSkillMissingFence(ctx) {
|
|
4595
|
-
const {
|
|
4596
|
-
|
|
4597
|
-
if (!sev)
|
|
4683
|
+
const { silent } = ctx;
|
|
4684
|
+
if (!ctx.rules.mayFire("skill-missing-fence"))
|
|
4598
4685
|
return { issues: 0, errors: 0 };
|
|
4599
|
-
let
|
|
4686
|
+
let raw;
|
|
4600
4687
|
try {
|
|
4601
|
-
|
|
4688
|
+
raw = ctx.scan().skillFenceIssues;
|
|
4602
4689
|
}
|
|
4603
4690
|
catch {
|
|
4604
4691
|
return { issues: 0, errors: 0 };
|
|
4605
4692
|
}
|
|
4693
|
+
const found = graded(ctx.rules, "skill-missing-fence", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4606
4694
|
if (found.length > 0 && !silent) {
|
|
4607
4695
|
console.log("\nSkill-missing-fence check:\n");
|
|
4608
4696
|
for (const s of found) {
|
|
4609
4697
|
const msg = `${s.name}: ${s.finding.message}`;
|
|
4610
|
-
console.log(` ${sev
|
|
4611
|
-
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));
|
|
4612
4700
|
}
|
|
4613
4701
|
}
|
|
4614
|
-
return
|
|
4702
|
+
return tally(found);
|
|
4615
4703
|
}
|
|
4616
4704
|
/**
|
|
4617
4705
|
* Apply the `plugin-dir-layout` rule: functional surface dirs (skills/agents/
|
|
@@ -4621,25 +4709,25 @@ function checkSkillMissingFence(ctx) {
|
|
|
4621
4709
|
* gates CI.
|
|
4622
4710
|
*/
|
|
4623
4711
|
function checkPluginDirLayout(ctx) {
|
|
4624
|
-
const {
|
|
4625
|
-
|
|
4626
|
-
if (!sev)
|
|
4712
|
+
const { silent } = ctx;
|
|
4713
|
+
if (!ctx.rules.mayFire("plugin-dir-layout"))
|
|
4627
4714
|
return { issues: 0, errors: 0 };
|
|
4628
|
-
let
|
|
4715
|
+
let raw;
|
|
4629
4716
|
try {
|
|
4630
|
-
|
|
4717
|
+
raw = ctx.scan().pluginLayoutIssues;
|
|
4631
4718
|
}
|
|
4632
4719
|
catch {
|
|
4633
4720
|
return { issues: 0, errors: 0 };
|
|
4634
4721
|
}
|
|
4722
|
+
const found = graded(ctx.rules, "plugin-dir-layout", raw, (f) => ctx.bundle.scanned(f.dir));
|
|
4635
4723
|
if (found.length > 0 && !silent) {
|
|
4636
4724
|
console.log("\nPlugin-dir-layout check:\n");
|
|
4637
4725
|
for (const p of found) {
|
|
4638
|
-
console.log(` ${sev
|
|
4639
|
-
ctx.annotate(sev
|
|
4726
|
+
console.log(` ${markOf(p.sev).mark} ${ctx.label(p.message)}`);
|
|
4727
|
+
ctx.annotate(markOf(p.sev).level, p.message);
|
|
4640
4728
|
}
|
|
4641
4729
|
}
|
|
4642
|
-
return
|
|
4730
|
+
return tally(found);
|
|
4643
4731
|
}
|
|
4644
4732
|
/**
|
|
4645
4733
|
* Apply the `delegation-trifecta` rule: a lethal trifecta that EMERGES across a
|
|
@@ -4650,26 +4738,26 @@ function checkPluginDirLayout(ctx) {
|
|
|
4650
4738
|
* capability the way a surface-specific rule is.
|
|
4651
4739
|
*/
|
|
4652
4740
|
function checkDelegationTrifecta(ctx) {
|
|
4653
|
-
const {
|
|
4654
|
-
|
|
4655
|
-
if (!sev)
|
|
4741
|
+
const { silent } = ctx;
|
|
4742
|
+
if (!ctx.rules.mayFire("delegation-trifecta"))
|
|
4656
4743
|
return { issues: 0, errors: 0 };
|
|
4657
|
-
let
|
|
4744
|
+
let raw;
|
|
4658
4745
|
try {
|
|
4659
|
-
|
|
4746
|
+
raw = ctx.scan().delegationTrifecta;
|
|
4660
4747
|
}
|
|
4661
4748
|
catch {
|
|
4662
4749
|
return { issues: 0, errors: 0 };
|
|
4663
4750
|
}
|
|
4751
|
+
const found = graded(ctx.rules, "delegation-trifecta", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4664
4752
|
if (found.length > 0 && !silent) {
|
|
4665
4753
|
console.log("\nDelegation-trifecta check:\n");
|
|
4666
4754
|
for (const d of found) {
|
|
4667
4755
|
const msg = `${d.finding.name}: ${d.finding.message}`;
|
|
4668
|
-
console.log(` ${sev
|
|
4669
|
-
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));
|
|
4670
4758
|
}
|
|
4671
4759
|
}
|
|
4672
|
-
return
|
|
4760
|
+
return tally(found);
|
|
4673
4761
|
}
|
|
4674
4762
|
/**
|
|
4675
4763
|
* Apply the `hook-block-ineffective` rule: a hook that LOOKS like it blocks but
|
|
@@ -4787,43 +4875,57 @@ function checkMcpHookTargets(ctx) {
|
|
|
4787
4875
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
4788
4876
|
}
|
|
4789
4877
|
/**
|
|
4790
|
-
* Apply the `hook-script-exists` rule: a
|
|
4791
|
-
*
|
|
4792
|
-
*
|
|
4793
|
-
*
|
|
4794
|
-
* existence-guarded one-liners, inline commands). Matches Anthropic's own
|
|
4795
|
-
* `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.
|
|
4796
4882
|
*/
|
|
4797
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) {
|
|
4798
4904
|
const { config, silent, adapter } = ctx;
|
|
4799
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.[
|
|
4905
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.[rule]);
|
|
4800
4906
|
if (!sev)
|
|
4801
4907
|
return { issues: 0, errors: 0 };
|
|
4802
4908
|
if (!adapter.shellHooks) {
|
|
4803
|
-
reportNotApplicable(
|
|
4909
|
+
reportNotApplicable(heading, "shell hooks", adapter, silent);
|
|
4804
4910
|
return { issues: 0, errors: 0 };
|
|
4805
4911
|
}
|
|
4806
|
-
let
|
|
4912
|
+
let messages;
|
|
4807
4913
|
try {
|
|
4808
|
-
|
|
4914
|
+
messages = messagesOf(ctx.scan());
|
|
4809
4915
|
}
|
|
4810
4916
|
catch {
|
|
4811
4917
|
return { issues: 0, errors: 0 };
|
|
4812
4918
|
}
|
|
4813
|
-
if (
|
|
4814
|
-
console.log(
|
|
4815
|
-
for (const
|
|
4816
|
-
// `script` arrives with the plugin-root token EXPANDED, i.e. absolute —
|
|
4817
|
-
// printed as-is it was a machine path (#281). A relative one is the
|
|
4818
|
-
// bundle's own, which is what `scanned` assumes.
|
|
4819
|
-
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) {
|
|
4820
4922
|
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(msg)}`);
|
|
4821
4923
|
ctx.annotate(sev === "error" ? "error" : "warning", msg);
|
|
4822
4924
|
}
|
|
4823
4925
|
}
|
|
4824
4926
|
return {
|
|
4825
|
-
issues:
|
|
4826
|
-
errors: sev === "error" ?
|
|
4927
|
+
issues: messages.length,
|
|
4928
|
+
errors: sev === "error" ? messages.length : 0,
|
|
4827
4929
|
};
|
|
4828
4930
|
}
|
|
4829
4931
|
/**
|
|
@@ -4834,31 +4936,31 @@ function checkHookScriptExists(ctx) {
|
|
|
4834
4936
|
* plugin-namespaced form skipped). Warning by default; "error" gates CI.
|
|
4835
4937
|
*/
|
|
4836
4938
|
function checkMcpToolResolves(ctx) {
|
|
4837
|
-
const {
|
|
4838
|
-
|
|
4839
|
-
if (!sev)
|
|
4939
|
+
const { silent, adapter } = ctx;
|
|
4940
|
+
if (!ctx.rules.mayFire("mcp-tool-resolves"))
|
|
4840
4941
|
return { issues: 0, errors: 0 };
|
|
4841
4942
|
if (!adapter.subagents) {
|
|
4842
4943
|
reportNotApplicable("MCP tool-resolution check", "subagents", adapter, silent);
|
|
4843
4944
|
return { issues: 0, errors: 0 };
|
|
4844
4945
|
}
|
|
4845
|
-
let
|
|
4946
|
+
let raw;
|
|
4846
4947
|
try {
|
|
4847
|
-
|
|
4948
|
+
raw = ctx
|
|
4848
4949
|
.scan()
|
|
4849
4950
|
.agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
4850
4951
|
}
|
|
4851
4952
|
catch {
|
|
4852
4953
|
return { issues: 0, errors: 0 };
|
|
4853
4954
|
}
|
|
4955
|
+
const found = graded(ctx.rules, "mcp-tool-resolves", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4854
4956
|
if (found.length > 0 && !silent) {
|
|
4855
4957
|
console.log("\nMCP tool-resolution check:\n");
|
|
4856
4958
|
for (const issue of found) {
|
|
4857
|
-
console.log(` ${sev
|
|
4858
|
-
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));
|
|
4859
4961
|
}
|
|
4860
4962
|
}
|
|
4861
|
-
return
|
|
4963
|
+
return tally(found);
|
|
4862
4964
|
}
|
|
4863
4965
|
/**
|
|
4864
4966
|
* Apply the configured coverage thresholds. Returns the number of failing
|
|
@@ -5082,6 +5184,17 @@ async function handleGenerate(restArgs, args) {
|
|
|
5082
5184
|
process.exit(2);
|
|
5083
5185
|
}
|
|
5084
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
|
+
}
|
|
5085
5198
|
function handleGenerateTypes(args, restArgs) {
|
|
5086
5199
|
const checkOnly = args.includes("--check");
|
|
5087
5200
|
const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
|
|
@@ -5097,6 +5210,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
5097
5210
|
for (const l of result.linters) {
|
|
5098
5211
|
console.log(` ${l.linter}: ${String(l.rules.length)} enabled rules (via ${l.via})`);
|
|
5099
5212
|
}
|
|
5213
|
+
printUnreadableLinters(result.unreadable);
|
|
5100
5214
|
if (result.scripts.length > 0) {
|
|
5101
5215
|
console.log(` npm scripts: ${String(result.scripts.length)}`);
|
|
5102
5216
|
}
|
|
@@ -6382,7 +6496,8 @@ function evalLockNudgeHookCommand() {
|
|
|
6382
6496
|
// into a failed edit: the nudge not firing is the cheaper failure. The verbs
|
|
6383
6497
|
// throw on the same config; what is CHECKED is identical.
|
|
6384
6498
|
const config = (0, validate_js_1.loadConfig)(cwd, { onInvalid: "warn" });
|
|
6385
|
-
const
|
|
6499
|
+
const nudgeRules = (0, overrides_js_1.ruleResolver)(config.rules, config.overrides);
|
|
6500
|
+
const { options } = untestedRules(config, nudgeRules);
|
|
6386
6501
|
// 🔴 THE SAME LAYOUT `vigiles lint` RESOLVES, for the same reason as the config
|
|
6387
6502
|
// above. This used to pass `basePath` alone, so the detector fell back to the
|
|
6388
6503
|
// Claude Code layout: in a Codex repo whose surfaces live at
|
|
@@ -6397,9 +6512,8 @@ function evalLockNudgeHookCommand() {
|
|
|
6397
6512
|
// heard nothing at all, while a repo that already tests got reminded. That is
|
|
6398
6513
|
// backwards, and `untested-skill` already stated the missing half correctly;
|
|
6399
6514
|
// it just lived in `vigiles lint`, which someone has to run by hand.
|
|
6400
|
-
//
|
|
6401
|
-
//
|
|
6402
|
-
// 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.
|
|
6403
6517
|
const excludes = (0, exclude_js_1.excludeSet)(cwd, config.exclude);
|
|
6404
6518
|
// 🔴 THE BUNDLE THAT HOLDS THE EDIT, found the way `lint` finds it (#281, D6).
|
|
6405
6519
|
// The nudge scanned the root bundle only and matched by SUFFIX, so an edit to
|
|
@@ -6416,13 +6530,18 @@ function evalLockNudgeHookCommand() {
|
|
|
6416
6530
|
return r !== "" && !r.startsWith("..") && !(0, node_path_1.isAbsolute)(r);
|
|
6417
6531
|
})
|
|
6418
6532
|
: undefined) ?? cwd;
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
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));
|
|
6426
6545
|
if (!msg)
|
|
6427
6546
|
return;
|
|
6428
6547
|
process.stdout.write(JSON.stringify({
|
|
@@ -6462,7 +6581,10 @@ async function refsHookCommand() {
|
|
|
6462
6581
|
// from the process's directory is how a disabled rule comes back to life.
|
|
6463
6582
|
const cwd = runtimeRoot(eventRoot(raw));
|
|
6464
6583
|
// A hook rail — see `evalLockNudgeHookCommand` for why it warns, not throws.
|
|
6465
|
-
|
|
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)));
|
|
6466
6588
|
if (severity === false)
|
|
6467
6589
|
return;
|
|
6468
6590
|
const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
|
|
@@ -6542,7 +6664,7 @@ async function compileProviders() {
|
|
|
6542
6664
|
* the string the user typed — `x.hook.ts` and `./x.hook.ts` are the same wiring),
|
|
6543
6665
|
* so recompiling updates in place and never clobbers the user's own hooks.
|
|
6544
6666
|
*/
|
|
6545
|
-
async function installHookFile(file, adapter, registeredProviders
|
|
6667
|
+
async function installHookFile(file, adapter, registeredProviders, runtime) {
|
|
6546
6668
|
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
|
|
6547
6669
|
const program = await (0, hook_runtime_js_1.loadHookProgram)(file);
|
|
6548
6670
|
// Emit the CANONICAL path so two spellings of the same file produce the same
|
|
@@ -6561,16 +6683,17 @@ async function installHookFile(file, adapter, registeredProviders = []) {
|
|
|
6561
6683
|
// cache, thirteen times, on every tool call. The `|| exit N` ending is what
|
|
6562
6684
|
// the shell does when that file cannot start at all. `compileHookProgram`
|
|
6563
6685
|
// adds it (see `hookRuntimeMissingExit`), so it is not written here.
|
|
6564
|
-
gateCommand:
|
|
6565
|
-
(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)),
|
|
6566
6687
|
dialect: adapter.dialect,
|
|
6567
6688
|
hookProtocol: adapter.hookProtocol,
|
|
6568
6689
|
settings: adapter.layout.settings,
|
|
6569
6690
|
registeredProviders,
|
|
6570
6691
|
});
|
|
6571
|
-
// 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.
|
|
6572
6695
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)((0, hook_runtime_js_1.hookStampPath)(file)), { recursive: true });
|
|
6573
|
-
(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");
|
|
6574
6697
|
// Merge into the harness's native config, idempotently.
|
|
6575
6698
|
//
|
|
6576
6699
|
// 🔴 THREE FORMAT BRANCHES BECAME ZERO, and they were three because the ONE
|
|
@@ -6646,7 +6769,14 @@ async function installHookFile(file, adapter, registeredProviders = []) {
|
|
|
6646
6769
|
`yet automatic).`;
|
|
6647
6770
|
}
|
|
6648
6771
|
}
|
|
6649
|
-
|
|
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
|
+
};
|
|
6650
6780
|
}
|
|
6651
6781
|
/**
|
|
6652
6782
|
* Compile + install every hook (explicit paths, else discovered under
|
|
@@ -6680,15 +6810,29 @@ async function installHooks(hookFiles, harnessFlag, configHarness, validateProvi
|
|
|
6680
6810
|
}
|
|
6681
6811
|
if (hookFiles.length === 0)
|
|
6682
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
|
+
}
|
|
6683
6826
|
let ok = true;
|
|
6684
6827
|
for (const file of hookFiles) {
|
|
6685
6828
|
try {
|
|
6686
6829
|
// Fan out: the same compiled hook lands in each enabled harness's config.
|
|
6687
6830
|
for (const adapter of adapters) {
|
|
6688
|
-
const r = await installHookFile(file, adapter, registeredProviders);
|
|
6831
|
+
const r = await installHookFile(file, adapter, registeredProviders, runtime);
|
|
6689
6832
|
console.log(`✓ ${file} → ${r.settingsPath} (role: ${r.role}, harness: ${adapter.name})`);
|
|
6690
|
-
|
|
6691
|
-
console.warn(`⚠ ${
|
|
6833
|
+
r.warnings.forEach((w) => {
|
|
6834
|
+
console.warn(`⚠ ${w}`);
|
|
6835
|
+
});
|
|
6692
6836
|
}
|
|
6693
6837
|
}
|
|
6694
6838
|
catch (e) {
|
|
@@ -7091,6 +7235,30 @@ async function askOnce(q) {
|
|
|
7091
7235
|
rl.close();
|
|
7092
7236
|
}
|
|
7093
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
|
+
}
|
|
7094
7262
|
/**
|
|
7095
7263
|
* Persist the audit consent (`audit.measure`) into `.vigilesrc.json` without
|
|
7096
7264
|
* clobbering existing keys — the "ask once, remember" sticky choice. Best-effort:
|
|
@@ -7120,8 +7288,7 @@ function rememberAuditMeasure(value) {
|
|
|
7120
7288
|
}
|
|
7121
7289
|
}
|
|
7122
7290
|
async function main() {
|
|
7123
|
-
const args =
|
|
7124
|
-
const command = args[0];
|
|
7291
|
+
const { command, projectRoot, args } = enterProjectRoot();
|
|
7125
7292
|
// `--version` / `-v` / `version` prints the version number, not the help
|
|
7126
7293
|
// banner — so `npx vigiles --version` reports e.g. `3.0.0`.
|
|
7127
7294
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -7156,11 +7323,11 @@ async function main() {
|
|
|
7156
7323
|
const restArgs = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
7157
7324
|
// Shared flags (--max-rules, --catalog-only) override the loaded config so
|
|
7158
7325
|
// every GitHub Action input maps to a real CLI flag. See src/cli-flags.ts.
|
|
7159
|
-
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);
|
|
7160
7327
|
// `.vigilesrc.json#exclude`, parsed ONCE here and threaded to every walk that
|
|
7161
7328
|
// polices the repo (src/exclude.ts). Built where the config is loaded so a
|
|
7162
7329
|
// command cannot re-derive it differently — the drift #192 measured.
|
|
7163
|
-
const excludes = (0, exclude_js_1.excludeSet)(
|
|
7330
|
+
const excludes = (0, exclude_js_1.excludeSet)(projectRoot, config.exclude);
|
|
7164
7331
|
switch (command) {
|
|
7165
7332
|
// --- Primary commands ---
|
|
7166
7333
|
case "init": {
|
|
@@ -7193,7 +7360,7 @@ async function main() {
|
|
|
7193
7360
|
// is refused below, out loud, with the command that fixes it.
|
|
7194
7361
|
const found = restArgs.length > 0
|
|
7195
7362
|
? (0, hook_install_js_1.partitionHookArgs)(restArgs.filter((f) => !f.endsWith(".spec.ts")))
|
|
7196
|
-
: (0, hook_install_js_1.discoveredHookArgs)(
|
|
7363
|
+
: (0, hook_install_js_1.discoveredHookArgs)(projectRoot);
|
|
7197
7364
|
const hooks = [...found.hooks];
|
|
7198
7365
|
const unclaimed = found.unclaimed;
|
|
7199
7366
|
if (specs.length === 0 &&
|
|
@@ -7228,8 +7395,8 @@ async function main() {
|
|
|
7228
7395
|
// committed baseline — shown at the moment of change. Report-only:
|
|
7229
7396
|
// `lint` is the gate (rule `instruction-weight`).
|
|
7230
7397
|
if (specs.length > 0) {
|
|
7231
|
-
const weightLine = (0, instruction_weight_ratchet_js_1.compileWeightLine)(
|
|
7232
|
-
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,
|
|
7233
7400
|
flag: harnessFlag,
|
|
7234
7401
|
configHarness: (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses),
|
|
7235
7402
|
}).adapter));
|
|
@@ -7432,7 +7599,7 @@ async function main() {
|
|
|
7432
7599
|
}
|
|
7433
7600
|
// The same single owner of "relative to which directory" `lint` uses
|
|
7434
7601
|
// (#281): cwd, unless the audited dir is somebody else's repository.
|
|
7435
|
-
const frame = (0, frame_js_1.frameFor)(
|
|
7602
|
+
const frame = (0, frame_js_1.frameFor)(projectRoot, root);
|
|
7436
7603
|
const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
|
|
7437
7604
|
deprecatedKeyFiles: specsUsingKeyFiles(root, excludes.globIgnore),
|
|
7438
7605
|
sharedDirs: config.sharedDirs,
|
|
@@ -7478,9 +7645,11 @@ async function main() {
|
|
|
7478
7645
|
// --target=skills/x/SKILL.md` named a file that does not exist from where
|
|
7479
7646
|
// you run it, and offered to adopt a skill `exclude` had removed.
|
|
7480
7647
|
const auditedBundle = frame.bundle(root);
|
|
7481
|
-
const adoptableSurfaces = discoverAdoptableForAudit(root,
|
|
7482
|
-
|
|
7483
|
-
|
|
7648
|
+
const adoptableSurfaces = discoverAdoptableForAudit(root, {
|
|
7649
|
+
instructionFile: adapter.layout.instructionFile,
|
|
7650
|
+
excludes,
|
|
7651
|
+
config,
|
|
7652
|
+
}).map((p) => auditedBundle.scanned(p));
|
|
7484
7653
|
// Read the local flight recorder ONCE — feeds both the JSON report
|
|
7485
7654
|
// (structured summary, the product boundary) and the terminal render.
|
|
7486
7655
|
const ledgerRecords = (0, observe_js_1.readObservations)(root);
|
|
@@ -7488,7 +7657,7 @@ async function main() {
|
|
|
7488
7657
|
// report is built (it's a pure read off the scan) because the report's
|
|
7489
7658
|
// `Evaluated` ring needs to know whether this run will ever ask the
|
|
7490
7659
|
// skill-firing question — see `firingMeasured` below.
|
|
7491
|
-
const isForeign = root !==
|
|
7660
|
+
const isForeign = root !== projectRoot;
|
|
7492
7661
|
// ONE read of "can we reach a model, and on whose bill", from the driving
|
|
7493
7662
|
// adapter. It replaces a name check OR-ed with one harness's env vars at
|
|
7494
7663
|
// four sites below, and the two hard-coded "authenticate the `claude` CLI"
|
|
@@ -7758,8 +7927,8 @@ async function main() {
|
|
|
7758
7927
|
// CI upload / a custom location). The dir is created if missing.
|
|
7759
7928
|
const outFlag = args.find((a) => a.startsWith("--out="));
|
|
7760
7929
|
const outDir = outFlag
|
|
7761
|
-
? (0, node_path_1.resolve)(
|
|
7762
|
-
:
|
|
7930
|
+
? (0, node_path_1.resolve)(projectRoot, outFlag.slice("--out=".length))
|
|
7931
|
+
: projectRoot;
|
|
7763
7932
|
const openReport = !args.includes("--no-open");
|
|
7764
7933
|
const wroteReports = [];
|
|
7765
7934
|
// The versioned JSON artifact — the upload/CI boundary (a hosted dashboard
|
|
@@ -7805,14 +7974,14 @@ async function main() {
|
|
|
7805
7974
|
// `--out` pointing elsewhere yields nothing to write rather than a
|
|
7806
7975
|
// dead `../../..` entry.
|
|
7807
7976
|
const rel = wroteReports
|
|
7808
|
-
.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), {
|
|
7809
7978
|
relative: node_path_1.relative,
|
|
7810
7979
|
resolve: node_path_1.resolve,
|
|
7811
7980
|
isAbsolute: node_path_1.isAbsolute,
|
|
7812
7981
|
sep: node_path_1.sep,
|
|
7813
7982
|
}))
|
|
7814
7983
|
.filter((p) => p !== null);
|
|
7815
|
-
ensureReportGitignored(
|
|
7984
|
+
ensureReportGitignored(projectRoot, rel);
|
|
7816
7985
|
}
|
|
7817
7986
|
// A shareable deep-link for a public GitHub repo: the in-browser demo
|
|
7818
7987
|
// re-runs the audit LIVE for whoever opens it, so a local result is
|