vigiles 31.0.0 → 32.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/run-scripts.d.ts +5 -3
- package/dist/adapters/claude-code/run-scripts.js +5 -3
- package/dist/cli-main.d.ts +8 -0
- package/dist/cli-main.js +297 -192
- package/dist/core/coverage.d.ts +4 -3
- package/dist/core/coverage.js +4 -3
- package/dist/core/doc-refs.d.ts +3 -1
- package/dist/core/doc-refs.js +2 -1
- package/dist/core/frame.d.ts +90 -0
- package/dist/core/frame.js +58 -0
- package/dist/core/glob-ignore.d.ts +28 -0
- package/dist/core/glob-ignore.js +42 -0
- package/dist/core/orphans.d.ts +5 -3
- package/dist/core/orphans.js +5 -4
- package/dist/eval.d.ts +50 -11
- package/dist/eval.js +371 -152
- package/dist/exclude.d.ts +3 -5
- package/dist/exclude.js +3 -3
- package/dist/scan.d.ts +3 -3
- package/dist/scan.js +8 -7
- package/dist/test-coverage.d.ts +20 -3
- package/dist/test-coverage.js +40 -14
- package/package.json +3 -2
package/dist/cli-main.js
CHANGED
|
@@ -34,6 +34,7 @@ const setup_plan_js_1 = require("./setup-plan.js");
|
|
|
34
34
|
const types_js_1 = require("./core/types.js");
|
|
35
35
|
const test_coverage_js_1 = require("./test-coverage.js");
|
|
36
36
|
const scan_js_1 = require("./scan.js");
|
|
37
|
+
const frame_js_1 = require("./core/frame.js");
|
|
37
38
|
const surface_discovery_js_1 = require("./core/surface-discovery.js");
|
|
38
39
|
const scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
|
|
39
40
|
const install_reader_js_1 = require("./core/install-reader.js");
|
|
@@ -630,6 +631,13 @@ function isGitHubActions() {
|
|
|
630
631
|
/**
|
|
631
632
|
* Emit a GitHub Actions annotation for the inline PR experience.
|
|
632
633
|
* No-op outside GitHub Actions.
|
|
634
|
+
*
|
|
635
|
+
* `file` is a {@link RepoPath}, not a `string`, and that is the #281 fix made
|
|
636
|
+
* structural: GitHub resolves `file=` against the checked-out repository, so a
|
|
637
|
+
* path relative to a nested bundle (`skills/x/SKILL.md` for
|
|
638
|
+
* `plugins/p/skills/x/SKILL.md`) or an absolute machine path lands on a missing
|
|
639
|
+
* file — or, worse, on the ROOT bundle's file of the same name. Such a path no
|
|
640
|
+
* longer compiles here; it has to go through `core/frame.ts` first.
|
|
633
641
|
*/
|
|
634
642
|
function ghAnnotate(level, message, file, line) {
|
|
635
643
|
if (!isGitHubActions())
|
|
@@ -642,7 +650,7 @@ function ghAnnotate(level, message, file, line) {
|
|
|
642
650
|
const loc = locParts.length > 0 ? " " + locParts.join(",") : "";
|
|
643
651
|
console.log(`::${level}${loc}::${message}`);
|
|
644
652
|
}
|
|
645
|
-
function verifyHashes(filePaths, silent = false) {
|
|
653
|
+
function verifyHashes(frame, filePaths, silent = false) {
|
|
646
654
|
let errorCount = 0;
|
|
647
655
|
const log = (msg) => {
|
|
648
656
|
if (!silent)
|
|
@@ -656,7 +664,7 @@ function verifyHashes(filePaths, silent = false) {
|
|
|
656
664
|
if (!(0, node_fs_1.existsSync)(fullPath)) {
|
|
657
665
|
log(`\n✗ ${filePath} — file not found`);
|
|
658
666
|
if (!silent) {
|
|
659
|
-
ghAnnotate("error", `File not found: ${filePath}`,
|
|
667
|
+
ghAnnotate("error", `File not found: ${filePath}`, frame.repo(fullPath));
|
|
660
668
|
}
|
|
661
669
|
errorCount++;
|
|
662
670
|
continue;
|
|
@@ -673,7 +681,7 @@ function verifyHashes(filePaths, silent = false) {
|
|
|
673
681
|
log(`\n✗ ${filePath} — hash mismatch (manually edited after compilation)`);
|
|
674
682
|
log(` Re-run \`vigiles compile\` to regenerate from ${result.specFile ?? "spec"}.`);
|
|
675
683
|
if (!silent) {
|
|
676
|
-
ghAnnotate("error", "Hash mismatch — file was manually edited after compilation",
|
|
684
|
+
ghAnnotate("error", "Hash mismatch — file was manually edited after compilation", frame.repo(fullPath));
|
|
677
685
|
}
|
|
678
686
|
errorCount++;
|
|
679
687
|
}
|
|
@@ -720,8 +728,8 @@ function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
|
720
728
|
}
|
|
721
729
|
return allValid;
|
|
722
730
|
}
|
|
723
|
-
function check(filePaths, silent = false) {
|
|
724
|
-
const hashes = verifyHashes(filePaths, silent);
|
|
731
|
+
function check(frame, filePaths, silent = false) {
|
|
732
|
+
const hashes = verifyHashes(frame, filePaths, silent);
|
|
725
733
|
const vConfig = (0, validate_js_1.loadConfig)();
|
|
726
734
|
const specsValid = validateSpecs(filePaths, vConfig.rules, silent);
|
|
727
735
|
return {
|
|
@@ -821,7 +829,7 @@ async function findDuplicateRules(excludes, threshold = 0.3, silent = false, sco
|
|
|
821
829
|
* Each named file is parsed on demand; there is no project-wide index. Returns
|
|
822
830
|
* the count of broken references.
|
|
823
831
|
*/
|
|
824
|
-
async function verifyMarkdownSymbols(files, silent) {
|
|
832
|
+
async function verifyMarkdownSymbols(frame, files, silent) {
|
|
825
833
|
if (files.length === 0)
|
|
826
834
|
return 0;
|
|
827
835
|
const cwd = process.cwd();
|
|
@@ -845,7 +853,7 @@ async function verifyMarkdownSymbols(files, silent) {
|
|
|
845
853
|
}
|
|
846
854
|
for (const b of broken) {
|
|
847
855
|
console.log(` ✗ ${f}:${String(b.line)} ${b.reason}`);
|
|
848
|
-
ghAnnotate("error", b.reason, f, b.line);
|
|
856
|
+
ghAnnotate("error", b.reason, frame.repo((0, node_path_1.resolve)(cwd, f)), b.line);
|
|
849
857
|
}
|
|
850
858
|
}
|
|
851
859
|
errors += broken.length;
|
|
@@ -859,7 +867,7 @@ async function verifyMarkdownSymbols(files, silent) {
|
|
|
859
867
|
* only started if a mark actually references it. Returns the count of broken
|
|
860
868
|
* references. Async because it speaks to real servers.
|
|
861
869
|
*/
|
|
862
|
-
async function verifyMarkdownMcpRefs(files, silent) {
|
|
870
|
+
async function verifyMarkdownMcpRefs(frame, files, silent) {
|
|
863
871
|
const cwd = process.cwd();
|
|
864
872
|
const servers = (0, mcp_js_1.loadMcpServers)(cwd);
|
|
865
873
|
if (files.length === 0 || Object.keys(servers).length === 0)
|
|
@@ -885,7 +893,7 @@ async function verifyMarkdownMcpRefs(files, silent) {
|
|
|
885
893
|
for (const b of broken) {
|
|
886
894
|
const msg = (0, mcp_js_1.mcpRefMessage)(b);
|
|
887
895
|
console.log(` ✗ ${f}:${String(b.line)} ${msg}`);
|
|
888
|
-
ghAnnotate("error", msg, f, b.line);
|
|
896
|
+
ghAnnotate("error", msg, frame.repo((0, node_path_1.resolve)(cwd, f)), b.line);
|
|
889
897
|
}
|
|
890
898
|
}
|
|
891
899
|
errors += broken.length;
|
|
@@ -993,7 +1001,7 @@ function lintExitCode(report) {
|
|
|
993
1001
|
* Verify one parsed enforce rule against the linter catalog/config, logging
|
|
994
1002
|
* and annotating on failure. Returns true when the rule is valid+enabled.
|
|
995
1003
|
*/
|
|
996
|
-
function verifyOneRule(rule, filePath, silent, linterOptions) {
|
|
1004
|
+
function verifyOneRule(frame, rule, filePath, silent, linterOptions) {
|
|
997
1005
|
const log = (msg) => {
|
|
998
1006
|
if (!silent)
|
|
999
1007
|
console.log(msg);
|
|
@@ -1003,14 +1011,14 @@ function verifyOneRule(rule, filePath, silent, linterOptions) {
|
|
|
1003
1011
|
const message = result.error ?? `Rule "${rule.linterRule}" not found`;
|
|
1004
1012
|
log(` ✗ line ${String(rule.line)}: ${message}`);
|
|
1005
1013
|
if (!silent)
|
|
1006
|
-
ghAnnotate("error", message, filePath, rule.line);
|
|
1014
|
+
ghAnnotate("error", message, frame.repo((0, node_path_1.resolve)(process.cwd(), filePath)), rule.line);
|
|
1007
1015
|
return false;
|
|
1008
1016
|
}
|
|
1009
1017
|
if (result.enabled === "disabled") {
|
|
1010
1018
|
const message = `Rule "${rule.linterRule}" exists but is disabled in ${result.linter} config`;
|
|
1011
1019
|
log(` ✗ line ${String(rule.line)}: ${message}`);
|
|
1012
1020
|
if (!silent)
|
|
1013
|
-
ghAnnotate("error", message, filePath, rule.line);
|
|
1021
|
+
ghAnnotate("error", message, frame.repo((0, node_path_1.resolve)(process.cwd(), filePath)), rule.line);
|
|
1014
1022
|
return false;
|
|
1015
1023
|
}
|
|
1016
1024
|
log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
|
|
@@ -1023,13 +1031,13 @@ function verifyOneRule(rule, filePath, silent, linterOptions) {
|
|
|
1023
1031
|
* package.json / the filesystem. References resolve relative to the markdown
|
|
1024
1032
|
* file's own directory. Returns the number of stale references found.
|
|
1025
1033
|
*/
|
|
1026
|
-
function verifyMarkdownRefs(files, commands, filePath, silent) {
|
|
1034
|
+
function verifyMarkdownRefs(frame, files, commands, filePath, silent) {
|
|
1027
1035
|
const basePath = (0, node_path_1.dirname)((0, node_path_1.resolve)(process.cwd(), filePath));
|
|
1028
1036
|
let errorCount = 0;
|
|
1029
1037
|
const report = (err, line) => {
|
|
1030
1038
|
if (!silent) {
|
|
1031
1039
|
console.log(` ✗ line ${String(line)}: ${err.message}`);
|
|
1032
|
-
ghAnnotate("error", err.message, filePath, line);
|
|
1040
|
+
ghAnnotate("error", err.message, frame.repo((0, node_path_1.resolve)(process.cwd(), filePath)), line);
|
|
1033
1041
|
}
|
|
1034
1042
|
errorCount++;
|
|
1035
1043
|
};
|
|
@@ -1045,7 +1053,7 @@ function verifyMarkdownRefs(files, commands, filePath, silent) {
|
|
|
1045
1053
|
}
|
|
1046
1054
|
return errorCount;
|
|
1047
1055
|
}
|
|
1048
|
-
function verifyInlineRules(filePath, silent, linterOptions) {
|
|
1056
|
+
function verifyInlineRules(frame, filePath, silent, linterOptions) {
|
|
1049
1057
|
const log = (msg) => {
|
|
1050
1058
|
if (!silent)
|
|
1051
1059
|
console.log(msg);
|
|
@@ -1070,14 +1078,14 @@ function verifyInlineRules(filePath, silent, linterOptions) {
|
|
|
1070
1078
|
log(` ✗ line ${String(err.line)}: ${err.message}`);
|
|
1071
1079
|
errorCount++;
|
|
1072
1080
|
if (!silent) {
|
|
1073
|
-
ghAnnotate("error", err.message, filePath, err.line);
|
|
1081
|
+
ghAnnotate("error", err.message, frame.repo((0, node_path_1.resolve)(process.cwd(), filePath)), err.line);
|
|
1074
1082
|
}
|
|
1075
1083
|
}
|
|
1076
1084
|
for (const rule of rules) {
|
|
1077
|
-
if (!verifyOneRule(rule, filePath, silent, linterOptions))
|
|
1085
|
+
if (!verifyOneRule(frame, rule, filePath, silent, linterOptions))
|
|
1078
1086
|
errorCount++;
|
|
1079
1087
|
}
|
|
1080
|
-
errorCount += verifyMarkdownRefs(files, commands, filePath, silent);
|
|
1088
|
+
errorCount += verifyMarkdownRefs(frame, files, commands, filePath, silent);
|
|
1081
1089
|
return {
|
|
1082
1090
|
ok: errorCount === 0,
|
|
1083
1091
|
errorCount,
|
|
@@ -1091,7 +1099,7 @@ function verifyInlineRules(filePath, silent, linterOptions) {
|
|
|
1091
1099
|
* in `exclude` (e.g. declared inline in the same file) are skipped so a
|
|
1092
1100
|
* rule present in both sources is reported once, not twice.
|
|
1093
1101
|
*/
|
|
1094
|
-
function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
|
|
1102
|
+
function verifyFrontmatterRules(frame, filePath, silent, exclude, linterOptions) {
|
|
1095
1103
|
const log = (msg) => {
|
|
1096
1104
|
if (!silent)
|
|
1097
1105
|
console.log(msg);
|
|
@@ -1117,14 +1125,14 @@ function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
|
|
|
1117
1125
|
log(` ✗ line ${String(err.line)}: ${err.message}`);
|
|
1118
1126
|
errorCount++;
|
|
1119
1127
|
if (!silent) {
|
|
1120
|
-
ghAnnotate("error", err.message, filePath, err.line);
|
|
1128
|
+
ghAnnotate("error", err.message, frame.repo((0, node_path_1.resolve)(process.cwd(), filePath)), err.line);
|
|
1121
1129
|
}
|
|
1122
1130
|
}
|
|
1123
1131
|
for (const rule of rules) {
|
|
1124
|
-
if (!verifyOneRule(rule, filePath, silent, linterOptions))
|
|
1132
|
+
if (!verifyOneRule(frame, rule, filePath, silent, linterOptions))
|
|
1125
1133
|
errorCount++;
|
|
1126
1134
|
}
|
|
1127
|
-
errorCount += verifyMarkdownRefs(files, commands, filePath, silent);
|
|
1135
|
+
errorCount += verifyMarkdownRefs(frame, files, commands, filePath, silent);
|
|
1128
1136
|
return {
|
|
1129
1137
|
ok: errorCount === 0,
|
|
1130
1138
|
errorCount,
|
|
@@ -1162,7 +1170,7 @@ const FRONTMATTER_MODE_ENABLED = false;
|
|
|
1162
1170
|
* declared both inline and in frontmatter is verified once (inline wins as
|
|
1163
1171
|
* the first source). See docs/markdown-mode.md.
|
|
1164
1172
|
*/
|
|
1165
|
-
function verifyMarkdownModeRules(files, silent, config) {
|
|
1173
|
+
function verifyMarkdownModeRules(frame, files, silent, config) {
|
|
1166
1174
|
const totals = {
|
|
1167
1175
|
inlineErrors: 0,
|
|
1168
1176
|
inlineRules: 0,
|
|
@@ -1190,13 +1198,13 @@ function verifyMarkdownModeRules(files, silent, config) {
|
|
|
1190
1198
|
}
|
|
1191
1199
|
if (compiledFromRe.test(content))
|
|
1192
1200
|
continue; // managed via hash header
|
|
1193
|
-
const inline = verifyInlineRules(filePath, silent, linterOptions);
|
|
1201
|
+
const inline = verifyInlineRules(frame, filePath, silent, linterOptions);
|
|
1194
1202
|
totals.inlineErrors += inline.errorCount;
|
|
1195
1203
|
totals.inlineRules += inline.ruleCount;
|
|
1196
1204
|
// Frontmatter mode is DISABLED (kept in code, inert in lint) — a `vigiles:`
|
|
1197
1205
|
// block is ignored, never verified. See FRONTMATTER_MODE_ENABLED.
|
|
1198
1206
|
if (FRONTMATTER_MODE_ENABLED) {
|
|
1199
|
-
const fm = verifyFrontmatterRules(filePath, silent, new Set(inline.ruleNames), linterOptions);
|
|
1207
|
+
const fm = verifyFrontmatterRules(frame, filePath, silent, new Set(inline.ruleNames), linterOptions);
|
|
1200
1208
|
totals.frontmatterErrors += fm.errorCount;
|
|
1201
1209
|
totals.frontmatterRules += fm.ruleCount;
|
|
1202
1210
|
}
|
|
@@ -1217,21 +1225,6 @@ function verifyMarkdownModeRules(files, silent, config) {
|
|
|
1217
1225
|
* --summary Print a single-line summary (for SessionStart hooks)
|
|
1218
1226
|
* --json Print structured JSON report (for CI integration)
|
|
1219
1227
|
*/
|
|
1220
|
-
/**
|
|
1221
|
-
* The repo root that `sharedDirs` resolve against. The caller's cwd (where
|
|
1222
|
-
* `.vigilesrc.json` lives) is used ONLY when the scan target is INSIDE it — e.g.
|
|
1223
|
-
* `lint packages/foo` from the repo root, where the shared tree is an ancestor of
|
|
1224
|
-
* the scoped subdir. When the target is NOT under cwd (`lint path/to/other-repo`),
|
|
1225
|
-
* we resolve against the TARGET itself, so a foreign-repo lint never lets the
|
|
1226
|
-
* caller's own files satisfy the target's bundled resources (scoped-lint integrity).
|
|
1227
|
-
*/
|
|
1228
|
-
function sharedDirsRootFor(scanTarget) {
|
|
1229
|
-
const cwd = process.cwd();
|
|
1230
|
-
const target = (0, node_path_1.resolve)(scanTarget);
|
|
1231
|
-
const rel = (0, node_path_1.relative)(cwd, target);
|
|
1232
|
-
const underCwd = rel === "" || (!rel.startsWith("..") && !(0, node_path_1.isAbsolute)(rel));
|
|
1233
|
-
return underCwd ? cwd : target;
|
|
1234
|
-
}
|
|
1235
1228
|
/**
|
|
1236
1229
|
* Nested plugin bundles under a lint root — a directory that is itself a harness
|
|
1237
1230
|
* (its own `.claude-plugin/plugin.json`, or its own skills dir) and is NOT the
|
|
@@ -1303,21 +1296,39 @@ function discoverNestedBundles(root, excludes) {
|
|
|
1303
1296
|
return out.sort();
|
|
1304
1297
|
}
|
|
1305
1298
|
/**
|
|
1306
|
-
* Run one per-surface check over EVERY
|
|
1299
|
+
* Run one per-surface check over EVERY bundle and sum its counters.
|
|
1307
1300
|
*
|
|
1308
|
-
* The checks all
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1301
|
+
* The checks all take a {@link BundleCheck} and return a small record of
|
|
1302
|
+
* numbers, so one wrapper covers all of them rather than twenty edits — and a
|
|
1303
|
+
* check added later is swept in by using it, not by remembering to. The first
|
|
1304
|
+
* bundle is the lint target; the rest are nested bundles.
|
|
1311
1305
|
*/
|
|
1312
|
-
function overBundles(fn,
|
|
1313
|
-
const
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
const
|
|
1306
|
+
function overBundles(fn, run, bundles) {
|
|
1307
|
+
const many = bundles.length > 1;
|
|
1308
|
+
const ctxFor = (abs, i) => {
|
|
1309
|
+
const bundle = run.frame.bundle(abs);
|
|
1310
|
+
const label = (text) => many && i > 0 ? `[${bundle.at}] ${text}` : text;
|
|
1311
|
+
return {
|
|
1312
|
+
...run,
|
|
1313
|
+
bundle,
|
|
1314
|
+
label,
|
|
1315
|
+
annotate: (level, message, file) => {
|
|
1316
|
+
ghAnnotate(level, file === undefined ? label(message) : message, file);
|
|
1317
|
+
},
|
|
1318
|
+
scan: (extra = {}) => (0, scan_js_1.scanPlugin)(bundle.abs, run.adapter.layout, run.adapter.dialect, {
|
|
1319
|
+
...extra,
|
|
1320
|
+
excludes: run.excludes,
|
|
1321
|
+
}),
|
|
1322
|
+
};
|
|
1323
|
+
};
|
|
1324
|
+
const [first, ...rest] = bundles;
|
|
1325
|
+
const total = { ...fn(ctxFor(first, 0)) };
|
|
1326
|
+
rest.forEach((abs, i) => {
|
|
1327
|
+
const next = fn(ctxFor(abs, i + 1));
|
|
1317
1328
|
for (const key of Object.keys(next))
|
|
1318
1329
|
total[key] =
|
|
1319
1330
|
(total[key] ?? 0) + (next[key] ?? 0);
|
|
1320
|
-
}
|
|
1331
|
+
});
|
|
1321
1332
|
return total;
|
|
1322
1333
|
}
|
|
1323
1334
|
/**
|
|
@@ -1485,6 +1496,11 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1485
1496
|
const nestedBundles = discoverNestedBundles(scanRoot, excludes);
|
|
1486
1497
|
const scoreAll = flags.includes("--bundles=all") || config?.bundles === "all";
|
|
1487
1498
|
const lintRoots = scoreAll ? [scanRoot, ...nestedBundles] : [scanRoot];
|
|
1499
|
+
// ONE frame for the whole run (#281): every path a check prints or annotates is
|
|
1500
|
+
// relative to it, and config globs resolve from it — whichever bundle is being
|
|
1501
|
+
// scored. Built here, once, instead of each option re-deriving its own root.
|
|
1502
|
+
const frame = (0, frame_js_1.frameFor)(process.cwd(), scanRoot);
|
|
1503
|
+
const run = { config, silent, adapter, excludes, frame };
|
|
1488
1504
|
if (!silent && nestedBundles.length > 0) {
|
|
1489
1505
|
const rel = nestedBundles.map((b) => (0, node_path_1.relative)(scanRoot, b) || b);
|
|
1490
1506
|
console.log(scoreAll
|
|
@@ -1510,11 +1526,11 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1510
1526
|
}
|
|
1511
1527
|
}
|
|
1512
1528
|
const hashResult = files.length > 0
|
|
1513
|
-
? check(files, silent)
|
|
1529
|
+
? check(frame, files, silent)
|
|
1514
1530
|
: { valid: true, hashErrors: 0, validationErrors: 0 };
|
|
1515
1531
|
// 1b. Verify inline + frontmatter rules in instruction files not managed
|
|
1516
1532
|
// by a spec. See verifyMarkdownModeRules / docs/markdown-mode.md.
|
|
1517
|
-
const md = verifyMarkdownModeRules(files, silent, config);
|
|
1533
|
+
const md = verifyMarkdownModeRules(frame, files, silent, config);
|
|
1518
1534
|
const { inlineErrors, inlineRules, frontmatterErrors, frontmatterRules } = md;
|
|
1519
1535
|
// 2. Coverage gaps (discover)
|
|
1520
1536
|
if (!silent)
|
|
@@ -1570,7 +1586,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1570
1586
|
// The repo-wide `exclude` is the FLOOR under the rule's own `exclude`
|
|
1571
1587
|
// (union, never override): an excluded corpus is neither an orphan
|
|
1572
1588
|
// candidate nor a source of references that keep a doc alive (#192).
|
|
1573
|
-
repoExclude: excludes.
|
|
1589
|
+
repoExclude: excludes.globIgnore,
|
|
1574
1590
|
// Exempt every registered harness's surface files (instruction file,
|
|
1575
1591
|
// SKILL.md, subagents, commands) as orphan candidates — layout-driven so
|
|
1576
1592
|
// core carries no harness literal (see src/core/orphans.ts).
|
|
@@ -1591,74 +1607,74 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1591
1607
|
// A compiled artifact's refs, re-derived from its spec — the hash says the file
|
|
1592
1608
|
// is unchanged, not that what it names still exists (#173).
|
|
1593
1609
|
const specRefs = await checkSpecRefs(excludes, config, silent, adapter.dialect);
|
|
1594
|
-
const untested = overBundles(
|
|
1610
|
+
const untested = overBundles(checkUntestedSurfaces, run, lintRoots);
|
|
1595
1611
|
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
1596
1612
|
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
1597
1613
|
// subagents. Off by default unless a severity is configured; warning surfaces
|
|
1598
1614
|
// a typo/never-available tool, error gates CI.
|
|
1599
|
-
const toolContract = overBundles(checkSubagentToolContracts,
|
|
1615
|
+
const toolContract = overBundles(checkSubagentToolContracts, run, lintRoots);
|
|
1600
1616
|
// 7d. Hook-event check — a hook registered under an event the harness doesn't
|
|
1601
1617
|
// define never fires. High-precision (close typos only). Off unless configured.
|
|
1602
|
-
const hookEvents = overBundles(checkHookEvents,
|
|
1618
|
+
const hookEvents = overBundles(checkHookEvents, run, lintRoots);
|
|
1603
1619
|
// 7e. Subagent-frontmatter check — a subagent missing required frontmatter
|
|
1604
1620
|
// (name + description) won't register. n/a on a harness with no subagents.
|
|
1605
|
-
const frontmatter = overBundles(checkFrontmatterSchema,
|
|
1621
|
+
const frontmatter = overBundles(checkFrontmatterSchema, run, lintRoots);
|
|
1606
1622
|
// 7f. MCP-config check — a declared MCP server with no command/url can't start.
|
|
1607
|
-
const mcpConfig = overBundles(checkMcpConfig,
|
|
1623
|
+
const mcpConfig = overBundles(checkMcpConfig, run, lintRoots);
|
|
1608
1624
|
// 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
|
|
1609
1625
|
// reliable trigger surface). Best-practice nudge; skills load without it.
|
|
1610
|
-
const skillFm = overBundles(checkSkillFrontmatter,
|
|
1626
|
+
const skillFm = overBundles(checkSkillFrontmatter, run, lintRoots);
|
|
1611
1627
|
// 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
|
|
1612
1628
|
// the plugin doesn't declare can't resolve (the MCP half of the tool moat).
|
|
1613
|
-
const mcpToolResolves = overBundles(checkMcpToolResolves,
|
|
1629
|
+
const mcpToolResolves = overBundles(checkMcpToolResolves, run, lintRoots);
|
|
1614
1630
|
// 7i. Hook-script existence — a hook command referencing a missing script file
|
|
1615
1631
|
// never runs. This comment used to add "matches Anthropic's own `claude plugin
|
|
1616
1632
|
// validate`"; measured false on 2026-09-08 (Claude Code 2.1.263) — see
|
|
1617
1633
|
// docs/rules/hook-script-exists.md and tools/measure-validate-overlap.mjs.
|
|
1618
|
-
const hookScripts = overBundles(checkHookScriptExists,
|
|
1634
|
+
const hookScripts = overBundles(checkHookScriptExists, run, lintRoots);
|
|
1619
1635
|
// 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
|
|
1620
1636
|
// (the deny-side mirror of subagent-tool-contract; close-typo only).
|
|
1621
|
-
const disallowedTools = overBundles(checkDisallowedTools,
|
|
1637
|
+
const disallowedTools = overBundles(checkDisallowedTools, run, lintRoots);
|
|
1622
1638
|
// 7k. Description-overlap — two model-invocable skills with near-identical
|
|
1623
1639
|
// descriptions collide in the selector (deterministic NCD precision proxy).
|
|
1624
|
-
const descriptionOverlap = overBundles(checkDescriptionOverlap,
|
|
1640
|
+
const descriptionOverlap = overBundles(checkDescriptionOverlap, run, lintRoots);
|
|
1625
1641
|
// 7k². Skill-description-budget — a model-invocable skill whose description is
|
|
1626
1642
|
// so long the trigger signal is buried (heuristic proxy; degrades recall +
|
|
1627
1643
|
// precision). Generous 500-char budget; warn-tier, never gates.
|
|
1628
|
-
const descriptionBudget = overBundles(checkDescriptionBudget,
|
|
1644
|
+
const descriptionBudget = overBundles(checkDescriptionBudget, run, lintRoots);
|
|
1629
1645
|
// 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
|
|
1630
1646
|
// stricter than some loaders, so verify before enforcing).
|
|
1631
|
-
const frontmatterValid = overBundles(checkFrontmatterValid,
|
|
1647
|
+
const frontmatterValid = overBundles(checkFrontmatterValid, run, lintRoots);
|
|
1632
1648
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
1633
1649
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
1634
|
-
const mcpHookTargets = overBundles(checkMcpHookTargets,
|
|
1650
|
+
const mcpHookTargets = overBundles(checkMcpHookTargets, run, lintRoots);
|
|
1635
1651
|
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
1636
1652
|
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
1637
|
-
const preferCompiledHooks = overBundles(checkPreferCompiledHooks,
|
|
1653
|
+
const preferCompiledHooks = overBundles(checkPreferCompiledHooks, run, lintRoots);
|
|
1638
1654
|
// 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
|
|
1639
1655
|
// hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
|
|
1640
1656
|
// prompt-injection exfil path (Rule of Two). Capability SET-intersection.
|
|
1641
|
-
const lethalTrifecta = overBundles(checkLethalTrifecta,
|
|
1657
|
+
const lethalTrifecta = overBundles(checkLethalTrifecta, run, lintRoots);
|
|
1642
1658
|
// 7p. Skill-resource — a SKILL.md body referencing a bundled file that doesn't
|
|
1643
1659
|
// exist on disk under the skill dir (the agent gets nothing). FP-safe.
|
|
1644
|
-
const skillResources = overBundles(checkSkillResourceResolves,
|
|
1660
|
+
const skillResources = overBundles(checkSkillResourceResolves, run, lintRoots);
|
|
1645
1661
|
// 7q. Skill-missing-fence — a SKILL.md opening with `name:`/`description:` but no
|
|
1646
1662
|
// `---` fence loads as plain body (invisible — no name/description/trigger).
|
|
1647
|
-
const skillFence = overBundles(checkSkillMissingFence,
|
|
1663
|
+
const skillFence = overBundles(checkSkillMissingFence, run, lintRoots);
|
|
1648
1664
|
// 7r. Plugin-dir-layout — functional surface dirs (skills/agents/commands) nested
|
|
1649
1665
|
// inside the `.claude-plugin/` manifest dir where the harness can't see them.
|
|
1650
|
-
const pluginLayout = overBundles(checkPluginDirLayout,
|
|
1666
|
+
const pluginLayout = overBundles(checkPluginDirLayout, run, lintRoots);
|
|
1651
1667
|
// 7s. Delegation-trifecta — a lethal trifecta that emerges across a delegation
|
|
1652
1668
|
// edge (a subagent's own ∪ delegated-to capability) though no single unit trips it.
|
|
1653
|
-
const delegationTrifecta = overBundles(checkDelegationTrifecta,
|
|
1669
|
+
const delegationTrifecta = overBundles(checkDelegationTrifecta, run, lintRoots);
|
|
1654
1670
|
// 7t. Hook-block-ineffective — a hook that looks like it blocks but silently
|
|
1655
1671
|
// doesn't (block decision on a non-blocking event, or the legacy `decision`
|
|
1656
1672
|
// field on a permission-gated event). The #1 verified hook pain (#19009).
|
|
1657
|
-
const hookBlock = overBundles(checkHookBlockIneffective,
|
|
1673
|
+
const hookBlock = overBundles(checkHookBlockIneffective, run, lintRoots);
|
|
1658
1674
|
// 7u. Hook-matcher — a hook `matcher` that doesn't fire as written (tool-name
|
|
1659
1675
|
// typo, an uncompilable or unreachable MCP pattern, one too narrow for real
|
|
1660
1676
|
// server naming, or an undeclared MCP server).
|
|
1661
|
-
const hookMatcher = overBundles(checkHookMatcher,
|
|
1677
|
+
const hookMatcher = overBundles(checkHookMatcher, run, lintRoots);
|
|
1662
1678
|
// 8. Validate vigiles builder calls inside markdown code blocks — the
|
|
1663
1679
|
// `doc-refs` rule, DEFAULT OFF. Illustrative blocks opt out via
|
|
1664
1680
|
// `<!-- vigiles:ignore -->` (single block) or `<!-- vigiles:ignore-file -->`
|
|
@@ -1696,7 +1712,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1696
1712
|
// whose exit code is discarded gates nothing — after which hand-written CI steps
|
|
1697
1713
|
// grew to do the gating instead. One unpassed argument, that whole chain.
|
|
1698
1714
|
const docRefReport = docRefSeverity
|
|
1699
|
-
? (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd(), ignore: excludes.
|
|
1715
|
+
? (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd(), ignore: excludes.globIgnore })
|
|
1700
1716
|
: {
|
|
1701
1717
|
filesScanned: 0,
|
|
1702
1718
|
filesIgnored: 0,
|
|
@@ -1719,14 +1735,14 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1719
1735
|
// on the PR. CI-only (isGitHubActions); skipped under --json/--summary.
|
|
1720
1736
|
if (isGitHubActions() && !silent && docRefSeverity) {
|
|
1721
1737
|
for (const e of docRefReport.errors) {
|
|
1722
|
-
ghAnnotate(docRefSeverity === "error" ? "error" : "warning", `${e.kind}("${e.value}") — ${e.message}`, e.file, e.line);
|
|
1738
|
+
ghAnnotate(docRefSeverity === "error" ? "error" : "warning", `${e.kind}("${e.value}") — ${e.message}`, frame.repo((0, node_path_1.resolve)(process.cwd(), e.file)), e.line);
|
|
1723
1739
|
}
|
|
1724
1740
|
}
|
|
1725
1741
|
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
1726
|
-
const symbolRefErrors = await verifyMarkdownSymbols(files, silent);
|
|
1742
|
+
const symbolRefErrors = await verifyMarkdownSymbols(frame, files, silent);
|
|
1727
1743
|
// 10. Verify `vigiles:mcp server#tool` marks against live MCP servers
|
|
1728
1744
|
// (only when a .mcp.json declares them). See src/mcp.ts.
|
|
1729
|
-
const mcpRefErrors = await verifyMarkdownMcpRefs(files, silent);
|
|
1745
|
+
const mcpRefErrors = await verifyMarkdownMcpRefs(frame, files, silent);
|
|
1730
1746
|
const report = {
|
|
1731
1747
|
hashErrors: hashResult.hashErrors,
|
|
1732
1748
|
validationErrors: hashResult.validationErrors,
|
|
@@ -3605,18 +3621,22 @@ function untestedRules(config) {
|
|
|
3605
3621
|
* "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
|
|
3606
3622
|
* untested count plus the severity-gated error count.
|
|
3607
3623
|
*/
|
|
3608
|
-
function checkUntestedSurfaces(
|
|
3624
|
+
function checkUntestedSurfaces(ctx) {
|
|
3625
|
+
const { config, silent, adapter, frame, bundle } = ctx;
|
|
3609
3626
|
const { severity: sevFor, anyEnabled, options } = untestedRules(config);
|
|
3610
3627
|
if (!anyEnabled)
|
|
3611
3628
|
return { untested: 0, errors: 0 };
|
|
3612
3629
|
const report = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
3613
3630
|
...options,
|
|
3614
|
-
basePath:
|
|
3631
|
+
basePath: bundle.abs,
|
|
3632
|
+
// #281: `include`/`exclude` and every reported path are relative to the
|
|
3633
|
+
// frame root (where `.vigilesrc.json` lives), not to the bundle being scored.
|
|
3634
|
+
root: frame.root,
|
|
3615
3635
|
layout: adapter.layout,
|
|
3616
3636
|
// The rule's own `exclude` NARROWS; the repo-wide one is the floor under
|
|
3617
3637
|
// it. Union, never override — a rule option must not re-admit a vendored
|
|
3618
|
-
// corpus the repo excluded (#192).
|
|
3619
|
-
|
|
3638
|
+
// corpus the repo excluded (#192). Passed WHOLE so it carries its own root.
|
|
3639
|
+
excludes: ctx.excludes,
|
|
3620
3640
|
});
|
|
3621
3641
|
if (!silent) {
|
|
3622
3642
|
console.log("\nUntested surfaces:\n");
|
|
@@ -3624,7 +3644,7 @@ function checkUntestedSurfaces(excludes, config, silent, adapter, scanRoot) {
|
|
|
3624
3644
|
console.log(` ${line}`);
|
|
3625
3645
|
}
|
|
3626
3646
|
for (const s of report.untested) {
|
|
3627
|
-
|
|
3647
|
+
ctx.annotate(sevFor(s.kind) === "error" ? "error" : "warning", `${s.kind} ${s.path} ships without a test or eval`, frame.repo((0, node_path_1.join)(frame.root, s.path)));
|
|
3628
3648
|
}
|
|
3629
3649
|
}
|
|
3630
3650
|
return {
|
|
@@ -3653,7 +3673,8 @@ function reportNotApplicable(check, surface, adapter, silent) {
|
|
|
3653
3673
|
* (plugin/MCP-provided) is never a false alarm. Warning by default; set
|
|
3654
3674
|
* `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
|
|
3655
3675
|
*/
|
|
3656
|
-
function checkSubagentToolContracts(
|
|
3676
|
+
function checkSubagentToolContracts(ctx) {
|
|
3677
|
+
const { config, silent, adapter } = ctx;
|
|
3657
3678
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
|
|
3658
3679
|
if (!sev)
|
|
3659
3680
|
return { issues: 0, errors: 0 };
|
|
@@ -3666,7 +3687,7 @@ function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
|
|
|
3666
3687
|
// `agents/` path, so a harness with a different subagent dir Just Works.
|
|
3667
3688
|
let agents;
|
|
3668
3689
|
try {
|
|
3669
|
-
agents =
|
|
3690
|
+
agents = ctx.scan().agents;
|
|
3670
3691
|
}
|
|
3671
3692
|
catch {
|
|
3672
3693
|
return { issues: 0, errors: 0 };
|
|
@@ -3683,8 +3704,8 @@ function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
|
|
|
3683
3704
|
printedHeader = true;
|
|
3684
3705
|
}
|
|
3685
3706
|
for (const issue of agent.toolIssues) {
|
|
3686
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${agent.path}: ${issue.message}`);
|
|
3687
|
-
|
|
3707
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(agent.path)}: ${issue.message}`);
|
|
3708
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(agent.path));
|
|
3688
3709
|
}
|
|
3689
3710
|
}
|
|
3690
3711
|
}
|
|
@@ -3696,7 +3717,8 @@ function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
|
|
|
3696
3717
|
* `hookEventIssues` (the shared detector, high-precision: close typos only, never
|
|
3697
3718
|
* a framework/custom event). Warning by default; "error" gates CI.
|
|
3698
3719
|
*/
|
|
3699
|
-
function checkHookEvents(
|
|
3720
|
+
function checkHookEvents(ctx) {
|
|
3721
|
+
const { config, silent, adapter } = ctx;
|
|
3700
3722
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
|
|
3701
3723
|
if (!sev)
|
|
3702
3724
|
return { issues: 0, errors: 0 };
|
|
@@ -3706,7 +3728,7 @@ function checkHookEvents(config, silent, adapter, scanRoot) {
|
|
|
3706
3728
|
}
|
|
3707
3729
|
let found;
|
|
3708
3730
|
try {
|
|
3709
|
-
found =
|
|
3731
|
+
found = ctx.scan().hookEventIssues;
|
|
3710
3732
|
}
|
|
3711
3733
|
catch {
|
|
3712
3734
|
return { issues: 0, errors: 0 };
|
|
@@ -3714,8 +3736,8 @@ function checkHookEvents(config, silent, adapter, scanRoot) {
|
|
|
3714
3736
|
if (found.length > 0 && !silent) {
|
|
3715
3737
|
console.log("\nHook-event check:\n");
|
|
3716
3738
|
for (const issue of found) {
|
|
3717
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3718
|
-
|
|
3739
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3740
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
3719
3741
|
}
|
|
3720
3742
|
}
|
|
3721
3743
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3727,7 +3749,8 @@ function checkHookEvents(config, silent, adapter, scanRoot) {
|
|
|
3727
3749
|
* of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
|
|
3728
3750
|
* `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
|
|
3729
3751
|
*/
|
|
3730
|
-
function checkFrontmatterSchema(
|
|
3752
|
+
function checkFrontmatterSchema(ctx) {
|
|
3753
|
+
const { config, silent, adapter } = ctx;
|
|
3731
3754
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
|
|
3732
3755
|
if (!sev)
|
|
3733
3756
|
return { issues: 0, errors: 0 };
|
|
@@ -3737,7 +3760,7 @@ function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
|
|
|
3737
3760
|
}
|
|
3738
3761
|
let found;
|
|
3739
3762
|
try {
|
|
3740
|
-
const r =
|
|
3763
|
+
const r = ctx.scan();
|
|
3741
3764
|
found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
|
|
3742
3765
|
}
|
|
3743
3766
|
catch {
|
|
@@ -3746,8 +3769,8 @@ function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
|
|
|
3746
3769
|
if (found.length > 0 && !silent) {
|
|
3747
3770
|
console.log("\nFrontmatter-schema check:\n");
|
|
3748
3771
|
for (const issue of found) {
|
|
3749
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3750
|
-
|
|
3772
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3773
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(issue.path));
|
|
3751
3774
|
}
|
|
3752
3775
|
}
|
|
3753
3776
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3760,13 +3783,14 @@ function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
|
|
|
3760
3783
|
* default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
|
|
3761
3784
|
* `skillMetaIssues`.
|
|
3762
3785
|
*/
|
|
3763
|
-
function checkSkillFrontmatter(
|
|
3786
|
+
function checkSkillFrontmatter(ctx) {
|
|
3787
|
+
const { config, silent } = ctx;
|
|
3764
3788
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
|
|
3765
3789
|
if (!sev)
|
|
3766
3790
|
return { issues: 0, errors: 0 };
|
|
3767
3791
|
let found;
|
|
3768
3792
|
try {
|
|
3769
|
-
found =
|
|
3793
|
+
found = ctx.scan().skillMetaIssues;
|
|
3770
3794
|
}
|
|
3771
3795
|
catch {
|
|
3772
3796
|
return { issues: 0, errors: 0 };
|
|
@@ -3774,8 +3798,8 @@ function checkSkillFrontmatter(config, silent, adapter, scanRoot) {
|
|
|
3774
3798
|
if (found.length > 0 && !silent) {
|
|
3775
3799
|
console.log("\nSkill-frontmatter check:\n");
|
|
3776
3800
|
for (const issue of found) {
|
|
3777
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3778
|
-
|
|
3801
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3802
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(issue.path));
|
|
3779
3803
|
}
|
|
3780
3804
|
}
|
|
3781
3805
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3787,13 +3811,14 @@ function checkSkillFrontmatter(config, silent, adapter, scanRoot) {
|
|
|
3787
3811
|
* lane stays first-class — so it fires once and the message links the guide.
|
|
3788
3812
|
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
3789
3813
|
*/
|
|
3790
|
-
function checkPreferCompiledHooks(
|
|
3814
|
+
function checkPreferCompiledHooks(ctx) {
|
|
3815
|
+
const { config, silent } = ctx;
|
|
3791
3816
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
3792
3817
|
if (!sev)
|
|
3793
3818
|
return { issues: 0, errors: 0 };
|
|
3794
3819
|
let count;
|
|
3795
3820
|
try {
|
|
3796
|
-
count =
|
|
3821
|
+
count = ctx.scan().manualHookCount;
|
|
3797
3822
|
}
|
|
3798
3823
|
catch {
|
|
3799
3824
|
return { issues: 0, errors: 0 };
|
|
@@ -3803,8 +3828,8 @@ function checkPreferCompiledHooks(config, silent, adapter, scanRoot) {
|
|
|
3803
3828
|
const message = (0, scan_js_1.preferCompiledHooksMessage)(count);
|
|
3804
3829
|
if (!silent) {
|
|
3805
3830
|
console.log("\nCompiled-hooks check:\n");
|
|
3806
|
-
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${message}`);
|
|
3807
|
-
|
|
3831
|
+
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${ctx.label(message)}`);
|
|
3832
|
+
ctx.annotate(sev === "error" ? "error" : "warning", message);
|
|
3808
3833
|
}
|
|
3809
3834
|
return { issues: 1, errors: sev === "error" ? 1 : 0 };
|
|
3810
3835
|
}
|
|
@@ -3813,13 +3838,14 @@ function checkPreferCompiledHooks(config, silent, adapter, scanRoot) {
|
|
|
3813
3838
|
* (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
|
|
3814
3839
|
* Warning by default; "error" gates CI.
|
|
3815
3840
|
*/
|
|
3816
|
-
function checkMcpConfig(
|
|
3841
|
+
function checkMcpConfig(ctx) {
|
|
3842
|
+
const { config, silent } = ctx;
|
|
3817
3843
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
|
|
3818
3844
|
if (!sev)
|
|
3819
3845
|
return { issues: 0, errors: 0 };
|
|
3820
3846
|
let found;
|
|
3821
3847
|
try {
|
|
3822
|
-
found =
|
|
3848
|
+
found = ctx.scan().mcpIssues;
|
|
3823
3849
|
}
|
|
3824
3850
|
catch {
|
|
3825
3851
|
return { issues: 0, errors: 0 };
|
|
@@ -3827,8 +3853,8 @@ function checkMcpConfig(config, silent, adapter, scanRoot) {
|
|
|
3827
3853
|
if (found.length > 0 && !silent) {
|
|
3828
3854
|
console.log("\nMCP-config check:\n");
|
|
3829
3855
|
for (const issue of found) {
|
|
3830
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3831
|
-
|
|
3856
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3857
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
3832
3858
|
}
|
|
3833
3859
|
}
|
|
3834
3860
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3840,7 +3866,8 @@ function checkMcpConfig(config, silent, adapter, scanRoot) {
|
|
|
3840
3866
|
* `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
|
|
3841
3867
|
* "error" gates CI.
|
|
3842
3868
|
*/
|
|
3843
|
-
function checkDisallowedTools(
|
|
3869
|
+
function checkDisallowedTools(ctx) {
|
|
3870
|
+
const { config, silent, adapter } = ctx;
|
|
3844
3871
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
|
|
3845
3872
|
if (!sev)
|
|
3846
3873
|
return { issues: 0, errors: 0 };
|
|
@@ -3850,7 +3877,10 @@ function checkDisallowedTools(config, silent, adapter, scanRoot) {
|
|
|
3850
3877
|
}
|
|
3851
3878
|
let found;
|
|
3852
3879
|
try {
|
|
3853
|
-
found =
|
|
3880
|
+
found = ctx.scan().agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({
|
|
3881
|
+
message: i.message,
|
|
3882
|
+
path: a.path,
|
|
3883
|
+
})));
|
|
3854
3884
|
}
|
|
3855
3885
|
catch {
|
|
3856
3886
|
return { issues: 0, errors: 0 };
|
|
@@ -3858,8 +3888,8 @@ function checkDisallowedTools(config, silent, adapter, scanRoot) {
|
|
|
3858
3888
|
if (found.length > 0 && !silent) {
|
|
3859
3889
|
console.log("\nDisallowed-tools check:\n");
|
|
3860
3890
|
for (const issue of found) {
|
|
3861
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.path}: ${issue.message}`);
|
|
3862
|
-
|
|
3891
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(issue.path)}: ${issue.message}`);
|
|
3892
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(issue.path));
|
|
3863
3893
|
}
|
|
3864
3894
|
}
|
|
3865
3895
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3872,13 +3902,14 @@ function checkDisallowedTools(config, silent, adapter, scanRoot) {
|
|
|
3872
3902
|
* colon / `<example>` is flagged though it may still load — hence WARN by default
|
|
3873
3903
|
* (verify before setting "error").
|
|
3874
3904
|
*/
|
|
3875
|
-
function checkFrontmatterValid(
|
|
3905
|
+
function checkFrontmatterValid(ctx) {
|
|
3906
|
+
const { config, silent } = ctx;
|
|
3876
3907
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
|
|
3877
3908
|
if (!sev)
|
|
3878
3909
|
return { issues: 0, errors: 0 };
|
|
3879
3910
|
let found;
|
|
3880
3911
|
try {
|
|
3881
|
-
found =
|
|
3912
|
+
found = ctx.scan().malformedFrontmatter;
|
|
3882
3913
|
}
|
|
3883
3914
|
catch {
|
|
3884
3915
|
return { issues: 0, errors: 0 };
|
|
@@ -3886,8 +3917,8 @@ function checkFrontmatterValid(config, silent, adapter, scanRoot) {
|
|
|
3886
3917
|
if (found.length > 0 && !silent) {
|
|
3887
3918
|
console.log("\nFrontmatter-validity check:\n");
|
|
3888
3919
|
for (const issue of found) {
|
|
3889
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3890
|
-
|
|
3920
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3921
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(issue.path));
|
|
3891
3922
|
}
|
|
3892
3923
|
}
|
|
3893
3924
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3899,13 +3930,14 @@ function checkFrontmatterValid(config, silent, adapter, scanRoot) {
|
|
|
3899
3930
|
* `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
|
|
3900
3931
|
* identical text). Warning by default; "error" gates CI.
|
|
3901
3932
|
*/
|
|
3902
|
-
function checkDescriptionOverlap(
|
|
3933
|
+
function checkDescriptionOverlap(ctx) {
|
|
3934
|
+
const { config, silent } = ctx;
|
|
3903
3935
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
|
|
3904
3936
|
if (!sev)
|
|
3905
3937
|
return { issues: 0, errors: 0 };
|
|
3906
3938
|
let found;
|
|
3907
3939
|
try {
|
|
3908
|
-
found =
|
|
3940
|
+
found = ctx.scan().descriptionOverlaps;
|
|
3909
3941
|
}
|
|
3910
3942
|
catch {
|
|
3911
3943
|
return { issues: 0, errors: 0 };
|
|
@@ -3913,8 +3945,8 @@ function checkDescriptionOverlap(config, silent, adapter, scanRoot) {
|
|
|
3913
3945
|
if (found.length > 0 && !silent) {
|
|
3914
3946
|
console.log("\nDescription-overlap check:\n");
|
|
3915
3947
|
for (const issue of found) {
|
|
3916
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3917
|
-
|
|
3948
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3949
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
3918
3950
|
}
|
|
3919
3951
|
}
|
|
3920
3952
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3926,13 +3958,14 @@ function checkDescriptionOverlap(config, silent, adapter, scanRoot) {
|
|
|
3926
3958
|
* deterministic heuristic proxy (generous 500-char budget). Reuses `scanPlugin`'s
|
|
3927
3959
|
* `descriptionBudgetIssues`. Warning by default; "error" gates CI.
|
|
3928
3960
|
*/
|
|
3929
|
-
function checkDescriptionBudget(
|
|
3961
|
+
function checkDescriptionBudget(ctx) {
|
|
3962
|
+
const { config, silent } = ctx;
|
|
3930
3963
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-description-budget"]);
|
|
3931
3964
|
if (!sev)
|
|
3932
3965
|
return { issues: 0, errors: 0 };
|
|
3933
3966
|
let found;
|
|
3934
3967
|
try {
|
|
3935
|
-
found =
|
|
3968
|
+
found = ctx.scan().descriptionBudgetIssues;
|
|
3936
3969
|
}
|
|
3937
3970
|
catch {
|
|
3938
3971
|
return { issues: 0, errors: 0 };
|
|
@@ -3940,8 +3973,8 @@ function checkDescriptionBudget(config, silent, adapter, scanRoot) {
|
|
|
3940
3973
|
if (found.length > 0 && !silent) {
|
|
3941
3974
|
console.log("\nSkill-description-budget check:\n");
|
|
3942
3975
|
for (const issue of found) {
|
|
3943
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3944
|
-
|
|
3976
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3977
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
3945
3978
|
}
|
|
3946
3979
|
}
|
|
3947
3980
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3962,13 +3995,14 @@ function checkDescriptionBudget(config, silent, adapter, scanRoot) {
|
|
|
3962
3995
|
* COUNT is unchanged (units, not lines), so exit codes and the CI gate are
|
|
3963
3996
|
* unaffected by the collapse.
|
|
3964
3997
|
*/
|
|
3965
|
-
function checkLethalTrifecta(
|
|
3998
|
+
function checkLethalTrifecta(ctx) {
|
|
3999
|
+
const { config, silent } = ctx;
|
|
3966
4000
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["lethal-trifecta"]);
|
|
3967
4001
|
if (!sev)
|
|
3968
4002
|
return { issues: 0, errors: 0 };
|
|
3969
4003
|
let found;
|
|
3970
4004
|
try {
|
|
3971
|
-
found =
|
|
4005
|
+
found = ctx.scan().trifectaFindings;
|
|
3972
4006
|
}
|
|
3973
4007
|
catch {
|
|
3974
4008
|
return { issues: 0, errors: 0 };
|
|
@@ -3982,8 +4016,8 @@ function checkLethalTrifecta(config, silent, adapter, scanRoot) {
|
|
|
3982
4016
|
if (unfenced.includes(t))
|
|
3983
4017
|
continue;
|
|
3984
4018
|
const msg = `${t.kind} ${t.name}: ${t.finding.message}`;
|
|
3985
|
-
console.log(` ${mark} ${t.path}: ${msg}`);
|
|
3986
|
-
|
|
4019
|
+
console.log(` ${mark} ${ctx.bundle.scanned(t.path)}: ${msg}`);
|
|
4020
|
+
ctx.annotate(level, msg, ctx.bundle.scanned(t.path));
|
|
3987
4021
|
}
|
|
3988
4022
|
if (unfenced.length > 0) {
|
|
3989
4023
|
console.log(` ${mark} ${String(unfenced.length)} skill(s) declare no \`disallowed-tools:\` fence, so each ` +
|
|
@@ -4002,17 +4036,18 @@ function checkLethalTrifecta(config, silent, adapter, scanRoot) {
|
|
|
4002
4036
|
* nothing. Reuses `scanPlugin`'s `skillResourceIssues` (high-precision / FP-safe,
|
|
4003
4037
|
* one detector, no drift). Warning by default; "error" gates CI.
|
|
4004
4038
|
*/
|
|
4005
|
-
function checkSkillResourceResolves(
|
|
4039
|
+
function checkSkillResourceResolves(ctx) {
|
|
4040
|
+
const { config, silent } = ctx;
|
|
4006
4041
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-resource-resolves"]);
|
|
4007
4042
|
if (!sev)
|
|
4008
4043
|
return { issues: 0, errors: 0 };
|
|
4009
4044
|
let found;
|
|
4010
4045
|
try {
|
|
4011
|
-
found =
|
|
4046
|
+
found = ctx.scan({
|
|
4012
4047
|
sharedDirs: config?.sharedDirs,
|
|
4013
|
-
// sharedDirs live at the
|
|
4014
|
-
// scoped subdir
|
|
4015
|
-
sharedDirsRoot:
|
|
4048
|
+
// sharedDirs live at the frame root — the repo that OWNS the scan target:
|
|
4049
|
+
// cwd for a scoped subdir or a nested bundle, the target for a foreign lint.
|
|
4050
|
+
sharedDirsRoot: ctx.frame.root,
|
|
4016
4051
|
}).skillResourceIssues;
|
|
4017
4052
|
}
|
|
4018
4053
|
catch {
|
|
@@ -4027,8 +4062,8 @@ function checkSkillResourceResolves(config, silent, adapter, scanRoot) {
|
|
|
4027
4062
|
// it was documented only in docs/skills-monorepo.md, so a CI log gave no
|
|
4028
4063
|
// hint and the rule read as broken rather than misconfigured.
|
|
4029
4064
|
` If it resolves from the repo root instead, add its directory to \`sharedDirs\` in .vigilesrc.json.`;
|
|
4030
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
4031
|
-
|
|
4065
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(s.path)}: ${msg}`);
|
|
4066
|
+
ctx.annotate(sev === "error" ? "error" : "warning", msg, ctx.bundle.scanned(s.path));
|
|
4032
4067
|
}
|
|
4033
4068
|
}
|
|
4034
4069
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4040,13 +4075,14 @@ function checkSkillResourceResolves(config, silent, adapter, scanRoot) {
|
|
|
4040
4075
|
* Reuses `scanPlugin`'s `skillFenceIssues` (one detector, no drift). Warning by
|
|
4041
4076
|
* default; "error" gates CI.
|
|
4042
4077
|
*/
|
|
4043
|
-
function checkSkillMissingFence(
|
|
4078
|
+
function checkSkillMissingFence(ctx) {
|
|
4079
|
+
const { config, silent } = ctx;
|
|
4044
4080
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-missing-fence"]);
|
|
4045
4081
|
if (!sev)
|
|
4046
4082
|
return { issues: 0, errors: 0 };
|
|
4047
4083
|
let found;
|
|
4048
4084
|
try {
|
|
4049
|
-
found =
|
|
4085
|
+
found = ctx.scan().skillFenceIssues;
|
|
4050
4086
|
}
|
|
4051
4087
|
catch {
|
|
4052
4088
|
return { issues: 0, errors: 0 };
|
|
@@ -4055,8 +4091,8 @@ function checkSkillMissingFence(config, silent, adapter, scanRoot) {
|
|
|
4055
4091
|
console.log("\nSkill-missing-fence check:\n");
|
|
4056
4092
|
for (const s of found) {
|
|
4057
4093
|
const msg = `${s.name}: ${s.finding.message}`;
|
|
4058
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
4059
|
-
|
|
4094
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(s.path)}: ${msg}`);
|
|
4095
|
+
ctx.annotate(sev === "error" ? "error" : "warning", msg, ctx.bundle.scanned(s.path));
|
|
4060
4096
|
}
|
|
4061
4097
|
}
|
|
4062
4098
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4068,13 +4104,14 @@ function checkSkillMissingFence(config, silent, adapter, scanRoot) {
|
|
|
4068
4104
|
* `pluginLayoutIssues` (one detector, no drift). Warning by default; "error"
|
|
4069
4105
|
* gates CI.
|
|
4070
4106
|
*/
|
|
4071
|
-
function checkPluginDirLayout(
|
|
4107
|
+
function checkPluginDirLayout(ctx) {
|
|
4108
|
+
const { config, silent } = ctx;
|
|
4072
4109
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["plugin-dir-layout"]);
|
|
4073
4110
|
if (!sev)
|
|
4074
4111
|
return { issues: 0, errors: 0 };
|
|
4075
4112
|
let found;
|
|
4076
4113
|
try {
|
|
4077
|
-
found =
|
|
4114
|
+
found = ctx.scan().pluginLayoutIssues;
|
|
4078
4115
|
}
|
|
4079
4116
|
catch {
|
|
4080
4117
|
return { issues: 0, errors: 0 };
|
|
@@ -4082,8 +4119,8 @@ function checkPluginDirLayout(config, silent, adapter, scanRoot) {
|
|
|
4082
4119
|
if (found.length > 0 && !silent) {
|
|
4083
4120
|
console.log("\nPlugin-dir-layout check:\n");
|
|
4084
4121
|
for (const p of found) {
|
|
4085
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${p.message}`);
|
|
4086
|
-
|
|
4122
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(p.message)}`);
|
|
4123
|
+
ctx.annotate(sev === "error" ? "error" : "warning", p.message);
|
|
4087
4124
|
}
|
|
4088
4125
|
}
|
|
4089
4126
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4096,13 +4133,14 @@ function checkPluginDirLayout(config, silent, adapter, scanRoot) {
|
|
|
4096
4133
|
* gates CI. Surfaces across the subagent graph, so it is NOT gated on a
|
|
4097
4134
|
* capability the way a surface-specific rule is.
|
|
4098
4135
|
*/
|
|
4099
|
-
function checkDelegationTrifecta(
|
|
4136
|
+
function checkDelegationTrifecta(ctx) {
|
|
4137
|
+
const { config, silent } = ctx;
|
|
4100
4138
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["delegation-trifecta"]);
|
|
4101
4139
|
if (!sev)
|
|
4102
4140
|
return { issues: 0, errors: 0 };
|
|
4103
4141
|
let found;
|
|
4104
4142
|
try {
|
|
4105
|
-
found =
|
|
4143
|
+
found = ctx.scan().delegationTrifecta;
|
|
4106
4144
|
}
|
|
4107
4145
|
catch {
|
|
4108
4146
|
return { issues: 0, errors: 0 };
|
|
@@ -4111,8 +4149,8 @@ function checkDelegationTrifecta(config, silent, adapter, scanRoot) {
|
|
|
4111
4149
|
console.log("\nDelegation-trifecta check:\n");
|
|
4112
4150
|
for (const d of found) {
|
|
4113
4151
|
const msg = `${d.finding.name}: ${d.finding.message}`;
|
|
4114
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${d.path}: ${msg}`);
|
|
4115
|
-
|
|
4152
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(d.path)}: ${msg}`);
|
|
4153
|
+
ctx.annotate(sev === "error" ? "error" : "warning", msg, ctx.bundle.scanned(d.path));
|
|
4116
4154
|
}
|
|
4117
4155
|
}
|
|
4118
4156
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4124,7 +4162,8 @@ function checkDelegationTrifecta(config, silent, adapter, scanRoot) {
|
|
|
4124
4162
|
* permission-gated event (#19009, the #1 verified hook pain). Reuses `scanPlugin`'s
|
|
4125
4163
|
* `hookBlockFindings` (one detector, no drift). Warning by default; "error" gates CI.
|
|
4126
4164
|
*/
|
|
4127
|
-
function checkHookBlockIneffective(
|
|
4165
|
+
function checkHookBlockIneffective(ctx) {
|
|
4166
|
+
const { config, silent, adapter } = ctx;
|
|
4128
4167
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-block-ineffective"]);
|
|
4129
4168
|
if (!sev)
|
|
4130
4169
|
return { issues: 0, errors: 0 };
|
|
@@ -4134,22 +4173,38 @@ function checkHookBlockIneffective(config, silent, adapter, scanRoot) {
|
|
|
4134
4173
|
}
|
|
4135
4174
|
let found;
|
|
4136
4175
|
try {
|
|
4137
|
-
found =
|
|
4176
|
+
found = ctx.scan().hookBlockFindings;
|
|
4138
4177
|
}
|
|
4139
4178
|
catch {
|
|
4140
4179
|
return { issues: 0, errors: 0 };
|
|
4141
4180
|
}
|
|
4142
4181
|
if (found.length > 0 && !silent) {
|
|
4182
|
+
const mark = sev === "error" ? "✗" : "⚠";
|
|
4183
|
+
const level = sev === "error" ? "error" : "warning";
|
|
4143
4184
|
console.log("\nHook-block check:\n");
|
|
4144
4185
|
for (const h of found) {
|
|
4145
|
-
const
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
ghAnnotate(sev === "error" ? "error" : "warning", msg, h.scriptPath ?? undefined);
|
|
4186
|
+
const { msg, file, shown } = hookBlockLine(ctx, h);
|
|
4187
|
+
console.log(` ${mark} ${shown}`);
|
|
4188
|
+
ctx.annotate(level, msg, file);
|
|
4149
4189
|
}
|
|
4150
4190
|
}
|
|
4151
4191
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
4152
4192
|
}
|
|
4193
|
+
/**
|
|
4194
|
+
* One hook-block finding in the repo frame. scan-core reports the script
|
|
4195
|
+
* ABSOLUTE (it resolves the command against the bundle), which used to print a
|
|
4196
|
+
* machine path and annotate `file=/home/…` (#281); an inline hook names no file
|
|
4197
|
+
* and so carries the bundle label instead.
|
|
4198
|
+
*/
|
|
4199
|
+
function hookBlockLine(ctx, h) {
|
|
4200
|
+
if (h.scriptPath === null) {
|
|
4201
|
+
const msg = `[${h.event}] (inline): ${h.message}`;
|
|
4202
|
+
return { msg, shown: ctx.label(msg) };
|
|
4203
|
+
}
|
|
4204
|
+
const file = ctx.bundle.scanned(h.scriptPath);
|
|
4205
|
+
const msg = `[${h.event}] ${file}: ${h.message}`;
|
|
4206
|
+
return { msg, file, shown: msg };
|
|
4207
|
+
}
|
|
4153
4208
|
/**
|
|
4154
4209
|
* Apply the `hook-matcher` rule: a hook `matcher` string that doesn't fire as
|
|
4155
4210
|
* written — a tool-name typo (`bash`→`Bash`), a matcher that doesn't compile, an
|
|
@@ -4158,7 +4213,8 @@ function checkHookBlockIneffective(config, silent, adapter, scanRoot) {
|
|
|
4158
4213
|
* Reuses `scanPlugin`'s `hookMatcherFindings` (one detector, no drift). Warning
|
|
4159
4214
|
* by default; "error" gates CI.
|
|
4160
4215
|
*/
|
|
4161
|
-
function checkHookMatcher(
|
|
4216
|
+
function checkHookMatcher(ctx) {
|
|
4217
|
+
const { config, silent, adapter } = ctx;
|
|
4162
4218
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-matcher"]);
|
|
4163
4219
|
if (!sev)
|
|
4164
4220
|
return { issues: 0, errors: 0 };
|
|
@@ -4168,7 +4224,7 @@ function checkHookMatcher(config, silent, adapter, scanRoot) {
|
|
|
4168
4224
|
}
|
|
4169
4225
|
let found;
|
|
4170
4226
|
try {
|
|
4171
|
-
found =
|
|
4227
|
+
found = ctx.scan().hookMatcherFindings;
|
|
4172
4228
|
}
|
|
4173
4229
|
catch {
|
|
4174
4230
|
return { issues: 0, errors: 0 };
|
|
@@ -4176,8 +4232,8 @@ function checkHookMatcher(config, silent, adapter, scanRoot) {
|
|
|
4176
4232
|
if (found.length > 0 && !silent) {
|
|
4177
4233
|
console.log("\nHook-matcher check:\n");
|
|
4178
4234
|
for (const m of found) {
|
|
4179
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${m.message}`);
|
|
4180
|
-
|
|
4235
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(m.message)}`);
|
|
4236
|
+
ctx.annotate(sev === "error" ? "error" : "warning", m.message);
|
|
4181
4237
|
}
|
|
4182
4238
|
}
|
|
4183
4239
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4189,7 +4245,8 @@ function checkHookMatcher(config, silent, adapter, scanRoot) {
|
|
|
4189
4245
|
* `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
|
|
4190
4246
|
* Warning by default; "error" gates CI.
|
|
4191
4247
|
*/
|
|
4192
|
-
function checkMcpHookTargets(
|
|
4248
|
+
function checkMcpHookTargets(ctx) {
|
|
4249
|
+
const { config, silent, adapter } = ctx;
|
|
4193
4250
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
|
|
4194
4251
|
if (!sev)
|
|
4195
4252
|
return { issues: 0, errors: 0 };
|
|
@@ -4199,7 +4256,7 @@ function checkMcpHookTargets(config, silent, adapter, scanRoot) {
|
|
|
4199
4256
|
}
|
|
4200
4257
|
let found;
|
|
4201
4258
|
try {
|
|
4202
|
-
found =
|
|
4259
|
+
found = ctx.scan().mcpHookIssues;
|
|
4203
4260
|
}
|
|
4204
4261
|
catch {
|
|
4205
4262
|
return { issues: 0, errors: 0 };
|
|
@@ -4207,8 +4264,8 @@ function checkMcpHookTargets(config, silent, adapter, scanRoot) {
|
|
|
4207
4264
|
if (found.length > 0 && !silent) {
|
|
4208
4265
|
console.log("\nMCP hook-target check:\n");
|
|
4209
4266
|
for (const issue of found) {
|
|
4210
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
4211
|
-
|
|
4267
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
4268
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
4212
4269
|
}
|
|
4213
4270
|
}
|
|
4214
4271
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4221,7 +4278,8 @@ function checkMcpHookTargets(config, silent, adapter, scanRoot) {
|
|
|
4221
4278
|
* existence-guarded one-liners, inline commands). Matches Anthropic's own
|
|
4222
4279
|
* `claude plugin validate`. Warning by default; "error" gates CI.
|
|
4223
4280
|
*/
|
|
4224
|
-
function checkHookScriptExists(
|
|
4281
|
+
function checkHookScriptExists(ctx) {
|
|
4282
|
+
const { config, silent, adapter } = ctx;
|
|
4225
4283
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
|
|
4226
4284
|
if (!sev)
|
|
4227
4285
|
return { issues: 0, errors: 0 };
|
|
@@ -4231,7 +4289,7 @@ function checkHookScriptExists(config, silent, adapter, scanRoot) {
|
|
|
4231
4289
|
}
|
|
4232
4290
|
let missing;
|
|
4233
4291
|
try {
|
|
4234
|
-
missing =
|
|
4292
|
+
missing = ctx.scan().hooks.filter((h) => h.status === "missing");
|
|
4235
4293
|
}
|
|
4236
4294
|
catch {
|
|
4237
4295
|
return { issues: 0, errors: 0 };
|
|
@@ -4239,9 +4297,12 @@ function checkHookScriptExists(config, silent, adapter, scanRoot) {
|
|
|
4239
4297
|
if (missing.length > 0 && !silent) {
|
|
4240
4298
|
console.log("\nHook-script existence check:\n");
|
|
4241
4299
|
for (const h of missing) {
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4300
|
+
// `script` arrives with the plugin-root token EXPANDED, i.e. absolute —
|
|
4301
|
+
// printed as-is it was a machine path (#281). A relative one is the
|
|
4302
|
+
// bundle's own, which is what `scanned` assumes.
|
|
4303
|
+
const msg = `hook script "${ctx.bundle.scanned(h.script)}" is referenced but missing — the hook never runs.`;
|
|
4304
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(msg)}`);
|
|
4305
|
+
ctx.annotate(sev === "error" ? "error" : "warning", msg);
|
|
4245
4306
|
}
|
|
4246
4307
|
}
|
|
4247
4308
|
return {
|
|
@@ -4256,7 +4317,8 @@ function checkHookScriptExists(config, silent, adapter, scanRoot) {
|
|
|
4256
4317
|
* — high-precision (gated on a declared set, built-ins allowlisted, the
|
|
4257
4318
|
* plugin-namespaced form skipped). Warning by default; "error" gates CI.
|
|
4258
4319
|
*/
|
|
4259
|
-
function checkMcpToolResolves(
|
|
4320
|
+
function checkMcpToolResolves(ctx) {
|
|
4321
|
+
const { config, silent, adapter } = ctx;
|
|
4260
4322
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
|
|
4261
4323
|
if (!sev)
|
|
4262
4324
|
return { issues: 0, errors: 0 };
|
|
@@ -4266,7 +4328,9 @@ function checkMcpToolResolves(config, silent, adapter, scanRoot) {
|
|
|
4266
4328
|
}
|
|
4267
4329
|
let found;
|
|
4268
4330
|
try {
|
|
4269
|
-
found =
|
|
4331
|
+
found = ctx
|
|
4332
|
+
.scan()
|
|
4333
|
+
.agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
4270
4334
|
}
|
|
4271
4335
|
catch {
|
|
4272
4336
|
return { issues: 0, errors: 0 };
|
|
@@ -4274,8 +4338,8 @@ function checkMcpToolResolves(config, silent, adapter, scanRoot) {
|
|
|
4274
4338
|
if (found.length > 0 && !silent) {
|
|
4275
4339
|
console.log("\nMCP tool-resolution check:\n");
|
|
4276
4340
|
for (const issue of found) {
|
|
4277
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.path}: ${issue.message}`);
|
|
4278
|
-
|
|
4341
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(issue.path)}: ${issue.message}`);
|
|
4342
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(issue.path));
|
|
4279
4343
|
}
|
|
4280
4344
|
}
|
|
4281
4345
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4316,7 +4380,7 @@ async function checkCoverageThresholds(excludes, coverage, config, silent) {
|
|
|
4316
4380
|
// Load all claude specs so coverage doesn't depend on a built dist/.
|
|
4317
4381
|
const loaded = await Promise.all(findSpecs(excludes).map(loadSpec));
|
|
4318
4382
|
const claudeSpecs = loaded.filter((s) => s?._specType === "claude");
|
|
4319
|
-
const metric = (0, coverage_js_1.computeScriptCoverage)(process.cwd(), opts.scripts, claudeSpecs, excludes.
|
|
4383
|
+
const metric = (0, coverage_js_1.computeScriptCoverage)(process.cwd(), opts.scripts, claudeSpecs, excludes.globIgnore);
|
|
4320
4384
|
const ok = metric.passing;
|
|
4321
4385
|
if (!ok)
|
|
4322
4386
|
failing++;
|
|
@@ -4807,7 +4871,7 @@ function resolveRecords(cwd, runs, tier, harnessFlag) {
|
|
|
4807
4871
|
* 1. `.claude-plugin/plugin.json#name` — the repo's own declared name, used
|
|
4808
4872
|
* when the run installed the repo AS a plugin (`pluginDir`).
|
|
4809
4873
|
* 2. `vigiles-loose-skills` — the synthetic name OUR OWN packaging gives a
|
|
4810
|
-
* loose `.claude/skills` dir (`packageSkillsDir`, and `
|
|
4874
|
+
* loose `.claude/skills` dir (`packageSkillsDir`, and `installNamespace`'s
|
|
4811
4875
|
* fallback when a plugin manifest has no name). A repo that is not a plugin
|
|
4812
4876
|
* still reports namespaced ids under it, so omitting it would drop every
|
|
4813
4877
|
* trigger-rate record for the documented one-liner.
|
|
@@ -4828,20 +4892,35 @@ function resolveRecords(cwd, runs, tier, harnessFlag) {
|
|
|
4828
4892
|
return [...names];
|
|
4829
4893
|
}
|
|
4830
4894
|
const recordsConfig = (0, validate_js_1.loadConfig)();
|
|
4831
|
-
const
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4895
|
+
const layout = harnessLayoutFor(cwd, recordsConfig, harnessFlag);
|
|
4896
|
+
// The repo-wide `exclude` only — the per-rule severities/include stay out
|
|
4897
|
+
// of record resolution on purpose (a run's probes must map to a surface
|
|
4898
|
+
// whether or not the untested-* rule for its kind is on).
|
|
4899
|
+
const excludes = (0, exclude_js_1.excludeSet)(cwd, recordsConfig.exclude);
|
|
4900
|
+
// 🔴 THE SAME BUNDLES `lint` SCORES (#281, D5). This discovered the root
|
|
4901
|
+
// bundle only, so under `bundles: "all"` a run that exercised a nested skill
|
|
4902
|
+
// recorded nothing, and `lint` then called that skill untested. Every bundle's
|
|
4903
|
+
// surfaces are expressed from `cwd`, the one frame the artifact is keyed in; a
|
|
4904
|
+
// name two bundles share identifies neither (`resolveProbe` takes only a
|
|
4905
|
+
// unique match), which is the honest answer to an ambiguous probe.
|
|
4906
|
+
const bundles = recordsConfig.bundles === "all"
|
|
4907
|
+
? [cwd, ...discoverNestedBundles(cwd, excludes)]
|
|
4908
|
+
: [cwd];
|
|
4909
|
+
const surfaces = bundles.flatMap((basePath) => {
|
|
4910
|
+
const scan = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
4911
|
+
basePath,
|
|
4912
|
+
root: cwd,
|
|
4913
|
+
layout,
|
|
4914
|
+
excludes,
|
|
4915
|
+
});
|
|
4916
|
+
return [...scan.covered, ...scan.untested];
|
|
4838
4917
|
});
|
|
4839
4918
|
return (0, coverage_artifact_js_1.recordsFrom)({
|
|
4840
4919
|
runs,
|
|
4841
|
-
surfaces
|
|
4920
|
+
surfaces,
|
|
4842
4921
|
tier,
|
|
4843
4922
|
at: new Date().toISOString(),
|
|
4844
|
-
selfNamespaces: selfNamespaces
|
|
4923
|
+
selfNamespaces: [...new Set(bundles.flatMap(selfNamespaces))],
|
|
4845
4924
|
// The root an ABSOLUTE command ref must lie beneath to be OURS. Without it a
|
|
4846
4925
|
// harness that executed `/tmp/fixture/hooks/pre.sh` credited this repo's own
|
|
4847
4926
|
// `hooks/pre.sh`, because the tail matched. See the ladder note on
|
|
@@ -4943,7 +5022,7 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
4943
5022
|
}
|
|
4944
5023
|
// A script under an excluded path is not DISCOVERED (a vendored corpus's own
|
|
4945
5024
|
// harness must not run as ours), but a script you NAME still runs (#192).
|
|
4946
|
-
const files = (0, run_scripts_js_1.discoverScripts)(restArgs.map((p) => noteExplicitOverride(excludes, p, "running")), defaultGlob, cwd, excludes.
|
|
5025
|
+
const files = (0, run_scripts_js_1.discoverScripts)(restArgs.map((p) => noteExplicitOverride(excludes, p, "running")), defaultGlob, cwd, excludes.globIgnore);
|
|
4947
5026
|
// `--min=N`: a CI gate asserts at least N scripts actually LOADED — so a bad
|
|
4948
5027
|
// path, a renamed file, a glob that matched nothing, or a file that matched and
|
|
4949
5028
|
// never linked fails LOUD instead of passing green with nothing executed.
|
|
@@ -5766,17 +5845,31 @@ function evalLockNudgeHookCommand() {
|
|
|
5766
5845
|
// heard nothing at all, while a repo that already tests got reminded. That is
|
|
5767
5846
|
// backwards, and `untested-skill` already stated the missing half correctly;
|
|
5768
5847
|
// it just lived in `vigiles lint`, which someone has to run by hand.
|
|
5769
|
-
|
|
5848
|
+
// Same union `vigiles lint` applies: the rule's exclude narrows, the
|
|
5849
|
+
// repo-wide exclude is the floor (#192) — the nudge must not report a
|
|
5850
|
+
// vendored skill the linter would never list.
|
|
5851
|
+
const excludes = (0, exclude_js_1.excludeSet)(cwd, config.exclude);
|
|
5852
|
+
// 🔴 THE BUNDLE THAT HOLDS THE EDIT, found the way `lint` finds it (#281, D6).
|
|
5853
|
+
// The nudge scanned the root bundle only and matched by SUFFIX, so an edit to
|
|
5854
|
+
// `plugins/p/skills/x/SKILL.md` was answered with the coverage of the ROOT's
|
|
5855
|
+
// `skills/x`. Now the edited path is a `RepoPath`, the scan runs over the
|
|
5856
|
+
// bundle it lives in, and the two are compared for equality in one frame.
|
|
5857
|
+
// A nested bundle `lint` would not score (no `bundles: "all"`) gets no nudge,
|
|
5858
|
+
// for the same reason it gets no finding.
|
|
5859
|
+
const frame = (0, frame_js_1.frameAt)(cwd);
|
|
5860
|
+
const edited = (0, node_path_1.resolve)(cwd, file);
|
|
5861
|
+
const home = (config.bundles === "all"
|
|
5862
|
+
? discoverNestedBundles(cwd, excludes).find((b) => {
|
|
5863
|
+
const r = (0, node_path_1.relative)(b, edited);
|
|
5864
|
+
return r !== "" && !r.startsWith("..") && !(0, node_path_1.isAbsolute)(r);
|
|
5865
|
+
})
|
|
5866
|
+
: undefined) ?? cwd;
|
|
5867
|
+
const msg = (0, test_coverage_js_1.skillTestNudge)(frame.repo(edited), {
|
|
5770
5868
|
...options,
|
|
5771
|
-
basePath:
|
|
5869
|
+
basePath: home,
|
|
5870
|
+
root: cwd,
|
|
5772
5871
|
layout,
|
|
5773
|
-
|
|
5774
|
-
// repo-wide exclude is the floor (#192) — the nudge must not report a
|
|
5775
|
-
// vendored skill the linter would never list.
|
|
5776
|
-
exclude: [
|
|
5777
|
-
...(options.exclude ?? []),
|
|
5778
|
-
...(0, exclude_js_1.excludeSet)(cwd, config.exclude).ignore,
|
|
5779
|
-
],
|
|
5872
|
+
excludes,
|
|
5780
5873
|
}) ?? (0, eval_lock_js_1.evalLockNudge)(target, (0, node_path_1.resolve)(cwd, eval_lock_js_1.DEFAULT_LOCK_DIR));
|
|
5781
5874
|
if (!msg)
|
|
5782
5875
|
return;
|
|
@@ -6733,9 +6826,12 @@ async function main() {
|
|
|
6733
6826
|
process.exitCode = 2;
|
|
6734
6827
|
return;
|
|
6735
6828
|
}
|
|
6829
|
+
// The same single owner of "relative to which directory" `lint` uses
|
|
6830
|
+
// (#281): cwd, unless the audited dir is somebody else's repository.
|
|
6831
|
+
const frame = (0, frame_js_1.frameFor)(process.cwd(), root);
|
|
6736
6832
|
const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
|
|
6737
6833
|
sharedDirs: config.sharedDirs,
|
|
6738
|
-
sharedDirsRoot:
|
|
6834
|
+
sharedDirsRoot: frame.root,
|
|
6739
6835
|
// `.vigilesrc.json#exclude` reaches surface DISCOVERY, not just the
|
|
6740
6836
|
// instruction file. Measured 2026-09-21 before this line existed: a repo
|
|
6741
6837
|
// with `{"exclude": [".claude"]}` and one skill at `.claude/skills/demo`
|
|
@@ -6770,7 +6866,16 @@ async function main() {
|
|
|
6770
6866
|
// `init` adopts (layout-driven instruction file + skill/subagent sweep).
|
|
6771
6867
|
// Surfaced in the AuditReport (the report's "Create spec" command-emit
|
|
6772
6868
|
// buttons read it) and the terminal nudge below.
|
|
6773
|
-
|
|
6869
|
+
//
|
|
6870
|
+
// 🔴 IN THE FRAME `init` RUNS IN, AND WITHOUT WHAT THE REPO EXCLUDES (#281,
|
|
6871
|
+
// D7). The sweep reads the audited dir, so its paths are relative to it —
|
|
6872
|
+
// and for `audit plugins/p` the printed `npx vigiles init
|
|
6873
|
+
// --target=skills/x/SKILL.md` named a file that does not exist from where
|
|
6874
|
+
// you run it, and offered to adopt a skill `exclude` had removed.
|
|
6875
|
+
const auditedBundle = frame.bundle(root);
|
|
6876
|
+
const adoptableSurfaces = discoverAdoptableForAudit(root, adapter.layout.instructionFile)
|
|
6877
|
+
.filter((p) => !(0, exclude_js_1.excludedBy)(excludes)((0, node_path_1.resolve)(root, p)))
|
|
6878
|
+
.map((p) => auditedBundle.scanned(p));
|
|
6774
6879
|
// Read the local flight recorder ONCE — feeds both the JSON report
|
|
6775
6880
|
// (structured summary, the product boundary) and the terminal render.
|
|
6776
6881
|
const ledgerRecords = (0, observe_js_1.readObservations)(root);
|