vigiles 25.1.0 → 26.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/README.md +1 -1
- package/dist/adapters/claude-code/run-scripts.d.ts +5 -2
- package/dist/adapters/claude-code/run-scripts.js +6 -3
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +211 -85
- package/dist/core/adopt.d.ts +50 -1
- package/dist/core/adopt.js +100 -6
- package/dist/core/bash-effects.d.ts +11 -0
- package/dist/core/bash-effects.js +52 -12
- package/dist/core/bash-equivalents.d.ts +18 -0
- package/dist/core/bash-equivalents.js +239 -0
- package/dist/core/compile.js +20 -6
- package/dist/core/coverage.d.ts +3 -3
- package/dist/core/coverage.js +10 -6
- package/dist/core/hook-program.js +48 -2
- package/dist/core/orphans.d.ts +8 -1
- package/dist/core/orphans.js +8 -2
- package/dist/core/types.d.ts +14 -6
- package/dist/exclude.d.ts +25 -0
- package/dist/exclude.js +132 -0
- package/dist/guardrail-check.d.ts +43 -24
- package/dist/guardrail-check.js +57 -0
- package/dist/hook.js +14 -2
- package/dist/test.d.ts +1 -1
- package/dist/test.js +3 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16,10 +16,10 @@ exports.lintTotals = lintTotals;
|
|
|
16
16
|
exports.discoverNestedBundles = discoverNestedBundles;
|
|
17
17
|
const node_fs_1 = require("node:fs");
|
|
18
18
|
const node_path_1 = require("node:path");
|
|
19
|
-
const minimatch_1 = require("minimatch");
|
|
20
19
|
const repo_path_js_1 = require("./core/repo-path.js");
|
|
21
20
|
const node_child_process_1 = require("node:child_process");
|
|
22
21
|
const glob_1 = require("glob");
|
|
22
|
+
const exclude_js_1 = require("./exclude.js");
|
|
23
23
|
const generate_types_js_1 = require("./core/generate-types.js");
|
|
24
24
|
const generate_harness_js_1 = require("./core/generate-harness.js");
|
|
25
25
|
const capability_diff_js_1 = require("./core/capability-diff.js");
|
|
@@ -87,18 +87,29 @@ const load_hook_js_1 = require("./load-hook.js");
|
|
|
87
87
|
// ---------------------------------------------------------------------------
|
|
88
88
|
// Constants
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
90
|
+
// The always-excluded floor (node_modules/dist/.git/.vigiles) lives in
|
|
91
|
+
// src/exclude.ts, folded into every ExcludeSet — no walk carries a private copy.
|
|
91
92
|
// ---------------------------------------------------------------------------
|
|
92
93
|
// Spec loading
|
|
93
94
|
// ---------------------------------------------------------------------------
|
|
94
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Every `*.md.spec.ts` under cwd that `.vigilesrc.json#exclude` does not drop.
|
|
97
|
+
*
|
|
98
|
+
* 🔴 `excludes` IS REQUIRED, NOT DEFAULTED (#192). This function hard-coded its
|
|
99
|
+
* own ignore list for a year while the loaded config's `exclude` never reached
|
|
100
|
+
* it — so a repo that excluded a directory of frozen, un-loadable fixtures got
|
|
101
|
+
* "Compilation complete with errors" on every recompile hook. Eight call sites
|
|
102
|
+
* each had the config in scope; none passed it. An optional parameter is how
|
|
103
|
+
* that happens again; a required one makes the ninth call site a tsc error.
|
|
104
|
+
*/
|
|
105
|
+
function findSpecs(excludes, pattern) {
|
|
95
106
|
const glob = pattern ?? "**/*.md.spec.ts";
|
|
96
107
|
return (0, glob_1.globSync)(glob, {
|
|
97
108
|
// `dot: true` so specs that live in a sync tool's source slot (e.g.
|
|
98
109
|
// `.ruler/AGENTS.md.spec.ts`, the redirect target) are discovered by
|
|
99
110
|
// compile/lint/the recompile hook — not just root-level specs.
|
|
100
111
|
dot: true,
|
|
101
|
-
ignore:
|
|
112
|
+
ignore: excludes.globIgnore,
|
|
102
113
|
cwd: process.cwd(),
|
|
103
114
|
});
|
|
104
115
|
}
|
|
@@ -464,16 +475,16 @@ function compileRailwayToFile(spec, specPath, knownAgents) {
|
|
|
464
475
|
return false;
|
|
465
476
|
}
|
|
466
477
|
/** Names of every compiled agent spec in the project — resolves delegate() targets. */
|
|
467
|
-
async function collectAgentNames() {
|
|
478
|
+
async function collectAgentNames(excludes) {
|
|
468
479
|
const names = [];
|
|
469
|
-
for (const p of findSpecs()) {
|
|
480
|
+
for (const p of findSpecs(excludes)) {
|
|
470
481
|
const s = await loadSpec(p);
|
|
471
482
|
if (s && s._specType === "agent")
|
|
472
483
|
names.push(s.name);
|
|
473
484
|
}
|
|
474
485
|
return names;
|
|
475
486
|
}
|
|
476
|
-
async function compile(specPaths, config, opts = {}) {
|
|
487
|
+
async function compile(specPaths, config, excludes, opts = {}) {
|
|
477
488
|
let allValid = true;
|
|
478
489
|
// Parse the declared harness set ONCE (alias-normalized) and feed both the
|
|
479
490
|
// dialect pick and the mirror from it — no re-parsing, no cwd-sniffing in the
|
|
@@ -540,7 +551,7 @@ async function compile(specPaths, config, opts = {}) {
|
|
|
540
551
|
allValid = false;
|
|
541
552
|
}
|
|
542
553
|
else if (spec._specType === "railway") {
|
|
543
|
-
knownAgents ??= await collectAgentNames();
|
|
554
|
+
knownAgents ??= await collectAgentNames(excludes);
|
|
544
555
|
if (!compileRailwayToFile(spec, specPath, knownAgents))
|
|
545
556
|
allValid = false;
|
|
546
557
|
}
|
|
@@ -663,12 +674,12 @@ function check(filePaths, silent = false) {
|
|
|
663
674
|
* Catches spec bloat — rules that likely say the same thing in different words.
|
|
664
675
|
* Uses information-theoretic distance (gzip-based) — no LLM, fully deterministic.
|
|
665
676
|
*/
|
|
666
|
-
async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
|
|
677
|
+
async function findDuplicateRules(excludes, threshold = 0.3, silent = false, scopeFiles) {
|
|
667
678
|
const log = (msg) => {
|
|
668
679
|
if (!silent)
|
|
669
680
|
console.log(msg);
|
|
670
681
|
};
|
|
671
|
-
const allSpecs = findSpecs();
|
|
682
|
+
const allSpecs = findSpecs(excludes);
|
|
672
683
|
// If lint was invoked with explicit file arguments, only scan the specs
|
|
673
684
|
// for those files — otherwise an unrelated duplicate elsewhere in the
|
|
674
685
|
// repo would fail a targeted CI check (e.g. `vigiles lint path/foo.md`).
|
|
@@ -1173,7 +1184,7 @@ function sharedDirsRootFor(scanTarget) {
|
|
|
1173
1184
|
* third-party plugins — and scoring someone else's vendored plugin as if it were
|
|
1174
1185
|
* yours is the false-positive that gets a gate switched off.
|
|
1175
1186
|
*/
|
|
1176
|
-
function discoverNestedBundles(root,
|
|
1187
|
+
function discoverNestedBundles(root, excludes) {
|
|
1177
1188
|
const out = [];
|
|
1178
1189
|
const skip = new Set([
|
|
1179
1190
|
"node_modules",
|
|
@@ -1184,12 +1195,12 @@ function discoverNestedBundles(root, exclude = []) {
|
|
|
1184
1195
|
]);
|
|
1185
1196
|
const isBundle = (dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, ".claude-plugin", "plugin.json")) ||
|
|
1186
1197
|
(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, "skills"));
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
(0, minimatch_1.minimatch)(rel, `${pattern}/**`));
|
|
1198
|
+
// Root-relative to the REPO (excludes.root), not to `root`: `lint some/dir`
|
|
1199
|
+
// still honours a repo-root `exclude`. One predicate for every walk (#192).
|
|
1200
|
+
const excluded = (dir) => excludes.matches((0, node_path_1.relative)(excludes.root, dir));
|
|
1191
1201
|
let entries;
|
|
1192
1202
|
try {
|
|
1203
|
+
// eslint-disable-next-line no-restricted-syntax -- the nested-bundle walk: every entry is filtered by excludes.matches() below
|
|
1193
1204
|
entries = (0, node_fs_1.readdirSync)(root, { withFileTypes: true })
|
|
1194
1205
|
.filter((e) => e.isDirectory() && !skip.has(e.name) && !e.name.startsWith("."))
|
|
1195
1206
|
.map((e) => e.name);
|
|
@@ -1199,7 +1210,7 @@ function discoverNestedBundles(root, exclude = []) {
|
|
|
1199
1210
|
}
|
|
1200
1211
|
for (const name of entries) {
|
|
1201
1212
|
const dir = (0, node_path_1.join)(root, name);
|
|
1202
|
-
if (excluded(
|
|
1213
|
+
if (excluded(dir))
|
|
1203
1214
|
continue;
|
|
1204
1215
|
// A container (`plugins/`) holds bundles; a bundle may also sit directly.
|
|
1205
1216
|
if (isBundle(dir)) {
|
|
@@ -1208,6 +1219,7 @@ function discoverNestedBundles(root, exclude = []) {
|
|
|
1208
1219
|
}
|
|
1209
1220
|
let inner;
|
|
1210
1221
|
try {
|
|
1222
|
+
// eslint-disable-next-line no-restricted-syntax -- the nested-bundle walk: every entry is filtered by excludes.matches() below
|
|
1211
1223
|
inner = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })
|
|
1212
1224
|
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
1213
1225
|
.map((e) => e.name);
|
|
@@ -1217,7 +1229,7 @@ function discoverNestedBundles(root, exclude = []) {
|
|
|
1217
1229
|
}
|
|
1218
1230
|
for (const child of inner) {
|
|
1219
1231
|
const sub = (0, node_path_1.join)(dir, child);
|
|
1220
|
-
if (excluded(
|
|
1232
|
+
if (excluded(sub))
|
|
1221
1233
|
continue;
|
|
1222
1234
|
if (isBundle(sub))
|
|
1223
1235
|
out.push(sub);
|
|
@@ -1266,29 +1278,25 @@ function overBundles(fn, config, silent, adapter, roots) {
|
|
|
1266
1278
|
* Cost is bounded: only specs whose compiled target actually EXISTS are loaded,
|
|
1267
1279
|
* so a repo with no specs does no extra work at all.
|
|
1268
1280
|
*/
|
|
1269
|
-
async function checkSpecRefs(config, silent, dialect) {
|
|
1281
|
+
async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
1270
1282
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["spec-refs"]) ?? "error";
|
|
1271
1283
|
if (!sev)
|
|
1272
1284
|
return { issues: 0, errors: 0 };
|
|
1273
1285
|
const found = [];
|
|
1274
|
-
|
|
1286
|
+
let knownAgents;
|
|
1287
|
+
for (const specPath of findSpecs(excludes)) {
|
|
1275
1288
|
const target = specPath.replace(/\.spec\.ts$/, "");
|
|
1276
1289
|
if (!(0, node_fs_1.existsSync)(target))
|
|
1277
1290
|
continue; // never compiled — `compile` reports it
|
|
1278
1291
|
const spec = await loadSpec(specPath);
|
|
1279
|
-
if (!spec
|
|
1292
|
+
if (!spec)
|
|
1280
1293
|
continue;
|
|
1281
1294
|
try {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
maxTokens: config?.maxTokens,
|
|
1288
|
-
maxSectionLines: config?.maxSectionLines,
|
|
1289
|
-
catalogOnly: config?.catalogOnly,
|
|
1290
|
-
linters: config?.linters,
|
|
1291
|
-
});
|
|
1295
|
+
// Railway is the one type whose validation needs the sibling agent names;
|
|
1296
|
+
// collected lazily so a repo with no railway spec never pays for the walk.
|
|
1297
|
+
if (spec._specType === "railway")
|
|
1298
|
+
knownAgents ??= await collectAgentNames(excludes);
|
|
1299
|
+
const errors = specCompileErrors(spec, specPath, dialect, config, knownAgents ?? []);
|
|
1292
1300
|
for (const e of errors)
|
|
1293
1301
|
found.push(`${target}: ${e.message} (from ${specPath})`);
|
|
1294
1302
|
}
|
|
@@ -1307,6 +1315,53 @@ async function checkSpecRefs(config, silent, dialect) {
|
|
|
1307
1315
|
}
|
|
1308
1316
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
1309
1317
|
}
|
|
1318
|
+
/**
|
|
1319
|
+
* The ONE spec-type dispatcher, in the only shape both directions can share:
|
|
1320
|
+
* errors, no writing. `compile` reaches its compilers through the
|
|
1321
|
+
* `compile*ToFile` wrappers (which write and print); `checkSpecRefs` reaches the
|
|
1322
|
+
* SAME compilers through this, because a read must never write.
|
|
1323
|
+
*
|
|
1324
|
+
* Why it exists (#190): `checkSpecRefs` used to call `compileClaude` directly and
|
|
1325
|
+
* skip every other spec type, so a SKILL.md / subagent / railway artifact
|
|
1326
|
+
* committed with a since-deleted reference hashed cleanly against its own header
|
|
1327
|
+
* forever — `lint` green, `compile` red. Measured on this repo:
|
|
1328
|
+
* `examples/SKILL.md.spec.ts` names `skills/enforce-rules-format/SKILL.md`, which
|
|
1329
|
+
* does not exist (the skill lives under `.claude/skills/`), and `lint` reported
|
|
1330
|
+
* `hash valid`.
|
|
1331
|
+
*
|
|
1332
|
+
* A switch rather than a per-type "ref accessor": all four compilers already
|
|
1333
|
+
* return the same `errors: CompileError[]`, so there is nothing to normalize —
|
|
1334
|
+
* only the options differ. Exhaustive over `_specType`, so a FIFTH spec type is
|
|
1335
|
+
* a tsc error here instead of a silent skip, which is the failure this closes.
|
|
1336
|
+
*/
|
|
1337
|
+
function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
1338
|
+
const basePath = process.cwd();
|
|
1339
|
+
switch (spec._specType) {
|
|
1340
|
+
case "claude":
|
|
1341
|
+
return (0, compile_js_1.compileClaude)(spec, {
|
|
1342
|
+
basePath,
|
|
1343
|
+
specFile: specPath,
|
|
1344
|
+
dialect,
|
|
1345
|
+
maxRules: config?.maxRules,
|
|
1346
|
+
maxTokens: config?.maxTokens,
|
|
1347
|
+
maxSectionLines: config?.maxSectionLines,
|
|
1348
|
+
catalogOnly: config?.catalogOnly,
|
|
1349
|
+
linters: config?.linters,
|
|
1350
|
+
}).errors;
|
|
1351
|
+
case "skill":
|
|
1352
|
+
return (0, compile_js_1.compileSkill)(spec, { basePath, specFile: specPath, dialect })
|
|
1353
|
+
.errors;
|
|
1354
|
+
case "agent":
|
|
1355
|
+
return (0, compile_js_1.compileAgent)(spec, { basePath, specFile: specPath, dialect })
|
|
1356
|
+
.errors;
|
|
1357
|
+
case "railway":
|
|
1358
|
+
return (0, compile_js_1.compileRailway)(spec, { specFile: specPath, knownAgents }).errors;
|
|
1359
|
+
default:
|
|
1360
|
+
// A `pipeline` spec compiles through its underlying railway, so it has no
|
|
1361
|
+
// artifact of its own to re-derive refs for.
|
|
1362
|
+
return [];
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1310
1365
|
/**
|
|
1311
1366
|
* Write a compiled artifact. Accepts ONLY a {@link StampedMarkdown}, so a body
|
|
1312
1367
|
* that failed to compile cannot reach the disk — there is no stamp to pass.
|
|
@@ -1319,7 +1374,7 @@ async function checkSpecRefs(config, silent, dialect) {
|
|
|
1319
1374
|
function writeArtifact(outputPath, artifact) {
|
|
1320
1375
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), artifact);
|
|
1321
1376
|
}
|
|
1322
|
-
async function runLint(restArgs, flags, config) {
|
|
1377
|
+
async function runLint(restArgs, flags, excludes, config) {
|
|
1323
1378
|
const summary = flags.includes("--summary");
|
|
1324
1379
|
const json = flags.includes("--json");
|
|
1325
1380
|
const silent = summary || json;
|
|
@@ -1364,7 +1419,7 @@ async function runLint(restArgs, flags, config) {
|
|
|
1364
1419
|
// is the false positive that gets a gate turned off. So the DEFAULT fixes the
|
|
1365
1420
|
// SILENCE, and `bundles: "all"` fixes the COVERAGE — one exit code over the
|
|
1366
1421
|
// whole repo, which is what a CI gate needs.
|
|
1367
|
-
const nestedBundles = discoverNestedBundles(scanRoot,
|
|
1422
|
+
const nestedBundles = discoverNestedBundles(scanRoot, excludes);
|
|
1368
1423
|
const scoreAll = flags.includes("--bundles=all") || config?.bundles === "all";
|
|
1369
1424
|
const lintRoots = scoreAll ? [scanRoot, ...nestedBundles] : [scanRoot];
|
|
1370
1425
|
if (!silent && nestedBundles.length > 0) {
|
|
@@ -1381,7 +1436,7 @@ async function runLint(restArgs, flags, config) {
|
|
|
1381
1436
|
// hand-edit of a compiled subagent slipped past `lint`. A hand-written agent
|
|
1382
1437
|
// (no hash header) stays a no-op, and require-instructions-spec is filename-
|
|
1383
1438
|
// scoped to CLAUDE/AGENTS so agents are never falsely flagged as spec-less.
|
|
1384
|
-
const files = findInstructionFiles(restArgs,
|
|
1439
|
+
const files = findInstructionFiles(restArgs, excludes, adapter.layout.agentDir);
|
|
1385
1440
|
// 1. Verify hashes and structure
|
|
1386
1441
|
if (!silent) {
|
|
1387
1442
|
if (files.length > 0) {
|
|
@@ -1401,15 +1456,15 @@ async function runLint(restArgs, flags, config) {
|
|
|
1401
1456
|
// 2. Coverage gaps (discover)
|
|
1402
1457
|
if (!silent)
|
|
1403
1458
|
console.log("\nLinter rule coverage:\n");
|
|
1404
|
-
const coverage = discover(silent);
|
|
1459
|
+
const coverage = discover(excludes, silent);
|
|
1405
1460
|
// 3. Duplicate rule detection (NCD). Scope to the requested files when
|
|
1406
1461
|
// lint was invoked with explicit paths, so targeted CI checks don't
|
|
1407
1462
|
// fail on unrelated duplicates elsewhere in the repo.
|
|
1408
1463
|
if (!silent)
|
|
1409
1464
|
console.log("\nDuplicate rule detection:\n");
|
|
1410
|
-
const dups = await findDuplicateRules(0.3, silent, restArgs.length > 0 ? files : undefined);
|
|
1465
|
+
const dups = await findDuplicateRules(excludes, 0.3, silent, restArgs.length > 0 ? files : undefined);
|
|
1411
1466
|
// 4. Guidance rule count (strengthen suggestions moved to /strengthen skill)
|
|
1412
|
-
const guidanceCount = await countGuidanceRules(silent);
|
|
1467
|
+
const guidanceCount = await countGuidanceRules(excludes, silent);
|
|
1413
1468
|
// 5. Integrity check (hand-edit detection via SHA-256 hash)
|
|
1414
1469
|
const integritySeverity = config?.rules.integrity ?? "warn";
|
|
1415
1470
|
let integrityErrors = 0;
|
|
@@ -1419,7 +1474,7 @@ async function runLint(restArgs, flags, config) {
|
|
|
1419
1474
|
integrityErrors = checkIntegrityForFiles(files, integritySeverity, silent);
|
|
1420
1475
|
}
|
|
1421
1476
|
// 6. Coverage thresholds (gates CI when severity is "error")
|
|
1422
|
-
const coverageErrors = await checkCoverageThresholds(coverage, config, silent);
|
|
1477
|
+
const coverageErrors = await checkCoverageThresholds(excludes, coverage, config, silent);
|
|
1423
1478
|
// 7. Orphan docs check — OPT-IN (the `orphans` block in .vigilesrc.json is
|
|
1424
1479
|
// the on-switch). "Unreferenced" only means "rot" for a hand-cross-linked
|
|
1425
1480
|
// corpus; on a nav-managed doc site (Docusaurus/MkDocs) the page graph lives
|
|
@@ -1449,6 +1504,10 @@ async function runLint(restArgs, flags, config) {
|
|
|
1449
1504
|
basePath: process.cwd(),
|
|
1450
1505
|
include: orphansCfg.include,
|
|
1451
1506
|
exclude: orphansCfg.exclude,
|
|
1507
|
+
// The repo-wide `exclude` is the FLOOR under the rule's own `exclude`
|
|
1508
|
+
// (union, never override): an excluded corpus is neither an orphan
|
|
1509
|
+
// candidate nor a source of references that keep a doc alive (#192).
|
|
1510
|
+
repoExclude: excludes.ignore,
|
|
1452
1511
|
// Exempt every registered harness's surface files (instruction file,
|
|
1453
1512
|
// SKILL.md, subagents, commands) as orphan candidates — layout-driven so
|
|
1454
1513
|
// core carries no harness literal (see src/core/orphans.ts).
|
|
@@ -1468,8 +1527,8 @@ async function runLint(restArgs, flags, config) {
|
|
|
1468
1527
|
// hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
|
|
1469
1528
|
// A compiled artifact's refs, re-derived from its spec — the hash says the file
|
|
1470
1529
|
// is unchanged, not that what it names still exists (#173).
|
|
1471
|
-
const specRefs = await checkSpecRefs(config, silent, adapter.dialect);
|
|
1472
|
-
const untested = overBundles(checkUntestedSurfaces, config, silent, adapter, lintRoots);
|
|
1530
|
+
const specRefs = await checkSpecRefs(excludes, config, silent, adapter.dialect);
|
|
1531
|
+
const untested = overBundles((c, s, a, r) => checkUntestedSurfaces(excludes, c, s, a, r), config, silent, adapter, lintRoots);
|
|
1473
1532
|
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
1474
1533
|
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
1475
1534
|
// subagents. Off by default unless a severity is configured; warning surfaces
|
|
@@ -1572,7 +1631,7 @@ async function runLint(restArgs, flags, config) {
|
|
|
1572
1631
|
// whose exit code is discarded gates nothing — after which hand-written CI steps
|
|
1573
1632
|
// grew to do the gating instead. One unpassed argument, that whole chain.
|
|
1574
1633
|
const docRefReport = docRefSeverity
|
|
1575
|
-
? (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd(), ignore:
|
|
1634
|
+
? (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd(), ignore: excludes.ignore })
|
|
1576
1635
|
: {
|
|
1577
1636
|
filesScanned: 0,
|
|
1578
1637
|
filesIgnored: 0,
|
|
@@ -1744,10 +1803,10 @@ function printLintSummary(report) {
|
|
|
1744
1803
|
console.log(`vigiles: ${parts.join(" / ")}`);
|
|
1745
1804
|
}
|
|
1746
1805
|
}
|
|
1747
|
-
function collectDocumentedRules() {
|
|
1806
|
+
function collectDocumentedRules(excludes) {
|
|
1748
1807
|
const documented = new Set();
|
|
1749
1808
|
const mdFiles = (0, glob_1.globSync)("**/CLAUDE.md", {
|
|
1750
|
-
ignore:
|
|
1809
|
+
ignore: excludes.globIgnore,
|
|
1751
1810
|
cwd: process.cwd(),
|
|
1752
1811
|
});
|
|
1753
1812
|
for (const mdFile of mdFiles) {
|
|
@@ -1789,14 +1848,14 @@ function printLinterCoverage(linter, documentedRules, silent = false) {
|
|
|
1789
1848
|
log("");
|
|
1790
1849
|
return { enabled: linter.rules.length, documented: documented.length };
|
|
1791
1850
|
}
|
|
1792
|
-
function discover(silent = false) {
|
|
1851
|
+
function discover(excludes, silent = false) {
|
|
1793
1852
|
const log = (msg) => {
|
|
1794
1853
|
if (!silent)
|
|
1795
1854
|
console.log(msg);
|
|
1796
1855
|
};
|
|
1797
1856
|
log("Scanning project for linter rules...\n");
|
|
1798
1857
|
const result = (0, generate_types_js_1.generateTypes)({ basePath: process.cwd() });
|
|
1799
|
-
const documentedRules = collectDocumentedRules();
|
|
1858
|
+
const documentedRules = collectDocumentedRules(excludes);
|
|
1800
1859
|
log("Detected linters:\n");
|
|
1801
1860
|
let totalEnabled = 0;
|
|
1802
1861
|
let totalDocumented = 0;
|
|
@@ -1889,6 +1948,7 @@ function discoverAdoptableSurfaces(cwd) {
|
|
|
1889
1948
|
const abs = (0, node_path_1.resolve)(cwd, root);
|
|
1890
1949
|
if (!(0, node_fs_1.existsSync)(abs))
|
|
1891
1950
|
continue;
|
|
1951
|
+
// eslint-disable-next-line no-restricted-syntax -- init's shallow adoptable-surface sweep, top level only (exclude.ts exceptions)
|
|
1892
1952
|
for (const e of (0, node_fs_1.readdirSync)(abs, { withFileTypes: true })) {
|
|
1893
1953
|
const rel = `${root}/${e.name}/SKILL.md`;
|
|
1894
1954
|
if (e.isDirectory() && unspecced(rel))
|
|
@@ -1899,6 +1959,7 @@ function discoverAdoptableSurfaces(cwd) {
|
|
|
1899
1959
|
const abs = (0, node_path_1.resolve)(cwd, root);
|
|
1900
1960
|
if (!(0, node_fs_1.existsSync)(abs))
|
|
1901
1961
|
continue;
|
|
1962
|
+
// eslint-disable-next-line no-restricted-syntax -- init's shallow adoptable-surface sweep, top level only (exclude.ts exceptions)
|
|
1902
1963
|
for (const e of (0, node_fs_1.readdirSync)(abs, { withFileTypes: true })) {
|
|
1903
1964
|
const rel = `${root}/${e.name}`;
|
|
1904
1965
|
if (e.isFile() && e.name.endsWith(".md") && unspecced(rel))
|
|
@@ -1970,6 +2031,7 @@ const RULE_INVENTORY_SKIP_DIRS = new Set([
|
|
|
1970
2031
|
/** readdir that returns [] instead of throwing (perms, races). */
|
|
1971
2032
|
function safeReaddir(dir) {
|
|
1972
2033
|
try {
|
|
2034
|
+
// eslint-disable-next-line no-restricted-syntax -- lint-config collection for the linter catalog, not repo policing (exclude.ts exceptions)
|
|
1973
2035
|
return (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
1974
2036
|
}
|
|
1975
2037
|
catch {
|
|
@@ -2011,11 +2073,11 @@ function collectLintConfigText(root) {
|
|
|
2011
2073
|
* file(s) + lint-config TEXT (never executed) and map documented intents to
|
|
2012
2074
|
* off-the-shelf rules + whether they're already configured. Best-effort, fs-only;
|
|
2013
2075
|
* NO model, NO config execution — safe on any repo. Composition-root. */
|
|
2014
|
-
function computeRuleInventory(root, instructionFile) {
|
|
2076
|
+
function computeRuleInventory(root, instructionFile, excludes) {
|
|
2015
2077
|
try {
|
|
2016
2078
|
// The inventory maps intents → rules; it has no per-file line provenance, so
|
|
2017
2079
|
// the concatenated text is fine here (unlike the routing preview below).
|
|
2018
|
-
const instructionText = gatherInstructionFiles(root, instructionFile)
|
|
2080
|
+
const instructionText = gatherInstructionFiles(root, instructionFile, excludes)
|
|
2019
2081
|
.map((f) => f.text)
|
|
2020
2082
|
.join("\n");
|
|
2021
2083
|
if (!instructionText.trim())
|
|
@@ -2034,7 +2096,7 @@ function computeRuleInventory(root, instructionFile) {
|
|
|
2034
2096
|
* `research/CLAUDE.md`, …), skipping fixture/demo/build/test dirs (`isFixturePath`)
|
|
2035
2097
|
* so a repo's real memory is read without the test-fixture noise. `.claude/` rule
|
|
2036
2098
|
* sources remain a future source. research/rule-enforcer-multilang-design.md §0. */
|
|
2037
|
-
function gatherInstructionFiles(root, instructionFile) {
|
|
2099
|
+
function gatherInstructionFiles(root, instructionFile, excludes) {
|
|
2038
2100
|
const raw = [];
|
|
2039
2101
|
const collect = (rel) => {
|
|
2040
2102
|
const p = (0, node_path_1.resolve)(root, rel);
|
|
@@ -2049,11 +2111,14 @@ function gatherInstructionFiles(root, instructionFile) {
|
|
|
2049
2111
|
// Root instruction files first (stable, deterministic order).
|
|
2050
2112
|
for (const name of new Set([instructionFile, "CLAUDE.md", "AGENTS.md"]))
|
|
2051
2113
|
collect(name);
|
|
2052
|
-
// Nested subdirectory memory, minus fixture/demo/build/test noise.
|
|
2114
|
+
// Nested subdirectory memory, minus fixture/demo/build/test noise. Two
|
|
2115
|
+
// filters, deliberately: `isFixturePath` is the zero-config floor (audit
|
|
2116
|
+
// reads any repo with no .vigilesrc.json), and the configured `exclude` is
|
|
2117
|
+
// unioned ON TOP for the dirs the heuristic cannot guess (#192).
|
|
2053
2118
|
try {
|
|
2054
2119
|
const nested = (0, glob_1.globSync)(["**/CLAUDE.md", "**/AGENTS.md"], {
|
|
2055
2120
|
cwd: root,
|
|
2056
|
-
ignore:
|
|
2121
|
+
ignore: excludes.globIgnore,
|
|
2057
2122
|
})
|
|
2058
2123
|
.filter((rel) => !(0, instruction_sources_js_1.isFixturePath)(rel))
|
|
2059
2124
|
.sort();
|
|
@@ -2111,9 +2176,9 @@ function hasPythonSurface(root) {
|
|
|
2111
2176
|
}
|
|
2112
2177
|
return false;
|
|
2113
2178
|
}
|
|
2114
|
-
function computeRuleRouting(root, instructionFile) {
|
|
2179
|
+
function computeRuleRouting(root, instructionFile, excludes) {
|
|
2115
2180
|
try {
|
|
2116
|
-
const files = gatherInstructionFiles(root, instructionFile);
|
|
2181
|
+
const files = gatherInstructionFiles(root, instructionFile, excludes);
|
|
2117
2182
|
if (files.every((f) => !f.text.trim()))
|
|
2118
2183
|
return undefined;
|
|
2119
2184
|
// Own-repo + consented → enumerate the live rule catalog of whichever
|
|
@@ -2246,16 +2311,23 @@ function scaffoldSpec(args) {
|
|
|
2246
2311
|
logAdoptedSurface(target, specPath, "subagent", unmappedKeys);
|
|
2247
2312
|
}
|
|
2248
2313
|
else {
|
|
2249
|
-
const { source, tier, sectionCount } = (0, adopt_js_1.adoptMarkdown)(md, (0, node_path_1.basename)(target)
|
|
2314
|
+
const { source, tier, sectionCount, adoptedRefs } = (0, adopt_js_1.adoptMarkdown)(md, (0, node_path_1.basename)(target),
|
|
2315
|
+
// RESOLVE-NOW, not a heuristic. The undecidable question is "is this OUR
|
|
2316
|
+
// path or one inside the repo this document DESCRIBES" — the wall that
|
|
2317
|
+
// disabled `doc-refs`. Asking "does it resolve here, right now?" sidesteps
|
|
2318
|
+
// it: a described repo's path does not exist locally, so only already-green
|
|
2319
|
+
// refs are emitted and adoption can never turn a passing file red.
|
|
2320
|
+
{ exists: (ref) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), ref)) });
|
|
2250
2321
|
(0, node_fs_1.writeFileSync)(specAbs, source);
|
|
2251
2322
|
console.log(`Adopted ${target} → ${specPath} (${tier}, ${String(sectionCount)} section${sectionCount === 1 ? "" : "s"}). ` +
|
|
2323
|
+
(adoptedRefs && adoptedRefs.length > 0
|
|
2324
|
+
? `${String(adoptedRefs.length)} reference${adoptedRefs.length === 1 ? "" : "s"} resolved and became verified \`file()\` calls — they now fail the build if the file moves. `
|
|
2325
|
+
: "") +
|
|
2252
2326
|
`Run \`vigiles compile\` and review the diff; the \`/strengthen\` skill upgrades prose to verified rules.`);
|
|
2253
|
-
// Adoption
|
|
2254
|
-
//
|
|
2255
|
-
//
|
|
2256
|
-
//
|
|
2257
|
-
// rather than as not-yet-claimed. Deliberately not a heuristic extractor:
|
|
2258
|
-
// guessing refs out of prose is what got `doc-refs` disabled.
|
|
2327
|
+
// Adoption infers NO RULES (that stays `strengthen`'s job) but it DOES now
|
|
2328
|
+
// extract refs that resolve — measured 2026-08-28 on a 51-skill monorepo,
|
|
2329
|
+
// where extracting nothing meant the cost landed at once (build artifact,
|
|
2330
|
+
// edits into TS) while the payoff waited on a manual pass nobody ran.
|
|
2259
2331
|
if (tier === "raw")
|
|
2260
2332
|
console.log(` ℹ 0 refs extracted — this spec verifies nothing yet. Wrap paths in \`file()\` ` +
|
|
2261
2333
|
`and commands in \`cmd()\` to make \`compile\` check them.`);
|
|
@@ -2401,6 +2473,7 @@ function specReferencedElsewhere(specFile, ejectedFile) {
|
|
|
2401
2473
|
const dir = stack.pop();
|
|
2402
2474
|
let entries;
|
|
2403
2475
|
try {
|
|
2476
|
+
// eslint-disable-next-line no-restricted-syntax -- eject's is-this-spec-compiled-elsewhere safety check; wider is safer (exclude.ts exceptions)
|
|
2404
2477
|
entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
2405
2478
|
}
|
|
2406
2479
|
catch {
|
|
@@ -2904,15 +2977,17 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
2904
2977
|
// run `npm install` yet, so compiling would just error; defer it with a clear
|
|
2905
2978
|
// next step instead of a scary stack-traceless "failed to load".
|
|
2906
2979
|
const canCompile = canResolveVigiles(cwd);
|
|
2980
|
+
const initConfig = (0, validate_js_1.loadConfig)();
|
|
2981
|
+
const excludes = (0, exclude_js_1.excludeSet)(cwd, initConfig.exclude);
|
|
2907
2982
|
const specs = canCompile
|
|
2908
|
-
? findSpecs().filter((s) => {
|
|
2983
|
+
? findSpecs(excludes).filter((s) => {
|
|
2909
2984
|
const tf = (0, node_path_1.resolve)(cwd, s.replace(/\.spec\.ts$/, ""));
|
|
2910
2985
|
return !(0, node_fs_1.existsSync)(tf) || targetHasHash(tf);
|
|
2911
2986
|
})
|
|
2912
2987
|
: [];
|
|
2913
2988
|
if (specs.length > 0) {
|
|
2914
2989
|
console.log("\nCompiling specs...");
|
|
2915
|
-
await compile(specs,
|
|
2990
|
+
await compile(specs, initConfig, excludes);
|
|
2916
2991
|
}
|
|
2917
2992
|
else if (!canCompile) {
|
|
2918
2993
|
// Honest, project-type-aware guidance. A JS repo just needs `npm install`
|
|
@@ -3476,7 +3551,7 @@ function untestedRules(config) {
|
|
|
3476
3551
|
* "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
|
|
3477
3552
|
* untested count plus the severity-gated error count.
|
|
3478
3553
|
*/
|
|
3479
|
-
function checkUntestedSurfaces(config, silent, adapter, scanRoot) {
|
|
3554
|
+
function checkUntestedSurfaces(excludes, config, silent, adapter, scanRoot) {
|
|
3480
3555
|
const { severity: sevFor, anyEnabled, options } = untestedRules(config);
|
|
3481
3556
|
if (!anyEnabled)
|
|
3482
3557
|
return { untested: 0, errors: 0 };
|
|
@@ -3484,6 +3559,10 @@ function checkUntestedSurfaces(config, silent, adapter, scanRoot) {
|
|
|
3484
3559
|
...options,
|
|
3485
3560
|
basePath: scanRoot,
|
|
3486
3561
|
layout: adapter.layout,
|
|
3562
|
+
// The rule's own `exclude` NARROWS; the repo-wide one is the floor under
|
|
3563
|
+
// it. Union, never override — a rule option must not re-admit a vendored
|
|
3564
|
+
// corpus the repo excluded (#192).
|
|
3565
|
+
exclude: [...(options.exclude ?? []), ...excludes.ignore],
|
|
3487
3566
|
});
|
|
3488
3567
|
if (!silent) {
|
|
3489
3568
|
console.log("\nUntested surfaces:\n");
|
|
@@ -4155,7 +4234,7 @@ function checkMcpToolResolves(config, silent, adapter, scanRoot) {
|
|
|
4155
4234
|
* avoids depending on a pre-built `dist/` tree, which the setup-generated
|
|
4156
4235
|
* CI step doesn't guarantee.
|
|
4157
4236
|
*/
|
|
4158
|
-
async function checkCoverageThresholds(coverage, config, silent) {
|
|
4237
|
+
async function checkCoverageThresholds(excludes, coverage, config, silent) {
|
|
4159
4238
|
const severity = (0, types_js_1.ruleSeverity)(config?.rules.coverage);
|
|
4160
4239
|
if (!severity)
|
|
4161
4240
|
return 0;
|
|
@@ -4181,9 +4260,9 @@ async function checkCoverageThresholds(coverage, config, silent) {
|
|
|
4181
4260
|
}
|
|
4182
4261
|
if (opts.scripts !== undefined) {
|
|
4183
4262
|
// Load all claude specs so coverage doesn't depend on a built dist/.
|
|
4184
|
-
const loaded = await Promise.all(findSpecs().map(loadSpec));
|
|
4263
|
+
const loaded = await Promise.all(findSpecs(excludes).map(loadSpec));
|
|
4185
4264
|
const claudeSpecs = loaded.filter((s) => s?._specType === "claude");
|
|
4186
|
-
const metric = (0, coverage_js_1.computeScriptCoverage)(process.cwd(), opts.scripts, claudeSpecs);
|
|
4265
|
+
const metric = (0, coverage_js_1.computeScriptCoverage)(process.cwd(), opts.scripts, claudeSpecs, excludes.ignore);
|
|
4187
4266
|
const ok = metric.passing;
|
|
4188
4267
|
if (!ok)
|
|
4189
4268
|
failing++;
|
|
@@ -4192,8 +4271,8 @@ async function checkCoverageThresholds(coverage, config, silent) {
|
|
|
4192
4271
|
}
|
|
4193
4272
|
return severity === "error" ? failing : 0;
|
|
4194
4273
|
}
|
|
4195
|
-
async function countGuidanceRules(silent = false) {
|
|
4196
|
-
const specs = findSpecs();
|
|
4274
|
+
async function countGuidanceRules(excludes, silent = false) {
|
|
4275
|
+
const specs = findSpecs(excludes);
|
|
4197
4276
|
if (specs.length === 0)
|
|
4198
4277
|
return 0;
|
|
4199
4278
|
let count = 0;
|
|
@@ -4214,7 +4293,22 @@ async function countGuidanceRules(silent = false) {
|
|
|
4214
4293
|
// ---------------------------------------------------------------------------
|
|
4215
4294
|
// Command handlers for main()
|
|
4216
4295
|
// ---------------------------------------------------------------------------
|
|
4217
|
-
|
|
4296
|
+
/**
|
|
4297
|
+
* An explicitly named path is processed even when `exclude` matches it, and ONE
|
|
4298
|
+
* line says so. `exclude` filters DISCOVERY; an argument is an instruction.
|
|
4299
|
+
* Measured 2026-09-03: ripgrep and tsc do this silently, ESLint skips the file
|
|
4300
|
+
* with a warning, prettier skips it and reports "all files use Prettier code
|
|
4301
|
+
* style" — the silent no-op. We take rg/tsc's semantics with ESLint's loudness.
|
|
4302
|
+
* Returns the path unchanged so it composes inside a `.map`.
|
|
4303
|
+
*/
|
|
4304
|
+
function noteExplicitOverride(excludes, path, verb) {
|
|
4305
|
+
const pattern = excludes.explain((0, node_path_1.relative)(excludes.root, (0, node_path_1.resolve)(path)));
|
|
4306
|
+
if (pattern !== null) {
|
|
4307
|
+
console.log(`note: ${path} matches exclude "${pattern}" — ${verb} because you named it`);
|
|
4308
|
+
}
|
|
4309
|
+
return path;
|
|
4310
|
+
}
|
|
4311
|
+
function findInstructionFiles(restArgs, excludes, agentDir = "") {
|
|
4218
4312
|
const patterns = ["**/CLAUDE.md", "**/AGENTS.md", "**/SKILL.md"];
|
|
4219
4313
|
// Include the active harness's subagent dir so a COMPILED `agents/<name>.md`
|
|
4220
4314
|
// (a vigiles-hashed file) is integrity-checked (dogfood E2). Empty for a
|
|
@@ -4222,12 +4316,17 @@ function findInstructionFiles(restArgs, exclude = [], agentDir = "") {
|
|
|
4222
4316
|
if (agentDir !== "")
|
|
4223
4317
|
patterns.push(`**/${agentDir}/*.md`);
|
|
4224
4318
|
// `exclude` (from .vigilesrc.json) drops vendored/benchmark fixtures the repo's
|
|
4225
|
-
// own lint shouldn't police — a third-party CLAUDE.md isn't held to
|
|
4226
|
-
//
|
|
4227
|
-
|
|
4319
|
+
// own lint shouldn't police — a third-party CLAUDE.md isn't held to
|
|
4320
|
+
// require-instructions-spec. The FUNCTION face of the ExcludeSet, not the
|
|
4321
|
+
// string list: `discoverIn` may glob from a subdirectory (`vigiles lint
|
|
4322
|
+
// some/dir`), where a root-relative string pattern would match nothing.
|
|
4228
4323
|
// Discover instruction files under one directory, as paths relative to cwd.
|
|
4229
4324
|
const discoverIn = (dirAbs) => patterns
|
|
4230
|
-
.flatMap((p) => (0, glob_1.globSync)(p, {
|
|
4325
|
+
.flatMap((p) => (0, glob_1.globSync)(p, {
|
|
4326
|
+
ignore: excludes.globIgnore,
|
|
4327
|
+
cwd: dirAbs,
|
|
4328
|
+
absolute: true,
|
|
4329
|
+
}))
|
|
4231
4330
|
.map((abs) => (0, node_path_1.relative)(process.cwd(), abs));
|
|
4232
4331
|
if (restArgs.length === 0)
|
|
4233
4332
|
return discoverIn(process.cwd());
|
|
@@ -4237,6 +4336,7 @@ function findInstructionFiles(restArgs, exclude = [], agentDir = "") {
|
|
|
4237
4336
|
const out = [];
|
|
4238
4337
|
for (const arg of restArgs) {
|
|
4239
4338
|
const abs = (0, node_path_1.resolve)(process.cwd(), arg);
|
|
4339
|
+
noteExplicitOverride(excludes, arg, "linting");
|
|
4240
4340
|
if ((0, node_fs_1.existsSync)(abs) && (0, node_fs_1.lstatSync)(abs).isDirectory()) {
|
|
4241
4341
|
out.push(...discoverIn(abs));
|
|
4242
4342
|
}
|
|
@@ -4674,9 +4774,14 @@ function resolveRecords(cwd, runs, tier, harnessFlag) {
|
|
|
4674
4774
|
}
|
|
4675
4775
|
return [...names];
|
|
4676
4776
|
}
|
|
4777
|
+
const recordsConfig = (0, validate_js_1.loadConfig)();
|
|
4677
4778
|
const scan = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
4678
4779
|
basePath: cwd,
|
|
4679
|
-
layout: harnessLayoutFor(cwd,
|
|
4780
|
+
layout: harnessLayoutFor(cwd, recordsConfig, harnessFlag),
|
|
4781
|
+
// The repo-wide `exclude` only — the per-rule severities/testGlobs stay out
|
|
4782
|
+
// of record resolution on purpose (a run's probes must map to a surface
|
|
4783
|
+
// whether or not the untested-* rule for its kind is on).
|
|
4784
|
+
exclude: (0, exclude_js_1.excludeSet)(cwd, recordsConfig.exclude).ignore,
|
|
4680
4785
|
});
|
|
4681
4786
|
return (0, coverage_artifact_js_1.recordsFrom)({
|
|
4682
4787
|
runs,
|
|
@@ -4754,7 +4859,7 @@ function gitHead(cwd) {
|
|
|
4754
4859
|
return "";
|
|
4755
4860
|
}
|
|
4756
4861
|
}
|
|
4757
|
-
async function handleRunScripts(kind, args, restArgs) {
|
|
4862
|
+
async function handleRunScripts(kind, args, restArgs, excludes) {
|
|
4758
4863
|
const cwd = process.cwd();
|
|
4759
4864
|
// Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
|
|
4760
4865
|
const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
|
|
@@ -4768,7 +4873,9 @@ async function handleRunScripts(kind, args, restArgs) {
|
|
|
4768
4873
|
return;
|
|
4769
4874
|
lockEnv = r;
|
|
4770
4875
|
}
|
|
4771
|
-
|
|
4876
|
+
// A script under an excluded path is not DISCOVERED (a vendored corpus's own
|
|
4877
|
+
// harness must not run as ours), but a script you NAME still runs (#192).
|
|
4878
|
+
const files = (0, run_scripts_js_1.discoverScripts)(restArgs.map((p) => noteExplicitOverride(excludes, p, "running")), defaultGlob, cwd, excludes.ignore);
|
|
4772
4879
|
// `--min=N`: a CI gate asserts at least N scripts actually RAN — so a bad path,
|
|
4773
4880
|
// a renamed file, or a glob that matched nothing fails LOUD instead of passing
|
|
4774
4881
|
// green with zero evals executed. Default 0 (off) keeps local runs ergonomic.
|
|
@@ -5481,8 +5588,18 @@ function evalLockNudgeHookCommand() {
|
|
|
5481
5588
|
// heard nothing at all, while a repo that already tests got reminded. That is
|
|
5482
5589
|
// backwards, and `untested-skill` already stated the missing half correctly;
|
|
5483
5590
|
// it just lived in `vigiles lint`, which someone has to run by hand.
|
|
5484
|
-
const msg = (0, test_coverage_js_1.skillTestNudge)(target, {
|
|
5485
|
-
|
|
5591
|
+
const msg = (0, test_coverage_js_1.skillTestNudge)(target, {
|
|
5592
|
+
...options,
|
|
5593
|
+
basePath: cwd,
|
|
5594
|
+
layout,
|
|
5595
|
+
// Same union `vigiles lint` applies: the rule's exclude narrows, the
|
|
5596
|
+
// repo-wide exclude is the floor (#192) — the nudge must not report a
|
|
5597
|
+
// vendored skill the linter would never list.
|
|
5598
|
+
exclude: [
|
|
5599
|
+
...(options.exclude ?? []),
|
|
5600
|
+
...(0, exclude_js_1.excludeSet)(cwd, config.exclude).ignore,
|
|
5601
|
+
],
|
|
5602
|
+
}) ?? (0, eval_lock_js_1.evalLockNudge)(target, (0, node_path_1.resolve)(cwd, eval_lock_js_1.DEFAULT_LOCK_DIR));
|
|
5486
5603
|
if (!msg)
|
|
5487
5604
|
return;
|
|
5488
5605
|
process.stdout.write(JSON.stringify({
|
|
@@ -6654,6 +6771,10 @@ async function main() {
|
|
|
6654
6771
|
// Shared flags (--max-rules, --catalog-only) override the loaded config so
|
|
6655
6772
|
// every GitHub Action input maps to a real CLI flag. See src/cli-flags.ts.
|
|
6656
6773
|
const config = (0, cli_flags_js_1.applyConfigFlags)((0, validate_js_1.loadConfig)(), args);
|
|
6774
|
+
// `.vigilesrc.json#exclude`, parsed ONCE here and threaded to every walk that
|
|
6775
|
+
// polices the repo (src/exclude.ts). Built where the config is loaded so a
|
|
6776
|
+
// command cannot re-derive it differently — the drift #192 measured.
|
|
6777
|
+
const excludes = (0, exclude_js_1.excludeSet)(process.cwd(), config.exclude);
|
|
6657
6778
|
switch (command) {
|
|
6658
6779
|
// --- Primary commands ---
|
|
6659
6780
|
case "init": {
|
|
@@ -6676,8 +6797,10 @@ async function main() {
|
|
|
6676
6797
|
// a hook program → its harness config + stamp (cohesive-cli-surface). With
|
|
6677
6798
|
// explicit args, partition by extension; bare, discover both.
|
|
6678
6799
|
const specs = restArgs.length > 0
|
|
6679
|
-
? restArgs
|
|
6680
|
-
|
|
6800
|
+
? restArgs
|
|
6801
|
+
.filter((f) => f.endsWith(".spec.ts"))
|
|
6802
|
+
.map((f) => noteExplicitOverride(excludes, f, "compiling"))
|
|
6803
|
+
: findSpecs(excludes);
|
|
6681
6804
|
const hooks = restArgs.length > 0
|
|
6682
6805
|
? restArgs.filter((f) => !f.endsWith(".spec.ts"))
|
|
6683
6806
|
: (0, hook_install_js_1.discoverHookFiles)(process.cwd());
|
|
@@ -6688,7 +6811,8 @@ async function main() {
|
|
|
6688
6811
|
}
|
|
6689
6812
|
let valid = true;
|
|
6690
6813
|
if (specs.length > 0)
|
|
6691
|
-
valid =
|
|
6814
|
+
valid =
|
|
6815
|
+
(await compile(specs, config, excludes, { harnessFlag })) && valid;
|
|
6692
6816
|
valid = (await installHooks(hooks, harnessFlag, config.harness)) && valid;
|
|
6693
6817
|
// Keep an existing whole-harness registry in sync (cheap, opt-in) so the
|
|
6694
6818
|
// user never hand-runs `generate-harness`. Skipped when no harness.gen.ts.
|
|
@@ -6712,7 +6836,7 @@ async function main() {
|
|
|
6712
6836
|
case "lint": {
|
|
6713
6837
|
// lint = verify references + discover + guidance count
|
|
6714
6838
|
const flags = args.slice(1).filter((a) => a.startsWith("--"));
|
|
6715
|
-
const report = await runLint(restArgs, flags, config);
|
|
6839
|
+
const report = await runLint(restArgs, flags, excludes, config);
|
|
6716
6840
|
annotateLintForGitHub(report, flags);
|
|
6717
6841
|
const exitCode = lintExitCode(report);
|
|
6718
6842
|
if (exitCode !== 0) {
|
|
@@ -6721,10 +6845,10 @@ async function main() {
|
|
|
6721
6845
|
break;
|
|
6722
6846
|
}
|
|
6723
6847
|
case "test":
|
|
6724
|
-
await handleRunScripts("test", args, restArgs);
|
|
6848
|
+
await handleRunScripts("test", args, restArgs, excludes);
|
|
6725
6849
|
break;
|
|
6726
6850
|
case "eval":
|
|
6727
|
-
await handleRunScripts("eval", args, restArgs);
|
|
6851
|
+
await handleRunScripts("eval", args, restArgs, excludes);
|
|
6728
6852
|
break;
|
|
6729
6853
|
case "audit": {
|
|
6730
6854
|
// The Lighthouse run: a plain `audit` is a deterministic READ — rings, each
|
|
@@ -6735,7 +6859,9 @@ async function main() {
|
|
|
6735
6859
|
// in `.vigilesrc.json`), headless it stays a read + a one-line nudge. There
|
|
6736
6860
|
// is NO execution flag — automation tests the harness via the vigiles testing
|
|
6737
6861
|
// API + skills, not the report verb. See the `audit-side-effect-free` rule.
|
|
6738
|
-
const dirs = restArgs.length > 0
|
|
6862
|
+
const dirs = restArgs.length > 0
|
|
6863
|
+
? restArgs.map((d) => noteExplicitOverride(excludes, d, "auditing"))
|
|
6864
|
+
: ["."];
|
|
6739
6865
|
const json = args.includes("--json");
|
|
6740
6866
|
// A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
|
|
6741
6867
|
// under one marketplace.json) expands into its members and ranks them.
|
|
@@ -6905,7 +7031,7 @@ async function main() {
|
|
|
6905
7031
|
vigilesVersion: getVersion(),
|
|
6906
7032
|
adoptableSurfaces,
|
|
6907
7033
|
observations: (0, observe_js_1.summarizeObservations)(ledgerRecords),
|
|
6908
|
-
rulesInventory: computeRuleInventory(root, adapter.layout.instructionFile),
|
|
7034
|
+
rulesInventory: computeRuleInventory(root, adapter.layout.instructionFile, excludes),
|
|
6909
7035
|
firingMeasured,
|
|
6910
7036
|
});
|
|
6911
7037
|
const sc = auditReportBase.score;
|
|
@@ -6994,7 +7120,7 @@ async function main() {
|
|
|
6994
7120
|
// computeRuleRouting re-reads) — route the prose rules, enumerating the
|
|
6995
7121
|
// live catalog when consented + own-repo. Feeds the JSON report, the
|
|
6996
7122
|
// written artifacts, and the terminal rule-map summary.
|
|
6997
|
-
const ruleRouting = computeRuleRouting(root, adapter.layout.instructionFile);
|
|
7123
|
+
const ruleRouting = computeRuleRouting(root, adapter.layout.instructionFile, excludes);
|
|
6998
7124
|
const auditReport = ruleRouting
|
|
6999
7125
|
? { ...auditReportBase, ruleRouting }
|
|
7000
7126
|
: auditReportBase;
|