vigiles 30.0.2 → 31.0.1
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/adapter-conformance.js +9 -15
- package/dist/adapters/claude-code/run-scripts.d.ts +5 -3
- package/dist/adapters/claude-code/run-scripts.js +5 -3
- package/dist/claude-code.js +5 -0
- package/dist/cli-main.d.ts +8 -0
- package/dist/cli-main.js +315 -212
- package/dist/codex.js +5 -0
- package/dist/core/compile.d.ts +5 -5
- package/dist/core/compile.js +28 -25
- package/dist/core/config-schema.d.ts +32 -32
- 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/core/refs.d.ts +2 -2
- package/dist/core/refs.js +9 -12
- package/dist/core/spec.js +5 -0
- package/dist/core/symbols.d.ts +25 -19
- package/dist/core/symbols.js +51 -74
- package/dist/core/tree-sitter-wasm.d.ts +24 -0
- package/dist/core/tree-sitter-wasm.js +128 -0
- package/dist/eval-surface.js +5 -0
- package/dist/exclude.d.ts +3 -5
- package/dist/exclude.js +3 -3
- package/dist/hook-install.js +23 -10
- package/dist/hook.js +5 -0
- package/dist/jest.js +5 -0
- package/dist/linting.d.ts +11 -30
- package/dist/linting.js +22 -35
- 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/dist/test.js +5 -0
- package/dist/vitest.mjs +5 -0
- package/package.json +4 -7
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");
|
|
@@ -335,9 +336,9 @@ function compileGeneratorSkillToFile(specPath, source) {
|
|
|
335
336
|
return false;
|
|
336
337
|
}
|
|
337
338
|
/** Compile a ClaudeSpec → its primary + any additional targets. */
|
|
338
|
-
function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
339
|
+
async function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
339
340
|
const basePath = process.cwd();
|
|
340
|
-
const { markdown, errors, warnings, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
|
|
341
|
+
const { markdown, errors, warnings, linterResults, targets } = await (0, compile_js_1.compileClaude)(spec, {
|
|
341
342
|
basePath,
|
|
342
343
|
specFile: specPath,
|
|
343
344
|
dialect,
|
|
@@ -471,9 +472,9 @@ function formatArtifactSize(markdown) {
|
|
|
471
472
|
return `${size} · ~${String(kTokens)}k tokens est.`;
|
|
472
473
|
}
|
|
473
474
|
/** Compile a declarative SkillSpec → SKILL.md. */
|
|
474
|
-
function compileSkillToFile(spec, specPath, dialect) {
|
|
475
|
+
async function compileSkillToFile(spec, specPath, dialect) {
|
|
475
476
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
476
|
-
const { artifact, errors, warnings } = (0, compile_js_1.compileSkill)(spec, {
|
|
477
|
+
const { artifact, errors, warnings } = await (0, compile_js_1.compileSkill)(spec, {
|
|
477
478
|
basePath: process.cwd(),
|
|
478
479
|
specFile: specPath,
|
|
479
480
|
// The SKILL.md frontmatter profile comes from the resolved harness — a Codex
|
|
@@ -495,9 +496,9 @@ function compileSkillToFile(spec, specPath, dialect) {
|
|
|
495
496
|
return false;
|
|
496
497
|
}
|
|
497
498
|
/** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
|
|
498
|
-
function compileAgentToFile(spec, specPath, dialect) {
|
|
499
|
+
async function compileAgentToFile(spec, specPath, dialect) {
|
|
499
500
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
500
|
-
const { artifact, errors, warnings } = (0, compile_js_1.compileAgent)(spec, {
|
|
501
|
+
const { artifact, errors, warnings } = await (0, compile_js_1.compileAgent)(spec, {
|
|
501
502
|
basePath: process.cwd(),
|
|
502
503
|
specFile: specPath,
|
|
503
504
|
dialect,
|
|
@@ -592,7 +593,7 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
592
593
|
const specDialect = opts.harnessFlag === undefined
|
|
593
594
|
? ((0, adapter_registry_js_1.adapterForInstructionFile)(targetFile)?.dialect ?? dialect)
|
|
594
595
|
: dialect;
|
|
595
|
-
if (compileClaudeToFile(spec, specPath, config, specDialect)) {
|
|
596
|
+
if (await compileClaudeToFile(spec, specPath, config, specDialect)) {
|
|
596
597
|
writeInstructionMirrors(specPath.replace(/\.spec\.ts$/, ""), declaredHarnesses);
|
|
597
598
|
}
|
|
598
599
|
else {
|
|
@@ -608,11 +609,11 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
608
609
|
for (const w of (0, skill_harness_js_1.skillFrontmatterDropWarnings)(spec, forHarnesses)) {
|
|
609
610
|
console.log(`⚠ ${w}`);
|
|
610
611
|
}
|
|
611
|
-
if (!compileSkillToFile(spec, specPath, dialect))
|
|
612
|
+
if (!(await compileSkillToFile(spec, specPath, dialect)))
|
|
612
613
|
allValid = false;
|
|
613
614
|
}
|
|
614
615
|
else if (spec._specType === "agent") {
|
|
615
|
-
if (!compileAgentToFile(spec, specPath, dialect))
|
|
616
|
+
if (!(await compileAgentToFile(spec, specPath, dialect)))
|
|
616
617
|
allValid = false;
|
|
617
618
|
}
|
|
618
619
|
else if (spec._specType === "railway") {
|
|
@@ -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
|
-
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();
|
|
@@ -835,7 +843,7 @@ function verifyMarkdownSymbols(files, silent) {
|
|
|
835
843
|
catch {
|
|
836
844
|
continue;
|
|
837
845
|
}
|
|
838
|
-
const broken = (0, refs_js_1.verifySymbolRefs)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, f)));
|
|
846
|
+
const broken = await (0, refs_js_1.verifySymbolRefs)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, f)));
|
|
839
847
|
if (broken.length === 0)
|
|
840
848
|
continue;
|
|
841
849
|
if (!silent) {
|
|
@@ -845,7 +853,7 @@ 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 @@ 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
|
/**
|
|
@@ -1361,7 +1372,7 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
|
1361
1372
|
// collected lazily so a repo with no railway spec never pays for the walk.
|
|
1362
1373
|
if (spec._specType === "railway")
|
|
1363
1374
|
knownAgents ??= await collectAgentNames(excludes);
|
|
1364
|
-
const errors = specCompileErrors(spec, specPath, dialect, config, knownAgents ?? []);
|
|
1375
|
+
const errors = await specCompileErrors(spec, specPath, dialect, config, knownAgents ?? []);
|
|
1365
1376
|
for (const e of errors)
|
|
1366
1377
|
found.push(`${target}: ${e.message} (from ${specPath})`);
|
|
1367
1378
|
}
|
|
@@ -1399,11 +1410,11 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
|
1399
1410
|
* only the options differ. Exhaustive over `_specType`, so a FIFTH spec type is
|
|
1400
1411
|
* a tsc error here instead of a silent skip, which is the failure this closes.
|
|
1401
1412
|
*/
|
|
1402
|
-
function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
1413
|
+
async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
1403
1414
|
const basePath = process.cwd();
|
|
1404
1415
|
switch (spec._specType) {
|
|
1405
1416
|
case "claude":
|
|
1406
|
-
return (0, compile_js_1.compileClaude)(spec, {
|
|
1417
|
+
return (await (0, compile_js_1.compileClaude)(spec, {
|
|
1407
1418
|
basePath,
|
|
1408
1419
|
specFile: specPath,
|
|
1409
1420
|
dialect,
|
|
@@ -1412,13 +1423,11 @@ function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
|
1412
1423
|
maxSectionLines: config?.maxSectionLines,
|
|
1413
1424
|
catalogOnly: config?.catalogOnly,
|
|
1414
1425
|
linters: config?.linters,
|
|
1415
|
-
}).errors;
|
|
1426
|
+
})).errors;
|
|
1416
1427
|
case "skill":
|
|
1417
|
-
return (0, compile_js_1.compileSkill)(spec, { basePath, specFile: specPath, dialect })
|
|
1418
|
-
.errors;
|
|
1428
|
+
return (await (0, compile_js_1.compileSkill)(spec, { basePath, specFile: specPath, dialect })).errors;
|
|
1419
1429
|
case "agent":
|
|
1420
|
-
return (0, compile_js_1.compileAgent)(spec, { basePath, specFile: specPath, dialect })
|
|
1421
|
-
.errors;
|
|
1430
|
+
return (await (0, compile_js_1.compileAgent)(spec, { basePath, specFile: specPath, dialect })).errors;
|
|
1422
1431
|
case "railway":
|
|
1423
1432
|
return (0, compile_js_1.compileRailway)(spec, { specFile: specPath, knownAgents }).errors;
|
|
1424
1433
|
default:
|
|
@@ -1487,6 +1496,11 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1487
1496
|
const nestedBundles = discoverNestedBundles(scanRoot, excludes);
|
|
1488
1497
|
const scoreAll = flags.includes("--bundles=all") || config?.bundles === "all";
|
|
1489
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 };
|
|
1490
1504
|
if (!silent && nestedBundles.length > 0) {
|
|
1491
1505
|
const rel = nestedBundles.map((b) => (0, node_path_1.relative)(scanRoot, b) || b);
|
|
1492
1506
|
console.log(scoreAll
|
|
@@ -1512,11 +1526,11 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1512
1526
|
}
|
|
1513
1527
|
}
|
|
1514
1528
|
const hashResult = files.length > 0
|
|
1515
|
-
? check(files, silent)
|
|
1529
|
+
? check(frame, files, silent)
|
|
1516
1530
|
: { valid: true, hashErrors: 0, validationErrors: 0 };
|
|
1517
1531
|
// 1b. Verify inline + frontmatter rules in instruction files not managed
|
|
1518
1532
|
// by a spec. See verifyMarkdownModeRules / docs/markdown-mode.md.
|
|
1519
|
-
const md = verifyMarkdownModeRules(files, silent, config);
|
|
1533
|
+
const md = verifyMarkdownModeRules(frame, files, silent, config);
|
|
1520
1534
|
const { inlineErrors, inlineRules, frontmatterErrors, frontmatterRules } = md;
|
|
1521
1535
|
// 2. Coverage gaps (discover)
|
|
1522
1536
|
if (!silent)
|
|
@@ -1572,7 +1586,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1572
1586
|
// The repo-wide `exclude` is the FLOOR under the rule's own `exclude`
|
|
1573
1587
|
// (union, never override): an excluded corpus is neither an orphan
|
|
1574
1588
|
// candidate nor a source of references that keep a doc alive (#192).
|
|
1575
|
-
repoExclude: excludes.
|
|
1589
|
+
repoExclude: excludes.globIgnore,
|
|
1576
1590
|
// Exempt every registered harness's surface files (instruction file,
|
|
1577
1591
|
// SKILL.md, subagents, commands) as orphan candidates — layout-driven so
|
|
1578
1592
|
// core carries no harness literal (see src/core/orphans.ts).
|
|
@@ -1593,74 +1607,74 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1593
1607
|
// A compiled artifact's refs, re-derived from its spec — the hash says the file
|
|
1594
1608
|
// is unchanged, not that what it names still exists (#173).
|
|
1595
1609
|
const specRefs = await checkSpecRefs(excludes, config, silent, adapter.dialect);
|
|
1596
|
-
const untested = overBundles(
|
|
1610
|
+
const untested = overBundles(checkUntestedSurfaces, run, lintRoots);
|
|
1597
1611
|
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
1598
1612
|
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
1599
1613
|
// subagents. Off by default unless a severity is configured; warning surfaces
|
|
1600
1614
|
// a typo/never-available tool, error gates CI.
|
|
1601
|
-
const toolContract = overBundles(checkSubagentToolContracts,
|
|
1615
|
+
const toolContract = overBundles(checkSubagentToolContracts, run, lintRoots);
|
|
1602
1616
|
// 7d. Hook-event check — a hook registered under an event the harness doesn't
|
|
1603
1617
|
// define never fires. High-precision (close typos only). Off unless configured.
|
|
1604
|
-
const hookEvents = overBundles(checkHookEvents,
|
|
1618
|
+
const hookEvents = overBundles(checkHookEvents, run, lintRoots);
|
|
1605
1619
|
// 7e. Subagent-frontmatter check — a subagent missing required frontmatter
|
|
1606
1620
|
// (name + description) won't register. n/a on a harness with no subagents.
|
|
1607
|
-
const frontmatter = overBundles(checkFrontmatterSchema,
|
|
1621
|
+
const frontmatter = overBundles(checkFrontmatterSchema, run, lintRoots);
|
|
1608
1622
|
// 7f. MCP-config check — a declared MCP server with no command/url can't start.
|
|
1609
|
-
const mcpConfig = overBundles(checkMcpConfig,
|
|
1623
|
+
const mcpConfig = overBundles(checkMcpConfig, run, lintRoots);
|
|
1610
1624
|
// 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
|
|
1611
1625
|
// reliable trigger surface). Best-practice nudge; skills load without it.
|
|
1612
|
-
const skillFm = overBundles(checkSkillFrontmatter,
|
|
1626
|
+
const skillFm = overBundles(checkSkillFrontmatter, run, lintRoots);
|
|
1613
1627
|
// 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
|
|
1614
1628
|
// the plugin doesn't declare can't resolve (the MCP half of the tool moat).
|
|
1615
|
-
const mcpToolResolves = overBundles(checkMcpToolResolves,
|
|
1629
|
+
const mcpToolResolves = overBundles(checkMcpToolResolves, run, lintRoots);
|
|
1616
1630
|
// 7i. Hook-script existence — a hook command referencing a missing script file
|
|
1617
1631
|
// never runs. This comment used to add "matches Anthropic's own `claude plugin
|
|
1618
1632
|
// validate`"; measured false on 2026-09-08 (Claude Code 2.1.263) — see
|
|
1619
1633
|
// docs/rules/hook-script-exists.md and tools/measure-validate-overlap.mjs.
|
|
1620
|
-
const hookScripts = overBundles(checkHookScriptExists,
|
|
1634
|
+
const hookScripts = overBundles(checkHookScriptExists, run, lintRoots);
|
|
1621
1635
|
// 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
|
|
1622
1636
|
// (the deny-side mirror of subagent-tool-contract; close-typo only).
|
|
1623
|
-
const disallowedTools = overBundles(checkDisallowedTools,
|
|
1637
|
+
const disallowedTools = overBundles(checkDisallowedTools, run, lintRoots);
|
|
1624
1638
|
// 7k. Description-overlap — two model-invocable skills with near-identical
|
|
1625
1639
|
// descriptions collide in the selector (deterministic NCD precision proxy).
|
|
1626
|
-
const descriptionOverlap = overBundles(checkDescriptionOverlap,
|
|
1640
|
+
const descriptionOverlap = overBundles(checkDescriptionOverlap, run, lintRoots);
|
|
1627
1641
|
// 7k². Skill-description-budget — a model-invocable skill whose description is
|
|
1628
1642
|
// so long the trigger signal is buried (heuristic proxy; degrades recall +
|
|
1629
1643
|
// precision). Generous 500-char budget; warn-tier, never gates.
|
|
1630
|
-
const descriptionBudget = overBundles(checkDescriptionBudget,
|
|
1644
|
+
const descriptionBudget = overBundles(checkDescriptionBudget, run, lintRoots);
|
|
1631
1645
|
// 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
|
|
1632
1646
|
// stricter than some loaders, so verify before enforcing).
|
|
1633
|
-
const frontmatterValid = overBundles(checkFrontmatterValid,
|
|
1647
|
+
const frontmatterValid = overBundles(checkFrontmatterValid, run, lintRoots);
|
|
1634
1648
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
1635
1649
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
1636
|
-
const mcpHookTargets = overBundles(checkMcpHookTargets,
|
|
1650
|
+
const mcpHookTargets = overBundles(checkMcpHookTargets, run, lintRoots);
|
|
1637
1651
|
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
1638
1652
|
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
1639
|
-
const preferCompiledHooks = overBundles(checkPreferCompiledHooks,
|
|
1653
|
+
const preferCompiledHooks = overBundles(checkPreferCompiledHooks, run, lintRoots);
|
|
1640
1654
|
// 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
|
|
1641
1655
|
// hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
|
|
1642
1656
|
// prompt-injection exfil path (Rule of Two). Capability SET-intersection.
|
|
1643
|
-
const lethalTrifecta = overBundles(checkLethalTrifecta,
|
|
1657
|
+
const lethalTrifecta = overBundles(checkLethalTrifecta, run, lintRoots);
|
|
1644
1658
|
// 7p. Skill-resource — a SKILL.md body referencing a bundled file that doesn't
|
|
1645
1659
|
// exist on disk under the skill dir (the agent gets nothing). FP-safe.
|
|
1646
|
-
const skillResources = overBundles(checkSkillResourceResolves,
|
|
1660
|
+
const skillResources = overBundles(checkSkillResourceResolves, run, lintRoots);
|
|
1647
1661
|
// 7q. Skill-missing-fence — a SKILL.md opening with `name:`/`description:` but no
|
|
1648
1662
|
// `---` fence loads as plain body (invisible — no name/description/trigger).
|
|
1649
|
-
const skillFence = overBundles(checkSkillMissingFence,
|
|
1663
|
+
const skillFence = overBundles(checkSkillMissingFence, run, lintRoots);
|
|
1650
1664
|
// 7r. Plugin-dir-layout — functional surface dirs (skills/agents/commands) nested
|
|
1651
1665
|
// inside the `.claude-plugin/` manifest dir where the harness can't see them.
|
|
1652
|
-
const pluginLayout = overBundles(checkPluginDirLayout,
|
|
1666
|
+
const pluginLayout = overBundles(checkPluginDirLayout, run, lintRoots);
|
|
1653
1667
|
// 7s. Delegation-trifecta — a lethal trifecta that emerges across a delegation
|
|
1654
1668
|
// edge (a subagent's own ∪ delegated-to capability) though no single unit trips it.
|
|
1655
|
-
const delegationTrifecta = overBundles(checkDelegationTrifecta,
|
|
1669
|
+
const delegationTrifecta = overBundles(checkDelegationTrifecta, run, lintRoots);
|
|
1656
1670
|
// 7t. Hook-block-ineffective — a hook that looks like it blocks but silently
|
|
1657
1671
|
// doesn't (block decision on a non-blocking event, or the legacy `decision`
|
|
1658
1672
|
// field on a permission-gated event). The #1 verified hook pain (#19009).
|
|
1659
|
-
const hookBlock = overBundles(checkHookBlockIneffective,
|
|
1673
|
+
const hookBlock = overBundles(checkHookBlockIneffective, run, lintRoots);
|
|
1660
1674
|
// 7u. Hook-matcher — a hook `matcher` that doesn't fire as written (tool-name
|
|
1661
1675
|
// typo, an uncompilable or unreachable MCP pattern, one too narrow for real
|
|
1662
1676
|
// server naming, or an undeclared MCP server).
|
|
1663
|
-
const hookMatcher = overBundles(checkHookMatcher,
|
|
1677
|
+
const hookMatcher = overBundles(checkHookMatcher, run, lintRoots);
|
|
1664
1678
|
// 8. Validate vigiles builder calls inside markdown code blocks — the
|
|
1665
1679
|
// `doc-refs` rule, DEFAULT OFF. Illustrative blocks opt out via
|
|
1666
1680
|
// `<!-- vigiles:ignore -->` (single block) or `<!-- vigiles:ignore-file -->`
|
|
@@ -1698,7 +1712,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1698
1712
|
// whose exit code is discarded gates nothing — after which hand-written CI steps
|
|
1699
1713
|
// grew to do the gating instead. One unpassed argument, that whole chain.
|
|
1700
1714
|
const docRefReport = docRefSeverity
|
|
1701
|
-
? (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd(), ignore: excludes.
|
|
1715
|
+
? (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd(), ignore: excludes.globIgnore })
|
|
1702
1716
|
: {
|
|
1703
1717
|
filesScanned: 0,
|
|
1704
1718
|
filesIgnored: 0,
|
|
@@ -1721,14 +1735,14 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1721
1735
|
// on the PR. CI-only (isGitHubActions); skipped under --json/--summary.
|
|
1722
1736
|
if (isGitHubActions() && !silent && docRefSeverity) {
|
|
1723
1737
|
for (const e of docRefReport.errors) {
|
|
1724
|
-
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);
|
|
1725
1739
|
}
|
|
1726
1740
|
}
|
|
1727
1741
|
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
1728
|
-
const symbolRefErrors = verifyMarkdownSymbols(files, silent);
|
|
1742
|
+
const symbolRefErrors = await verifyMarkdownSymbols(frame, files, silent);
|
|
1729
1743
|
// 10. Verify `vigiles:mcp server#tool` marks against live MCP servers
|
|
1730
1744
|
// (only when a .mcp.json declares them). See src/mcp.ts.
|
|
1731
|
-
const mcpRefErrors = await verifyMarkdownMcpRefs(files, silent);
|
|
1745
|
+
const mcpRefErrors = await verifyMarkdownMcpRefs(frame, files, silent);
|
|
1732
1746
|
const report = {
|
|
1733
1747
|
hashErrors: hashResult.hashErrors,
|
|
1734
1748
|
validationErrors: hashResult.validationErrors,
|
|
@@ -3607,18 +3621,22 @@ function untestedRules(config) {
|
|
|
3607
3621
|
* "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
|
|
3608
3622
|
* untested count plus the severity-gated error count.
|
|
3609
3623
|
*/
|
|
3610
|
-
function checkUntestedSurfaces(
|
|
3624
|
+
function checkUntestedSurfaces(ctx) {
|
|
3625
|
+
const { config, silent, adapter, frame, bundle } = ctx;
|
|
3611
3626
|
const { severity: sevFor, anyEnabled, options } = untestedRules(config);
|
|
3612
3627
|
if (!anyEnabled)
|
|
3613
3628
|
return { untested: 0, errors: 0 };
|
|
3614
3629
|
const report = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
3615
3630
|
...options,
|
|
3616
|
-
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,
|
|
3617
3635
|
layout: adapter.layout,
|
|
3618
3636
|
// The rule's own `exclude` NARROWS; the repo-wide one is the floor under
|
|
3619
3637
|
// it. Union, never override — a rule option must not re-admit a vendored
|
|
3620
|
-
// corpus the repo excluded (#192).
|
|
3621
|
-
|
|
3638
|
+
// corpus the repo excluded (#192). Passed WHOLE so it carries its own root.
|
|
3639
|
+
excludes: ctx.excludes,
|
|
3622
3640
|
});
|
|
3623
3641
|
if (!silent) {
|
|
3624
3642
|
console.log("\nUntested surfaces:\n");
|
|
@@ -3626,7 +3644,7 @@ function checkUntestedSurfaces(excludes, config, silent, adapter, scanRoot) {
|
|
|
3626
3644
|
console.log(` ${line}`);
|
|
3627
3645
|
}
|
|
3628
3646
|
for (const s of report.untested) {
|
|
3629
|
-
|
|
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)));
|
|
3630
3648
|
}
|
|
3631
3649
|
}
|
|
3632
3650
|
return {
|
|
@@ -3655,7 +3673,8 @@ function reportNotApplicable(check, surface, adapter, silent) {
|
|
|
3655
3673
|
* (plugin/MCP-provided) is never a false alarm. Warning by default; set
|
|
3656
3674
|
* `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
|
|
3657
3675
|
*/
|
|
3658
|
-
function checkSubagentToolContracts(
|
|
3676
|
+
function checkSubagentToolContracts(ctx) {
|
|
3677
|
+
const { config, silent, adapter } = ctx;
|
|
3659
3678
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
|
|
3660
3679
|
if (!sev)
|
|
3661
3680
|
return { issues: 0, errors: 0 };
|
|
@@ -3668,7 +3687,7 @@ function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
|
|
|
3668
3687
|
// `agents/` path, so a harness with a different subagent dir Just Works.
|
|
3669
3688
|
let agents;
|
|
3670
3689
|
try {
|
|
3671
|
-
agents =
|
|
3690
|
+
agents = ctx.scan().agents;
|
|
3672
3691
|
}
|
|
3673
3692
|
catch {
|
|
3674
3693
|
return { issues: 0, errors: 0 };
|
|
@@ -3685,8 +3704,8 @@ function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
|
|
|
3685
3704
|
printedHeader = true;
|
|
3686
3705
|
}
|
|
3687
3706
|
for (const issue of agent.toolIssues) {
|
|
3688
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${agent.path}: ${issue.message}`);
|
|
3689
|
-
|
|
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));
|
|
3690
3709
|
}
|
|
3691
3710
|
}
|
|
3692
3711
|
}
|
|
@@ -3698,7 +3717,8 @@ function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
|
|
|
3698
3717
|
* `hookEventIssues` (the shared detector, high-precision: close typos only, never
|
|
3699
3718
|
* a framework/custom event). Warning by default; "error" gates CI.
|
|
3700
3719
|
*/
|
|
3701
|
-
function checkHookEvents(
|
|
3720
|
+
function checkHookEvents(ctx) {
|
|
3721
|
+
const { config, silent, adapter } = ctx;
|
|
3702
3722
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
|
|
3703
3723
|
if (!sev)
|
|
3704
3724
|
return { issues: 0, errors: 0 };
|
|
@@ -3708,7 +3728,7 @@ function checkHookEvents(config, silent, adapter, scanRoot) {
|
|
|
3708
3728
|
}
|
|
3709
3729
|
let found;
|
|
3710
3730
|
try {
|
|
3711
|
-
found =
|
|
3731
|
+
found = ctx.scan().hookEventIssues;
|
|
3712
3732
|
}
|
|
3713
3733
|
catch {
|
|
3714
3734
|
return { issues: 0, errors: 0 };
|
|
@@ -3716,8 +3736,8 @@ function checkHookEvents(config, silent, adapter, scanRoot) {
|
|
|
3716
3736
|
if (found.length > 0 && !silent) {
|
|
3717
3737
|
console.log("\nHook-event check:\n");
|
|
3718
3738
|
for (const issue of found) {
|
|
3719
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3720
|
-
|
|
3739
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3740
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
3721
3741
|
}
|
|
3722
3742
|
}
|
|
3723
3743
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3729,7 +3749,8 @@ function checkHookEvents(config, silent, adapter, scanRoot) {
|
|
|
3729
3749
|
* of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
|
|
3730
3750
|
* `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
|
|
3731
3751
|
*/
|
|
3732
|
-
function checkFrontmatterSchema(
|
|
3752
|
+
function checkFrontmatterSchema(ctx) {
|
|
3753
|
+
const { config, silent, adapter } = ctx;
|
|
3733
3754
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
|
|
3734
3755
|
if (!sev)
|
|
3735
3756
|
return { issues: 0, errors: 0 };
|
|
@@ -3739,7 +3760,7 @@ function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
|
|
|
3739
3760
|
}
|
|
3740
3761
|
let found;
|
|
3741
3762
|
try {
|
|
3742
|
-
const r =
|
|
3763
|
+
const r = ctx.scan();
|
|
3743
3764
|
found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
|
|
3744
3765
|
}
|
|
3745
3766
|
catch {
|
|
@@ -3748,8 +3769,8 @@ function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
|
|
|
3748
3769
|
if (found.length > 0 && !silent) {
|
|
3749
3770
|
console.log("\nFrontmatter-schema check:\n");
|
|
3750
3771
|
for (const issue of found) {
|
|
3751
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3752
|
-
|
|
3772
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3773
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(issue.path));
|
|
3753
3774
|
}
|
|
3754
3775
|
}
|
|
3755
3776
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3762,13 +3783,14 @@ function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
|
|
|
3762
3783
|
* default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
|
|
3763
3784
|
* `skillMetaIssues`.
|
|
3764
3785
|
*/
|
|
3765
|
-
function checkSkillFrontmatter(
|
|
3786
|
+
function checkSkillFrontmatter(ctx) {
|
|
3787
|
+
const { config, silent } = ctx;
|
|
3766
3788
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
|
|
3767
3789
|
if (!sev)
|
|
3768
3790
|
return { issues: 0, errors: 0 };
|
|
3769
3791
|
let found;
|
|
3770
3792
|
try {
|
|
3771
|
-
found =
|
|
3793
|
+
found = ctx.scan().skillMetaIssues;
|
|
3772
3794
|
}
|
|
3773
3795
|
catch {
|
|
3774
3796
|
return { issues: 0, errors: 0 };
|
|
@@ -3776,8 +3798,8 @@ function checkSkillFrontmatter(config, silent, adapter, scanRoot) {
|
|
|
3776
3798
|
if (found.length > 0 && !silent) {
|
|
3777
3799
|
console.log("\nSkill-frontmatter check:\n");
|
|
3778
3800
|
for (const issue of found) {
|
|
3779
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3780
|
-
|
|
3801
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3802
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(issue.path));
|
|
3781
3803
|
}
|
|
3782
3804
|
}
|
|
3783
3805
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3789,13 +3811,14 @@ function checkSkillFrontmatter(config, silent, adapter, scanRoot) {
|
|
|
3789
3811
|
* lane stays first-class — so it fires once and the message links the guide.
|
|
3790
3812
|
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
3791
3813
|
*/
|
|
3792
|
-
function checkPreferCompiledHooks(
|
|
3814
|
+
function checkPreferCompiledHooks(ctx) {
|
|
3815
|
+
const { config, silent } = ctx;
|
|
3793
3816
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
3794
3817
|
if (!sev)
|
|
3795
3818
|
return { issues: 0, errors: 0 };
|
|
3796
3819
|
let count;
|
|
3797
3820
|
try {
|
|
3798
|
-
count =
|
|
3821
|
+
count = ctx.scan().manualHookCount;
|
|
3799
3822
|
}
|
|
3800
3823
|
catch {
|
|
3801
3824
|
return { issues: 0, errors: 0 };
|
|
@@ -3805,8 +3828,8 @@ function checkPreferCompiledHooks(config, silent, adapter, scanRoot) {
|
|
|
3805
3828
|
const message = (0, scan_js_1.preferCompiledHooksMessage)(count);
|
|
3806
3829
|
if (!silent) {
|
|
3807
3830
|
console.log("\nCompiled-hooks check:\n");
|
|
3808
|
-
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${message}`);
|
|
3809
|
-
|
|
3831
|
+
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${ctx.label(message)}`);
|
|
3832
|
+
ctx.annotate(sev === "error" ? "error" : "warning", message);
|
|
3810
3833
|
}
|
|
3811
3834
|
return { issues: 1, errors: sev === "error" ? 1 : 0 };
|
|
3812
3835
|
}
|
|
@@ -3815,13 +3838,14 @@ function checkPreferCompiledHooks(config, silent, adapter, scanRoot) {
|
|
|
3815
3838
|
* (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
|
|
3816
3839
|
* Warning by default; "error" gates CI.
|
|
3817
3840
|
*/
|
|
3818
|
-
function checkMcpConfig(
|
|
3841
|
+
function checkMcpConfig(ctx) {
|
|
3842
|
+
const { config, silent } = ctx;
|
|
3819
3843
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
|
|
3820
3844
|
if (!sev)
|
|
3821
3845
|
return { issues: 0, errors: 0 };
|
|
3822
3846
|
let found;
|
|
3823
3847
|
try {
|
|
3824
|
-
found =
|
|
3848
|
+
found = ctx.scan().mcpIssues;
|
|
3825
3849
|
}
|
|
3826
3850
|
catch {
|
|
3827
3851
|
return { issues: 0, errors: 0 };
|
|
@@ -3829,8 +3853,8 @@ function checkMcpConfig(config, silent, adapter, scanRoot) {
|
|
|
3829
3853
|
if (found.length > 0 && !silent) {
|
|
3830
3854
|
console.log("\nMCP-config check:\n");
|
|
3831
3855
|
for (const issue of found) {
|
|
3832
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3833
|
-
|
|
3856
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3857
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
3834
3858
|
}
|
|
3835
3859
|
}
|
|
3836
3860
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3842,7 +3866,8 @@ function checkMcpConfig(config, silent, adapter, scanRoot) {
|
|
|
3842
3866
|
* `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
|
|
3843
3867
|
* "error" gates CI.
|
|
3844
3868
|
*/
|
|
3845
|
-
function checkDisallowedTools(
|
|
3869
|
+
function checkDisallowedTools(ctx) {
|
|
3870
|
+
const { config, silent, adapter } = ctx;
|
|
3846
3871
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
|
|
3847
3872
|
if (!sev)
|
|
3848
3873
|
return { issues: 0, errors: 0 };
|
|
@@ -3852,7 +3877,10 @@ function checkDisallowedTools(config, silent, adapter, scanRoot) {
|
|
|
3852
3877
|
}
|
|
3853
3878
|
let found;
|
|
3854
3879
|
try {
|
|
3855
|
-
found =
|
|
3880
|
+
found = ctx.scan().agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({
|
|
3881
|
+
message: i.message,
|
|
3882
|
+
path: a.path,
|
|
3883
|
+
})));
|
|
3856
3884
|
}
|
|
3857
3885
|
catch {
|
|
3858
3886
|
return { issues: 0, errors: 0 };
|
|
@@ -3860,8 +3888,8 @@ function checkDisallowedTools(config, silent, adapter, scanRoot) {
|
|
|
3860
3888
|
if (found.length > 0 && !silent) {
|
|
3861
3889
|
console.log("\nDisallowed-tools check:\n");
|
|
3862
3890
|
for (const issue of found) {
|
|
3863
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.path}: ${issue.message}`);
|
|
3864
|
-
|
|
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));
|
|
3865
3893
|
}
|
|
3866
3894
|
}
|
|
3867
3895
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3874,13 +3902,14 @@ function checkDisallowedTools(config, silent, adapter, scanRoot) {
|
|
|
3874
3902
|
* colon / `<example>` is flagged though it may still load — hence WARN by default
|
|
3875
3903
|
* (verify before setting "error").
|
|
3876
3904
|
*/
|
|
3877
|
-
function checkFrontmatterValid(
|
|
3905
|
+
function checkFrontmatterValid(ctx) {
|
|
3906
|
+
const { config, silent } = ctx;
|
|
3878
3907
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
|
|
3879
3908
|
if (!sev)
|
|
3880
3909
|
return { issues: 0, errors: 0 };
|
|
3881
3910
|
let found;
|
|
3882
3911
|
try {
|
|
3883
|
-
found =
|
|
3912
|
+
found = ctx.scan().malformedFrontmatter;
|
|
3884
3913
|
}
|
|
3885
3914
|
catch {
|
|
3886
3915
|
return { issues: 0, errors: 0 };
|
|
@@ -3888,8 +3917,8 @@ function checkFrontmatterValid(config, silent, adapter, scanRoot) {
|
|
|
3888
3917
|
if (found.length > 0 && !silent) {
|
|
3889
3918
|
console.log("\nFrontmatter-validity check:\n");
|
|
3890
3919
|
for (const issue of found) {
|
|
3891
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3892
|
-
|
|
3920
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3921
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(issue.path));
|
|
3893
3922
|
}
|
|
3894
3923
|
}
|
|
3895
3924
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3901,13 +3930,14 @@ function checkFrontmatterValid(config, silent, adapter, scanRoot) {
|
|
|
3901
3930
|
* `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
|
|
3902
3931
|
* identical text). Warning by default; "error" gates CI.
|
|
3903
3932
|
*/
|
|
3904
|
-
function checkDescriptionOverlap(
|
|
3933
|
+
function checkDescriptionOverlap(ctx) {
|
|
3934
|
+
const { config, silent } = ctx;
|
|
3905
3935
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
|
|
3906
3936
|
if (!sev)
|
|
3907
3937
|
return { issues: 0, errors: 0 };
|
|
3908
3938
|
let found;
|
|
3909
3939
|
try {
|
|
3910
|
-
found =
|
|
3940
|
+
found = ctx.scan().descriptionOverlaps;
|
|
3911
3941
|
}
|
|
3912
3942
|
catch {
|
|
3913
3943
|
return { issues: 0, errors: 0 };
|
|
@@ -3915,8 +3945,8 @@ function checkDescriptionOverlap(config, silent, adapter, scanRoot) {
|
|
|
3915
3945
|
if (found.length > 0 && !silent) {
|
|
3916
3946
|
console.log("\nDescription-overlap check:\n");
|
|
3917
3947
|
for (const issue of found) {
|
|
3918
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3919
|
-
|
|
3948
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3949
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
3920
3950
|
}
|
|
3921
3951
|
}
|
|
3922
3952
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3928,13 +3958,14 @@ function checkDescriptionOverlap(config, silent, adapter, scanRoot) {
|
|
|
3928
3958
|
* deterministic heuristic proxy (generous 500-char budget). Reuses `scanPlugin`'s
|
|
3929
3959
|
* `descriptionBudgetIssues`. Warning by default; "error" gates CI.
|
|
3930
3960
|
*/
|
|
3931
|
-
function checkDescriptionBudget(
|
|
3961
|
+
function checkDescriptionBudget(ctx) {
|
|
3962
|
+
const { config, silent } = ctx;
|
|
3932
3963
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-description-budget"]);
|
|
3933
3964
|
if (!sev)
|
|
3934
3965
|
return { issues: 0, errors: 0 };
|
|
3935
3966
|
let found;
|
|
3936
3967
|
try {
|
|
3937
|
-
found =
|
|
3968
|
+
found = ctx.scan().descriptionBudgetIssues;
|
|
3938
3969
|
}
|
|
3939
3970
|
catch {
|
|
3940
3971
|
return { issues: 0, errors: 0 };
|
|
@@ -3942,8 +3973,8 @@ function checkDescriptionBudget(config, silent, adapter, scanRoot) {
|
|
|
3942
3973
|
if (found.length > 0 && !silent) {
|
|
3943
3974
|
console.log("\nSkill-description-budget check:\n");
|
|
3944
3975
|
for (const issue of found) {
|
|
3945
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
3946
|
-
|
|
3976
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
3977
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
3947
3978
|
}
|
|
3948
3979
|
}
|
|
3949
3980
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -3964,13 +3995,14 @@ function checkDescriptionBudget(config, silent, adapter, scanRoot) {
|
|
|
3964
3995
|
* COUNT is unchanged (units, not lines), so exit codes and the CI gate are
|
|
3965
3996
|
* unaffected by the collapse.
|
|
3966
3997
|
*/
|
|
3967
|
-
function checkLethalTrifecta(
|
|
3998
|
+
function checkLethalTrifecta(ctx) {
|
|
3999
|
+
const { config, silent } = ctx;
|
|
3968
4000
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["lethal-trifecta"]);
|
|
3969
4001
|
if (!sev)
|
|
3970
4002
|
return { issues: 0, errors: 0 };
|
|
3971
4003
|
let found;
|
|
3972
4004
|
try {
|
|
3973
|
-
found =
|
|
4005
|
+
found = ctx.scan().trifectaFindings;
|
|
3974
4006
|
}
|
|
3975
4007
|
catch {
|
|
3976
4008
|
return { issues: 0, errors: 0 };
|
|
@@ -3984,8 +4016,8 @@ function checkLethalTrifecta(config, silent, adapter, scanRoot) {
|
|
|
3984
4016
|
if (unfenced.includes(t))
|
|
3985
4017
|
continue;
|
|
3986
4018
|
const msg = `${t.kind} ${t.name}: ${t.finding.message}`;
|
|
3987
|
-
console.log(` ${mark} ${t.path}: ${msg}`);
|
|
3988
|
-
|
|
4019
|
+
console.log(` ${mark} ${ctx.bundle.scanned(t.path)}: ${msg}`);
|
|
4020
|
+
ctx.annotate(level, msg, ctx.bundle.scanned(t.path));
|
|
3989
4021
|
}
|
|
3990
4022
|
if (unfenced.length > 0) {
|
|
3991
4023
|
console.log(` ${mark} ${String(unfenced.length)} skill(s) declare no \`disallowed-tools:\` fence, so each ` +
|
|
@@ -4004,17 +4036,18 @@ function checkLethalTrifecta(config, silent, adapter, scanRoot) {
|
|
|
4004
4036
|
* nothing. Reuses `scanPlugin`'s `skillResourceIssues` (high-precision / FP-safe,
|
|
4005
4037
|
* one detector, no drift). Warning by default; "error" gates CI.
|
|
4006
4038
|
*/
|
|
4007
|
-
function checkSkillResourceResolves(
|
|
4039
|
+
function checkSkillResourceResolves(ctx) {
|
|
4040
|
+
const { config, silent } = ctx;
|
|
4008
4041
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-resource-resolves"]);
|
|
4009
4042
|
if (!sev)
|
|
4010
4043
|
return { issues: 0, errors: 0 };
|
|
4011
4044
|
let found;
|
|
4012
4045
|
try {
|
|
4013
|
-
found =
|
|
4046
|
+
found = ctx.scan({
|
|
4014
4047
|
sharedDirs: config?.sharedDirs,
|
|
4015
|
-
// sharedDirs live at the
|
|
4016
|
-
// scoped subdir
|
|
4017
|
-
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,
|
|
4018
4051
|
}).skillResourceIssues;
|
|
4019
4052
|
}
|
|
4020
4053
|
catch {
|
|
@@ -4029,8 +4062,8 @@ function checkSkillResourceResolves(config, silent, adapter, scanRoot) {
|
|
|
4029
4062
|
// it was documented only in docs/skills-monorepo.md, so a CI log gave no
|
|
4030
4063
|
// hint and the rule read as broken rather than misconfigured.
|
|
4031
4064
|
` If it resolves from the repo root instead, add its directory to \`sharedDirs\` in .vigilesrc.json.`;
|
|
4032
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
4033
|
-
|
|
4065
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(s.path)}: ${msg}`);
|
|
4066
|
+
ctx.annotate(sev === "error" ? "error" : "warning", msg, ctx.bundle.scanned(s.path));
|
|
4034
4067
|
}
|
|
4035
4068
|
}
|
|
4036
4069
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4042,13 +4075,14 @@ function checkSkillResourceResolves(config, silent, adapter, scanRoot) {
|
|
|
4042
4075
|
* Reuses `scanPlugin`'s `skillFenceIssues` (one detector, no drift). Warning by
|
|
4043
4076
|
* default; "error" gates CI.
|
|
4044
4077
|
*/
|
|
4045
|
-
function checkSkillMissingFence(
|
|
4078
|
+
function checkSkillMissingFence(ctx) {
|
|
4079
|
+
const { config, silent } = ctx;
|
|
4046
4080
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-missing-fence"]);
|
|
4047
4081
|
if (!sev)
|
|
4048
4082
|
return { issues: 0, errors: 0 };
|
|
4049
4083
|
let found;
|
|
4050
4084
|
try {
|
|
4051
|
-
found =
|
|
4085
|
+
found = ctx.scan().skillFenceIssues;
|
|
4052
4086
|
}
|
|
4053
4087
|
catch {
|
|
4054
4088
|
return { issues: 0, errors: 0 };
|
|
@@ -4057,8 +4091,8 @@ function checkSkillMissingFence(config, silent, adapter, scanRoot) {
|
|
|
4057
4091
|
console.log("\nSkill-missing-fence check:\n");
|
|
4058
4092
|
for (const s of found) {
|
|
4059
4093
|
const msg = `${s.name}: ${s.finding.message}`;
|
|
4060
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
4061
|
-
|
|
4094
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(s.path)}: ${msg}`);
|
|
4095
|
+
ctx.annotate(sev === "error" ? "error" : "warning", msg, ctx.bundle.scanned(s.path));
|
|
4062
4096
|
}
|
|
4063
4097
|
}
|
|
4064
4098
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4070,13 +4104,14 @@ function checkSkillMissingFence(config, silent, adapter, scanRoot) {
|
|
|
4070
4104
|
* `pluginLayoutIssues` (one detector, no drift). Warning by default; "error"
|
|
4071
4105
|
* gates CI.
|
|
4072
4106
|
*/
|
|
4073
|
-
function checkPluginDirLayout(
|
|
4107
|
+
function checkPluginDirLayout(ctx) {
|
|
4108
|
+
const { config, silent } = ctx;
|
|
4074
4109
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["plugin-dir-layout"]);
|
|
4075
4110
|
if (!sev)
|
|
4076
4111
|
return { issues: 0, errors: 0 };
|
|
4077
4112
|
let found;
|
|
4078
4113
|
try {
|
|
4079
|
-
found =
|
|
4114
|
+
found = ctx.scan().pluginLayoutIssues;
|
|
4080
4115
|
}
|
|
4081
4116
|
catch {
|
|
4082
4117
|
return { issues: 0, errors: 0 };
|
|
@@ -4084,8 +4119,8 @@ function checkPluginDirLayout(config, silent, adapter, scanRoot) {
|
|
|
4084
4119
|
if (found.length > 0 && !silent) {
|
|
4085
4120
|
console.log("\nPlugin-dir-layout check:\n");
|
|
4086
4121
|
for (const p of found) {
|
|
4087
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${p.message}`);
|
|
4088
|
-
|
|
4122
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(p.message)}`);
|
|
4123
|
+
ctx.annotate(sev === "error" ? "error" : "warning", p.message);
|
|
4089
4124
|
}
|
|
4090
4125
|
}
|
|
4091
4126
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4098,13 +4133,14 @@ function checkPluginDirLayout(config, silent, adapter, scanRoot) {
|
|
|
4098
4133
|
* gates CI. Surfaces across the subagent graph, so it is NOT gated on a
|
|
4099
4134
|
* capability the way a surface-specific rule is.
|
|
4100
4135
|
*/
|
|
4101
|
-
function checkDelegationTrifecta(
|
|
4136
|
+
function checkDelegationTrifecta(ctx) {
|
|
4137
|
+
const { config, silent } = ctx;
|
|
4102
4138
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["delegation-trifecta"]);
|
|
4103
4139
|
if (!sev)
|
|
4104
4140
|
return { issues: 0, errors: 0 };
|
|
4105
4141
|
let found;
|
|
4106
4142
|
try {
|
|
4107
|
-
found =
|
|
4143
|
+
found = ctx.scan().delegationTrifecta;
|
|
4108
4144
|
}
|
|
4109
4145
|
catch {
|
|
4110
4146
|
return { issues: 0, errors: 0 };
|
|
@@ -4113,8 +4149,8 @@ function checkDelegationTrifecta(config, silent, adapter, scanRoot) {
|
|
|
4113
4149
|
console.log("\nDelegation-trifecta check:\n");
|
|
4114
4150
|
for (const d of found) {
|
|
4115
4151
|
const msg = `${d.finding.name}: ${d.finding.message}`;
|
|
4116
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${d.path}: ${msg}`);
|
|
4117
|
-
|
|
4152
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(d.path)}: ${msg}`);
|
|
4153
|
+
ctx.annotate(sev === "error" ? "error" : "warning", msg, ctx.bundle.scanned(d.path));
|
|
4118
4154
|
}
|
|
4119
4155
|
}
|
|
4120
4156
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4126,7 +4162,8 @@ function checkDelegationTrifecta(config, silent, adapter, scanRoot) {
|
|
|
4126
4162
|
* permission-gated event (#19009, the #1 verified hook pain). Reuses `scanPlugin`'s
|
|
4127
4163
|
* `hookBlockFindings` (one detector, no drift). Warning by default; "error" gates CI.
|
|
4128
4164
|
*/
|
|
4129
|
-
function checkHookBlockIneffective(
|
|
4165
|
+
function checkHookBlockIneffective(ctx) {
|
|
4166
|
+
const { config, silent, adapter } = ctx;
|
|
4130
4167
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-block-ineffective"]);
|
|
4131
4168
|
if (!sev)
|
|
4132
4169
|
return { issues: 0, errors: 0 };
|
|
@@ -4136,22 +4173,38 @@ function checkHookBlockIneffective(config, silent, adapter, scanRoot) {
|
|
|
4136
4173
|
}
|
|
4137
4174
|
let found;
|
|
4138
4175
|
try {
|
|
4139
|
-
found =
|
|
4176
|
+
found = ctx.scan().hookBlockFindings;
|
|
4140
4177
|
}
|
|
4141
4178
|
catch {
|
|
4142
4179
|
return { issues: 0, errors: 0 };
|
|
4143
4180
|
}
|
|
4144
4181
|
if (found.length > 0 && !silent) {
|
|
4182
|
+
const mark = sev === "error" ? "✗" : "⚠";
|
|
4183
|
+
const level = sev === "error" ? "error" : "warning";
|
|
4145
4184
|
console.log("\nHook-block check:\n");
|
|
4146
4185
|
for (const h of found) {
|
|
4147
|
-
const
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
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);
|
|
4151
4189
|
}
|
|
4152
4190
|
}
|
|
4153
4191
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
4154
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
|
+
}
|
|
4155
4208
|
/**
|
|
4156
4209
|
* Apply the `hook-matcher` rule: a hook `matcher` string that doesn't fire as
|
|
4157
4210
|
* written — a tool-name typo (`bash`→`Bash`), a matcher that doesn't compile, an
|
|
@@ -4160,7 +4213,8 @@ function checkHookBlockIneffective(config, silent, adapter, scanRoot) {
|
|
|
4160
4213
|
* Reuses `scanPlugin`'s `hookMatcherFindings` (one detector, no drift). Warning
|
|
4161
4214
|
* by default; "error" gates CI.
|
|
4162
4215
|
*/
|
|
4163
|
-
function checkHookMatcher(
|
|
4216
|
+
function checkHookMatcher(ctx) {
|
|
4217
|
+
const { config, silent, adapter } = ctx;
|
|
4164
4218
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-matcher"]);
|
|
4165
4219
|
if (!sev)
|
|
4166
4220
|
return { issues: 0, errors: 0 };
|
|
@@ -4170,7 +4224,7 @@ function checkHookMatcher(config, silent, adapter, scanRoot) {
|
|
|
4170
4224
|
}
|
|
4171
4225
|
let found;
|
|
4172
4226
|
try {
|
|
4173
|
-
found =
|
|
4227
|
+
found = ctx.scan().hookMatcherFindings;
|
|
4174
4228
|
}
|
|
4175
4229
|
catch {
|
|
4176
4230
|
return { issues: 0, errors: 0 };
|
|
@@ -4178,8 +4232,8 @@ function checkHookMatcher(config, silent, adapter, scanRoot) {
|
|
|
4178
4232
|
if (found.length > 0 && !silent) {
|
|
4179
4233
|
console.log("\nHook-matcher check:\n");
|
|
4180
4234
|
for (const m of found) {
|
|
4181
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${m.message}`);
|
|
4182
|
-
|
|
4235
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(m.message)}`);
|
|
4236
|
+
ctx.annotate(sev === "error" ? "error" : "warning", m.message);
|
|
4183
4237
|
}
|
|
4184
4238
|
}
|
|
4185
4239
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4191,7 +4245,8 @@ function checkHookMatcher(config, silent, adapter, scanRoot) {
|
|
|
4191
4245
|
* `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
|
|
4192
4246
|
* Warning by default; "error" gates CI.
|
|
4193
4247
|
*/
|
|
4194
|
-
function checkMcpHookTargets(
|
|
4248
|
+
function checkMcpHookTargets(ctx) {
|
|
4249
|
+
const { config, silent, adapter } = ctx;
|
|
4195
4250
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
|
|
4196
4251
|
if (!sev)
|
|
4197
4252
|
return { issues: 0, errors: 0 };
|
|
@@ -4201,7 +4256,7 @@ function checkMcpHookTargets(config, silent, adapter, scanRoot) {
|
|
|
4201
4256
|
}
|
|
4202
4257
|
let found;
|
|
4203
4258
|
try {
|
|
4204
|
-
found =
|
|
4259
|
+
found = ctx.scan().mcpHookIssues;
|
|
4205
4260
|
}
|
|
4206
4261
|
catch {
|
|
4207
4262
|
return { issues: 0, errors: 0 };
|
|
@@ -4209,8 +4264,8 @@ function checkMcpHookTargets(config, silent, adapter, scanRoot) {
|
|
|
4209
4264
|
if (found.length > 0 && !silent) {
|
|
4210
4265
|
console.log("\nMCP hook-target check:\n");
|
|
4211
4266
|
for (const issue of found) {
|
|
4212
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
4213
|
-
|
|
4267
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(issue.message)}`);
|
|
4268
|
+
ctx.annotate(sev === "error" ? "error" : "warning", issue.message);
|
|
4214
4269
|
}
|
|
4215
4270
|
}
|
|
4216
4271
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4223,7 +4278,8 @@ function checkMcpHookTargets(config, silent, adapter, scanRoot) {
|
|
|
4223
4278
|
* existence-guarded one-liners, inline commands). Matches Anthropic's own
|
|
4224
4279
|
* `claude plugin validate`. Warning by default; "error" gates CI.
|
|
4225
4280
|
*/
|
|
4226
|
-
function checkHookScriptExists(
|
|
4281
|
+
function checkHookScriptExists(ctx) {
|
|
4282
|
+
const { config, silent, adapter } = ctx;
|
|
4227
4283
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
|
|
4228
4284
|
if (!sev)
|
|
4229
4285
|
return { issues: 0, errors: 0 };
|
|
@@ -4233,7 +4289,7 @@ function checkHookScriptExists(config, silent, adapter, scanRoot) {
|
|
|
4233
4289
|
}
|
|
4234
4290
|
let missing;
|
|
4235
4291
|
try {
|
|
4236
|
-
missing =
|
|
4292
|
+
missing = ctx.scan().hooks.filter((h) => h.status === "missing");
|
|
4237
4293
|
}
|
|
4238
4294
|
catch {
|
|
4239
4295
|
return { issues: 0, errors: 0 };
|
|
@@ -4241,9 +4297,12 @@ function checkHookScriptExists(config, silent, adapter, scanRoot) {
|
|
|
4241
4297
|
if (missing.length > 0 && !silent) {
|
|
4242
4298
|
console.log("\nHook-script existence check:\n");
|
|
4243
4299
|
for (const h of missing) {
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
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);
|
|
4247
4306
|
}
|
|
4248
4307
|
}
|
|
4249
4308
|
return {
|
|
@@ -4258,7 +4317,8 @@ function checkHookScriptExists(config, silent, adapter, scanRoot) {
|
|
|
4258
4317
|
* — high-precision (gated on a declared set, built-ins allowlisted, the
|
|
4259
4318
|
* plugin-namespaced form skipped). Warning by default; "error" gates CI.
|
|
4260
4319
|
*/
|
|
4261
|
-
function checkMcpToolResolves(
|
|
4320
|
+
function checkMcpToolResolves(ctx) {
|
|
4321
|
+
const { config, silent, adapter } = ctx;
|
|
4262
4322
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
|
|
4263
4323
|
if (!sev)
|
|
4264
4324
|
return { issues: 0, errors: 0 };
|
|
@@ -4268,7 +4328,9 @@ function checkMcpToolResolves(config, silent, adapter, scanRoot) {
|
|
|
4268
4328
|
}
|
|
4269
4329
|
let found;
|
|
4270
4330
|
try {
|
|
4271
|
-
found =
|
|
4331
|
+
found = ctx
|
|
4332
|
+
.scan()
|
|
4333
|
+
.agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
4272
4334
|
}
|
|
4273
4335
|
catch {
|
|
4274
4336
|
return { issues: 0, errors: 0 };
|
|
@@ -4276,8 +4338,8 @@ function checkMcpToolResolves(config, silent, adapter, scanRoot) {
|
|
|
4276
4338
|
if (found.length > 0 && !silent) {
|
|
4277
4339
|
console.log("\nMCP tool-resolution check:\n");
|
|
4278
4340
|
for (const issue of found) {
|
|
4279
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.path}: ${issue.message}`);
|
|
4280
|
-
|
|
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));
|
|
4281
4343
|
}
|
|
4282
4344
|
}
|
|
4283
4345
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
@@ -4318,7 +4380,7 @@ async function checkCoverageThresholds(excludes, coverage, config, silent) {
|
|
|
4318
4380
|
// Load all claude specs so coverage doesn't depend on a built dist/.
|
|
4319
4381
|
const loaded = await Promise.all(findSpecs(excludes).map(loadSpec));
|
|
4320
4382
|
const claudeSpecs = loaded.filter((s) => s?._specType === "claude");
|
|
4321
|
-
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);
|
|
4322
4384
|
const ok = metric.passing;
|
|
4323
4385
|
if (!ok)
|
|
4324
4386
|
failing++;
|
|
@@ -4830,20 +4892,35 @@ function resolveRecords(cwd, runs, tier, harnessFlag) {
|
|
|
4830
4892
|
return [...names];
|
|
4831
4893
|
}
|
|
4832
4894
|
const recordsConfig = (0, validate_js_1.loadConfig)();
|
|
4833
|
-
const
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
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];
|
|
4840
4917
|
});
|
|
4841
4918
|
return (0, coverage_artifact_js_1.recordsFrom)({
|
|
4842
4919
|
runs,
|
|
4843
|
-
surfaces
|
|
4920
|
+
surfaces,
|
|
4844
4921
|
tier,
|
|
4845
4922
|
at: new Date().toISOString(),
|
|
4846
|
-
selfNamespaces: selfNamespaces
|
|
4923
|
+
selfNamespaces: [...new Set(bundles.flatMap(selfNamespaces))],
|
|
4847
4924
|
// The root an ABSOLUTE command ref must lie beneath to be OURS. Without it a
|
|
4848
4925
|
// harness that executed `/tmp/fixture/hooks/pre.sh` credited this repo's own
|
|
4849
4926
|
// `hooks/pre.sh`, because the tail matched. See the ladder note on
|
|
@@ -4945,7 +5022,7 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
|
4945
5022
|
}
|
|
4946
5023
|
// A script under an excluded path is not DISCOVERED (a vendored corpus's own
|
|
4947
5024
|
// harness must not run as ours), but a script you NAME still runs (#192).
|
|
4948
|
-
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);
|
|
4949
5026
|
// `--min=N`: a CI gate asserts at least N scripts actually LOADED — so a bad
|
|
4950
5027
|
// path, a renamed file, a glob that matched nothing, or a file that matched and
|
|
4951
5028
|
// never linked fails LOUD instead of passing green with nothing executed.
|
|
@@ -5648,7 +5725,7 @@ async function handleHookRuntime(kind, restArgs) {
|
|
|
5648
5725
|
actionHookCommand();
|
|
5649
5726
|
return;
|
|
5650
5727
|
case "refs":
|
|
5651
|
-
refsHookCommand();
|
|
5728
|
+
await refsHookCommand();
|
|
5652
5729
|
return;
|
|
5653
5730
|
case "eval-lock-nudge":
|
|
5654
5731
|
evalLockNudgeHookCommand();
|
|
@@ -5768,17 +5845,31 @@ function evalLockNudgeHookCommand() {
|
|
|
5768
5845
|
// heard nothing at all, while a repo that already tests got reminded. That is
|
|
5769
5846
|
// backwards, and `untested-skill` already stated the missing half correctly;
|
|
5770
5847
|
// it just lived in `vigiles lint`, which someone has to run by hand.
|
|
5771
|
-
|
|
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), {
|
|
5772
5868
|
...options,
|
|
5773
|
-
basePath:
|
|
5869
|
+
basePath: home,
|
|
5870
|
+
root: cwd,
|
|
5774
5871
|
layout,
|
|
5775
|
-
|
|
5776
|
-
// repo-wide exclude is the floor (#192) — the nudge must not report a
|
|
5777
|
-
// vendored skill the linter would never list.
|
|
5778
|
-
exclude: [
|
|
5779
|
-
...(options.exclude ?? []),
|
|
5780
|
-
...(0, exclude_js_1.excludeSet)(cwd, config.exclude).ignore,
|
|
5781
|
-
],
|
|
5872
|
+
excludes,
|
|
5782
5873
|
}) ?? (0, eval_lock_js_1.evalLockNudge)(target, (0, node_path_1.resolve)(cwd, eval_lock_js_1.DEFAULT_LOCK_DIR));
|
|
5783
5874
|
if (!msg)
|
|
5784
5875
|
return;
|
|
@@ -5797,7 +5888,7 @@ function evalLockNudgeHookCommand() {
|
|
|
5797
5888
|
* agent mark its references, at write time, with full context. `vigiles:ignore`
|
|
5798
5889
|
* opts a prose span out.
|
|
5799
5890
|
*/
|
|
5800
|
-
function refsHookCommand() {
|
|
5891
|
+
async function refsHookCommand() {
|
|
5801
5892
|
let raw = "";
|
|
5802
5893
|
try {
|
|
5803
5894
|
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
@@ -5830,7 +5921,7 @@ function refsHookCommand() {
|
|
|
5830
5921
|
catch {
|
|
5831
5922
|
return;
|
|
5832
5923
|
}
|
|
5833
|
-
const issues = (0, refs_js_1.collectRefIssues)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)));
|
|
5924
|
+
const issues = await (0, refs_js_1.collectRefIssues)(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, file)));
|
|
5834
5925
|
const action = (0, refs_js_1.refsHookAction)(issues.length, severity);
|
|
5835
5926
|
if (action === "ok")
|
|
5836
5927
|
return;
|
|
@@ -6735,9 +6826,12 @@ async function main() {
|
|
|
6735
6826
|
process.exitCode = 2;
|
|
6736
6827
|
return;
|
|
6737
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);
|
|
6738
6832
|
const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
|
|
6739
6833
|
sharedDirs: config.sharedDirs,
|
|
6740
|
-
sharedDirsRoot:
|
|
6834
|
+
sharedDirsRoot: frame.root,
|
|
6741
6835
|
// `.vigilesrc.json#exclude` reaches surface DISCOVERY, not just the
|
|
6742
6836
|
// instruction file. Measured 2026-09-21 before this line existed: a repo
|
|
6743
6837
|
// with `{"exclude": [".claude"]}` and one skill at `.claude/skills/demo`
|
|
@@ -6772,7 +6866,16 @@ async function main() {
|
|
|
6772
6866
|
// `init` adopts (layout-driven instruction file + skill/subagent sweep).
|
|
6773
6867
|
// Surfaced in the AuditReport (the report's "Create spec" command-emit
|
|
6774
6868
|
// buttons read it) and the terminal nudge below.
|
|
6775
|
-
|
|
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));
|
|
6776
6879
|
// Read the local flight recorder ONCE — feeds both the JSON report
|
|
6777
6880
|
// (structured summary, the product boundary) and the terminal render.
|
|
6778
6881
|
const ledgerRecords = (0, observe_js_1.readObservations)(root);
|