vigiles 38.2.0 → 39.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/action.yml +18 -0
- package/dist/adapters/claude-code/event-capability.js +24 -8
- package/dist/adapters/claude-code/hook-protocol.js +2 -1
- package/dist/adapters/codex/hook-protocol.js +2 -1
- package/dist/audit-report.d.ts +14 -1
- package/dist/audit-report.js +21 -5
- package/dist/audit-report.template.html +33 -28
- package/dist/audit-score.d.ts +4 -1
- package/dist/audit-score.js +3 -1
- package/dist/audit-verdict.d.ts +3 -1
- package/dist/cli-flag-check.d.ts +36 -0
- package/dist/cli-flag-check.js +76 -1
- package/dist/cli-flags.d.ts +1 -1
- package/dist/cli-main.d.ts +2 -2
- package/dist/cli-main.js +684 -434
- package/dist/core/CLAUDE.md.spec.js +2 -2
- package/dist/core/apply-rules.d.ts +71 -0
- package/dist/core/apply-rules.js +48 -0
- package/dist/core/bash-effects.d.ts +0 -57
- package/dist/core/bash-effects.js +0 -147
- package/dist/core/compile-generator.d.ts +6 -6
- package/dist/core/compile-generator.js +4 -4
- package/dist/core/compile.d.ts +72 -8
- package/dist/core/compile.js +276 -133
- package/dist/core/config-migrations.d.ts +69 -0
- package/dist/core/config-migrations.js +145 -0
- package/dist/core/config-schema.d.ts +166 -160
- package/dist/core/config-schema.js +207 -244
- package/dist/core/coverage.d.ts +16 -0
- package/dist/core/coverage.js +22 -0
- package/dist/core/doc-refs.d.ts +3 -2
- package/dist/core/doc-refs.js +2 -2
- package/dist/core/enforce-marker.d.ts +28 -0
- package/dist/core/enforce-marker.js +34 -0
- package/dist/core/eslint-inventory.d.ts +96 -0
- package/dist/core/eslint-inventory.js +251 -0
- package/dist/core/event-capability.d.ts +5 -1
- package/dist/core/frame.d.ts +43 -2
- package/dist/core/frame.js +85 -2
- package/dist/core/generate-schema.d.ts +3 -2
- package/dist/core/generate-schema.js +2 -2
- package/dist/core/generate-types.d.ts +11 -2
- package/dist/core/generate-types.js +12 -5
- package/dist/core/hook-command.d.ts +172 -0
- package/dist/core/hook-command.js +277 -0
- package/dist/core/hook-delivery.d.ts +133 -0
- package/dist/core/hook-delivery.js +150 -0
- package/dist/core/hook-paths.d.ts +14 -0
- package/dist/core/hook-paths.js +22 -0
- package/dist/core/hook-program.d.ts +4 -4
- package/dist/core/hook-program.js +13 -25
- package/dist/core/hook-state.d.ts +1 -1
- package/dist/core/hook-state.js +1 -1
- package/dist/core/instruction-remedies.d.ts +35 -0
- package/dist/core/instruction-remedies.js +70 -1
- package/dist/core/integrity-header.d.ts +54 -0
- package/dist/core/integrity-header.js +90 -0
- package/dist/core/integrity.d.ts +1 -25
- package/dist/core/integrity.js +16 -74
- package/dist/core/linter-adapter.d.ts +28 -4
- package/dist/core/linters.d.ts +8 -1
- package/dist/core/linters.js +116 -81
- package/dist/core/orphans.d.ts +3 -5
- package/dist/core/orphans.js +9 -10
- package/dist/core/presets.d.ts +31 -34
- package/dist/core/presets.js +25 -38
- package/dist/core/rule-catalog.d.ts +11 -12
- package/dist/core/rule-catalog.js +25 -94
- package/dist/core/rule.d.ts +213 -0
- package/dist/core/rule.js +37 -0
- package/dist/core/skill-resources.d.ts +85 -15
- package/dist/core/skill-resources.js +63 -37
- package/dist/core/source-refs.d.ts +3 -23
- package/dist/core/source-refs.js +3 -27
- package/dist/core/test-utils.d.ts +13 -0
- package/dist/core/test-utils.js +40 -1
- package/dist/core/types.d.ts +19 -329
- package/dist/core/validate.d.ts +14 -14
- package/dist/core/validate.js +87 -73
- package/dist/coverage-evidence.js +2 -3
- package/dist/deprecated-key-files.d.ts +6 -0
- package/dist/deprecated-key-files.js +7 -6
- package/dist/exclude.d.ts +3 -1
- package/dist/exclude.js +9 -33
- package/dist/harness-assert.d.ts +18 -0
- package/dist/harness-assert.js +38 -0
- package/dist/hook-check-install.js +15 -6
- package/dist/hook-check.d.ts +1 -3
- package/dist/hook-check.js +3 -5
- package/dist/hook-install.d.ts +7 -41
- package/dist/hook-install.js +29 -96
- package/dist/hook-runtime.js +2 -1
- package/dist/load-hook.d.ts +3 -2
- package/dist/load-hook.js +28 -6
- package/dist/optimize.d.ts +2 -1
- package/dist/overrides.d.ts +29 -0
- package/dist/overrides.js +51 -0
- package/dist/plugin-loader.d.ts +23 -0
- package/dist/plugin-loader.js +42 -12
- package/dist/root-globs.d.ts +9 -0
- package/dist/root-globs.js +48 -0
- package/dist/rule-routing.js +6 -3
- package/dist/rules/CLAUDE.md.spec.d.ts +3 -0
- package/dist/rules/CLAUDE.md.spec.js +22 -0
- package/dist/rules/groups.d.ts +32 -0
- package/dist/rules/groups.js +27 -0
- package/dist/rules/index.d.ts +412 -0
- package/dist/{core/rule-meta.js → rules/index.js} +329 -200
- package/dist/rules/skill-resource-resolves.d.ts +36 -0
- package/dist/rules/skill-resource-resolves.js +51 -0
- package/dist/run-hook.d.ts +27 -3
- package/dist/run-hook.js +48 -12
- package/dist/scan-core.d.ts +103 -29
- package/dist/scan-core.js +178 -186
- package/dist/scan-files.d.ts +0 -8
- package/dist/scan-files.js +79 -24
- package/dist/scan.d.ts +23 -28
- package/dist/scan.js +64 -18
- package/dist/score-core.js +3 -9
- package/dist/setup-plan.d.ts +13 -27
- package/dist/setup-plan.js +46 -71
- package/dist/test-coverage.d.ts +3 -8
- package/dist/test-coverage.js +6 -6
- package/dist/test.d.ts +1 -1
- package/dist/vigilesrc.schema.json +1347 -121
- package/package.json +2 -2
- package/packages/eslint-config/index.mjs +61 -0
- package/skills/adopt-spec/SKILL.md +2 -1
- package/dist/core/rule-meta.d.ts +0 -82
package/dist/cli-main.js
CHANGED
|
@@ -32,10 +32,17 @@ const generate_types_js_1 = require("./core/generate-types.js");
|
|
|
32
32
|
const generate_harness_js_1 = require("./core/generate-harness.js");
|
|
33
33
|
const capability_diff_js_1 = require("./core/capability-diff.js");
|
|
34
34
|
const validate_js_1 = require("./core/validate.js");
|
|
35
|
+
const overrides_js_1 = require("./overrides.js");
|
|
36
|
+
const config_schema_js_1 = require("./core/config-schema.js");
|
|
37
|
+
const rule_js_1 = require("./core/rule.js");
|
|
38
|
+
const apply_rules_js_1 = require("./core/apply-rules.js");
|
|
39
|
+
const index_js_1 = require("./rules/index.js");
|
|
35
40
|
const eval_lock_js_1 = require("./eval-lock.js");
|
|
36
41
|
const eval_entry_js_1 = require("./eval-entry.js");
|
|
37
42
|
const zod_1 = require("zod");
|
|
38
43
|
const cli_flags_js_1 = require("./cli-flags.js");
|
|
44
|
+
const config_schema_js_2 = require("./core/config-schema.js");
|
|
45
|
+
const presets_js_1 = require("./core/presets.js");
|
|
39
46
|
const cli_commands_js_1 = require("./cli-commands.js");
|
|
40
47
|
const cli_flag_check_js_1 = require("./cli-flag-check.js");
|
|
41
48
|
const setup_plan_js_1 = require("./setup-plan.js");
|
|
@@ -69,6 +76,7 @@ const instruction_sources_js_1 = require("./instruction-sources.js");
|
|
|
69
76
|
const rule_catalog_js_1 = require("./core/rule-catalog.js");
|
|
70
77
|
const adoptability_js_1 = require("./adoptability.js");
|
|
71
78
|
const compile_js_1 = require("./core/compile.js");
|
|
79
|
+
const enforce_marker_js_1 = require("./core/enforce-marker.js");
|
|
72
80
|
const proofs_js_1 = require("./core/proofs.js");
|
|
73
81
|
const inline_js_1 = require("./core/inline.js");
|
|
74
82
|
const frontmatter_js_1 = require("./core/frontmatter.js");
|
|
@@ -79,6 +87,7 @@ const action_gate_js_1 = require("./action-gate.js");
|
|
|
79
87
|
const guards_js_1 = require("./core/guards.js");
|
|
80
88
|
const hook_program_js_1 = require("./core/hook-program.js");
|
|
81
89
|
const hook_install_js_1 = require("./hook-install.js");
|
|
90
|
+
const hook_command_js_1 = require("./core/hook-command.js");
|
|
82
91
|
const hook_check_install_js_1 = require("./hook-check-install.js");
|
|
83
92
|
const hook_providers_js_1 = require("./core/hook-providers.js");
|
|
84
93
|
const toml_1 = require("@iarna/toml");
|
|
@@ -96,8 +105,25 @@ const skill_runtime_js_1 = require("./adapters/claude-code/skill-runtime.js");
|
|
|
96
105
|
const linters_js_1 = require("./core/linters.js");
|
|
97
106
|
const harness_test_js_1 = require("./harness-test.js");
|
|
98
107
|
const run_scripts_js_1 = require("./adapters/claude-code/run-scripts.js");
|
|
108
|
+
/**
|
|
109
|
+
* THE RULE SET, WIRED IN — the composition root's one job for rules. Core
|
|
110
|
+
* declares the port and takes the set as a parameter; this file (and the
|
|
111
|
+
* browser demo's entry) is the only place `src/rules/` is imported. `CONFIG`
|
|
112
|
+
* is `.vigilesrc.json` built for this set, `loadConfig` the loader bound to it
|
|
113
|
+
* (so every verb reads the same schema), and `INIT_POLICY` what `init` writes,
|
|
114
|
+
* derived from the groups.
|
|
115
|
+
*/
|
|
116
|
+
const CONFIG = (0, config_schema_js_1.configFor)(index_js_1.RULES, index_js_1.RULE_GROUPS);
|
|
117
|
+
const INIT_POLICY = (0, rule_js_1.initPolicyOf)(index_js_1.RULES, index_js_1.RULE_GROUPS);
|
|
118
|
+
/** Every rule at its shipped default — what a missing `.vigilesrc.json` loads as. */
|
|
119
|
+
const DEFAULT_CONFIG = (0, validate_js_1.configDefaults)(CONFIG);
|
|
120
|
+
/** `.vigilesrc.json` for this rule set; see `core/validate.ts` `loadConfig`. */
|
|
121
|
+
function loadConfig(searchFrom, opts) {
|
|
122
|
+
return (0, validate_js_1.loadConfig)(CONFIG, searchFrom, opts);
|
|
123
|
+
}
|
|
99
124
|
const coverage_artifact_js_1 = require("./coverage-artifact.js");
|
|
100
125
|
const integrity_js_1 = require("./core/integrity.js");
|
|
126
|
+
const instruction_remedies_js_1 = require("./core/instruction-remedies.js");
|
|
101
127
|
const adopt_js_1 = require("./core/adopt.js");
|
|
102
128
|
const coverage_js_1 = require("./core/coverage.js");
|
|
103
129
|
const orphans_js_1 = require("./core/orphans.js");
|
|
@@ -343,6 +369,27 @@ function printErrors(specFile, errors) {
|
|
|
343
369
|
console.log(`::error file=${specFile}::${err.message}`);
|
|
344
370
|
}
|
|
345
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* The rule count line, split by what compile VERIFIED (#351): "linter-verified"
|
|
374
|
+
* counts only references rendered "**Enforced by:**"; every other `enforce()` was
|
|
375
|
+
* rendered "**Claimed (unverified):**" and gets one line saying why and what
|
|
376
|
+
* turns it into "Enforced by". Never changes the exit code.
|
|
377
|
+
*/
|
|
378
|
+
function printRuleSummary(specFile, ruleCount, verdicts) {
|
|
379
|
+
const unverified = verdicts.flatMap((v) => v.verdict.kind === "unverified"
|
|
380
|
+
? [{ ref: v.ref, reason: v.verdict.reason }]
|
|
381
|
+
: []);
|
|
382
|
+
const verified = verdicts.length - unverified.length;
|
|
383
|
+
const claimed = unverified.length > 0
|
|
384
|
+
? `, ${String(unverified.length)} claimed unverified`
|
|
385
|
+
: "";
|
|
386
|
+
console.log(` ${String(ruleCount)} rules (${String(verified)} linter-verified${claimed})`);
|
|
387
|
+
unverified.forEach((u) => {
|
|
388
|
+
const advice = (0, compile_js_1.unverifiedAdvice)(u.ref, u.reason);
|
|
389
|
+
console.log(` ⚠ Claimed (unverified) — ${advice}`);
|
|
390
|
+
console.log(`::warning file=${specFile}::${advice}`);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
346
393
|
/** Non-blocking advisories — printed, but never fail the compile. */
|
|
347
394
|
function printWarnings(specFile, warnings) {
|
|
348
395
|
for (const w of warnings) {
|
|
@@ -355,10 +402,10 @@ function printWarnings(specFile, warnings) {
|
|
|
355
402
|
// Commands
|
|
356
403
|
// ---------------------------------------------------------------------------
|
|
357
404
|
/** Compile a generator-skill spec from source → SKILL.md. Returns validity. */
|
|
358
|
-
function compileGeneratorSkillToFile(specPath, source) {
|
|
405
|
+
function compileGeneratorSkillToFile(root, specPath, source) {
|
|
359
406
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
360
407
|
const { artifact, errors } = (0, compile_generator_js_1.compileGeneratorSkill)(source, {
|
|
361
|
-
basePath:
|
|
408
|
+
basePath: root,
|
|
362
409
|
specFile: specPath,
|
|
363
410
|
});
|
|
364
411
|
// Written only when the compile is clean: `artifact` is null otherwise, and
|
|
@@ -375,9 +422,9 @@ function compileGeneratorSkillToFile(specPath, source) {
|
|
|
375
422
|
return false;
|
|
376
423
|
}
|
|
377
424
|
/** Compile a ClaudeSpec → its primary + any additional targets. */
|
|
378
|
-
async function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
379
|
-
const basePath =
|
|
380
|
-
const { markdown, errors, warnings,
|
|
425
|
+
async function compileClaudeToFile(root, spec, specPath, config, dialect) {
|
|
426
|
+
const basePath = root;
|
|
427
|
+
const { markdown, errors, warnings, enforceVerdicts, outputs } = await (0, compile_js_1.compileClaude)(spec, {
|
|
381
428
|
basePath,
|
|
382
429
|
specFile: specPath,
|
|
383
430
|
dialect,
|
|
@@ -386,7 +433,7 @@ async function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
|
386
433
|
maxSectionLines: config.maxSectionLines,
|
|
387
434
|
catalogOnly: config.catalogOnly,
|
|
388
435
|
linters: config.linters,
|
|
389
|
-
...compileBudgetOptions(config),
|
|
436
|
+
...compileBudgetOptions(config, specPath.replace(/\.spec\.ts$/, "")),
|
|
390
437
|
});
|
|
391
438
|
const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
|
|
392
439
|
// Budget findings print BEFORE the pass/fail line and never change the exit
|
|
@@ -418,20 +465,12 @@ async function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
|
418
465
|
console.log(` → ${primaryOutput} was NOT written; the previous version is left in place.`);
|
|
419
466
|
return false;
|
|
420
467
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
for (const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const dir = primaryOutput.substring(0, primaryOutput.lastIndexOf("/") + 1);
|
|
428
|
-
const targetPath = dir + t;
|
|
429
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, targetPath), (0, compile_js_1.addHash)(body, specPath));
|
|
430
|
-
outputNames.push(targetPath);
|
|
431
|
-
}
|
|
432
|
-
const linterCount = linterResults.filter((r) => r.exists).length;
|
|
433
|
-
console.log(`\n✓ ${specPath} → ${outputNames.join(", ")} (${formatArtifactSize(markdown)})`);
|
|
434
|
-
console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
|
|
468
|
+
// Each target as compile rendered it for its own path: a secondary in
|
|
469
|
+
// another directory has its own links, so it is never a re-headed copy.
|
|
470
|
+
for (const o of outputs)
|
|
471
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, o.path), o.markdown);
|
|
472
|
+
console.log(`\n✓ ${specPath} → ${outputs.map((o) => o.path).join(", ")} (${formatArtifactSize(markdown)})`);
|
|
473
|
+
printRuleSummary(specPath, Object.keys(spec.rules).length, enforceVerdicts);
|
|
435
474
|
return true;
|
|
436
475
|
}
|
|
437
476
|
/**
|
|
@@ -512,10 +551,10 @@ function formatArtifactSize(markdown) {
|
|
|
512
551
|
return `${size} · ~${String(kTokens)}k tokens est.`;
|
|
513
552
|
}
|
|
514
553
|
/** Compile a declarative SkillSpec → SKILL.md. */
|
|
515
|
-
async function compileSkillToFile(spec, specPath, dialect) {
|
|
554
|
+
async function compileSkillToFile(root, spec, specPath, dialect) {
|
|
516
555
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
517
556
|
const { artifact, errors, warnings } = await (0, compile_js_1.compileSkill)(spec, {
|
|
518
|
-
basePath:
|
|
557
|
+
basePath: root,
|
|
519
558
|
specFile: specPath,
|
|
520
559
|
// The SKILL.md frontmatter profile comes from the resolved harness — a Codex
|
|
521
560
|
// repo gets a minimal (name + description) SKILL.md; CC gets the full set.
|
|
@@ -536,12 +575,15 @@ async function compileSkillToFile(spec, specPath, dialect) {
|
|
|
536
575
|
return false;
|
|
537
576
|
}
|
|
538
577
|
/** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
|
|
539
|
-
async function compileAgentToFile(spec, specPath, dialect) {
|
|
578
|
+
async function compileAgentToFile(root, spec, specPath, config, dialect) {
|
|
540
579
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
580
|
+
// The same verify options an instruction file gets (`compileClaudeToFile`).
|
|
541
581
|
const { artifact, errors, warnings } = await (0, compile_js_1.compileAgent)(spec, {
|
|
542
|
-
basePath:
|
|
582
|
+
basePath: root,
|
|
543
583
|
specFile: specPath,
|
|
544
584
|
dialect,
|
|
585
|
+
catalogOnly: config.catalogOnly,
|
|
586
|
+
linters: config.linters,
|
|
545
587
|
});
|
|
546
588
|
// Written only when the compile is clean: `artifact` is null otherwise, and
|
|
547
589
|
// `writeArtifact` takes nothing else.
|
|
@@ -591,6 +633,7 @@ async function collectAgentNames(excludes) {
|
|
|
591
633
|
return names;
|
|
592
634
|
}
|
|
593
635
|
async function compile(specPaths, config, excludes, opts = {}) {
|
|
636
|
+
const root = excludes.root;
|
|
594
637
|
let allValid = true;
|
|
595
638
|
// Parse the declared harness set ONCE (alias-normalized) and feed both the
|
|
596
639
|
// dialect pick and the mirror from it — no re-parsing, no cwd-sniffing in the
|
|
@@ -598,7 +641,7 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
598
641
|
// ambiguous-detection pick.
|
|
599
642
|
const declaredHarnesses = (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses);
|
|
600
643
|
const selection = (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
601
|
-
root
|
|
644
|
+
root,
|
|
602
645
|
flag: opts.harnessFlag,
|
|
603
646
|
configHarness: declaredHarnesses,
|
|
604
647
|
});
|
|
@@ -610,9 +653,9 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
610
653
|
let knownAgents = null;
|
|
611
654
|
for (const specPath of specPaths) {
|
|
612
655
|
// Generator skills can't be executed to markdown — compile from source.
|
|
613
|
-
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(
|
|
656
|
+
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(root, specPath), "utf-8");
|
|
614
657
|
if (/\bgenSkill\s*\(/.test(source)) {
|
|
615
|
-
if (!compileGeneratorSkillToFile(specPath, source))
|
|
658
|
+
if (!compileGeneratorSkillToFile(root, specPath, source))
|
|
616
659
|
allValid = false;
|
|
617
660
|
continue;
|
|
618
661
|
}
|
|
@@ -633,7 +676,7 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
633
676
|
const specDialect = opts.harnessFlag === undefined
|
|
634
677
|
? ((0, adapter_registry_js_1.adapterForInstructionFile)(targetFile)?.dialect ?? dialect)
|
|
635
678
|
: dialect;
|
|
636
|
-
if (await compileClaudeToFile(spec, specPath, config, specDialect)) {
|
|
679
|
+
if (await compileClaudeToFile(root, spec, specPath, config, specDialect)) {
|
|
637
680
|
writeInstructionMirrors(specPath.replace(/\.spec\.ts$/, ""), declaredHarnesses);
|
|
638
681
|
}
|
|
639
682
|
else {
|
|
@@ -649,11 +692,11 @@ async function compile(specPaths, config, excludes, opts = {}) {
|
|
|
649
692
|
for (const w of (0, skill_harness_js_1.skillFrontmatterDropWarnings)(spec, forHarnesses)) {
|
|
650
693
|
console.log(`⚠ ${w}`);
|
|
651
694
|
}
|
|
652
|
-
if (!(await compileSkillToFile(spec, specPath, dialect)))
|
|
695
|
+
if (!(await compileSkillToFile(root, spec, specPath, dialect)))
|
|
653
696
|
allValid = false;
|
|
654
697
|
}
|
|
655
698
|
else if (spec._specType === "agent") {
|
|
656
|
-
if (!(await compileAgentToFile(spec, specPath, dialect)))
|
|
699
|
+
if (!(await compileAgentToFile(root, spec, specPath, config, dialect)))
|
|
657
700
|
allValid = false;
|
|
658
701
|
}
|
|
659
702
|
else if (spec._specType === "railway") {
|
|
@@ -690,14 +733,14 @@ function ghAnnotate(level, message, file, line) {
|
|
|
690
733
|
const loc = locParts.length > 0 ? " " + locParts.join(",") : "";
|
|
691
734
|
console.log(`::${level}${loc}::${message}`);
|
|
692
735
|
}
|
|
693
|
-
function verifyHashes(frame, filePaths, silent = false) {
|
|
736
|
+
function verifyHashes(root, frame, filePaths, silent = false) {
|
|
694
737
|
let errorCount = 0;
|
|
695
738
|
const log = (msg) => {
|
|
696
739
|
if (!silent)
|
|
697
740
|
console.log(msg);
|
|
698
741
|
};
|
|
699
742
|
for (const filePath of filePaths) {
|
|
700
|
-
const fullPath = (0, node_path_1.resolve)(
|
|
743
|
+
const fullPath = (0, node_path_1.resolve)(root, filePath);
|
|
701
744
|
// If the file doesn't exist at all (typo, deleted), that's an error —
|
|
702
745
|
// not a "no hash" informational message. Without this check, a scoped
|
|
703
746
|
// lint like `vigiles lint typo.md` would silently exit clean.
|
|
@@ -727,14 +770,14 @@ function verifyHashes(frame, filePaths, silent = false) {
|
|
|
727
770
|
}
|
|
728
771
|
return { valid: errorCount === 0, errorCount };
|
|
729
772
|
}
|
|
730
|
-
function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
773
|
+
function validateSpecs(root, filePaths, rulesConfig, rules, silent = false) {
|
|
731
774
|
let allValid = true;
|
|
732
775
|
const log = (msg) => {
|
|
733
776
|
if (!silent)
|
|
734
777
|
console.log(msg);
|
|
735
778
|
};
|
|
736
779
|
for (const filePath of filePaths) {
|
|
737
|
-
const fullPath = (0, node_path_1.resolve)(
|
|
780
|
+
const fullPath = (0, node_path_1.resolve)(root, filePath);
|
|
738
781
|
let content;
|
|
739
782
|
try {
|
|
740
783
|
content = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
|
|
@@ -747,16 +790,24 @@ function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
|
747
790
|
const hashMatch = content.match(/<!-- vigiles:sha256:[a-f0-9]+ compiled from (.+) -->/);
|
|
748
791
|
if (hashMatch) {
|
|
749
792
|
// Verify the referenced spec still exists
|
|
750
|
-
const specRef = (0, node_path_1.resolve)(
|
|
793
|
+
const specRef = (0, node_path_1.resolve)(root, hashMatch[1]);
|
|
751
794
|
if (!(0, node_fs_1.existsSync)(specRef)) {
|
|
752
795
|
log(` ✗ [require-instructions-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
|
|
753
796
|
allValid = false;
|
|
754
797
|
}
|
|
755
798
|
continue;
|
|
756
799
|
}
|
|
800
|
+
// The path as discovered (cwd-relative), not `fullPath`: the finding prints
|
|
801
|
+
// it, and its `exclude` snippet must be a root-relative glob.
|
|
757
802
|
const result = (0, validate_js_1.validate)(content, {
|
|
758
|
-
filePath
|
|
759
|
-
rules
|
|
803
|
+
filePath,
|
|
804
|
+
// The two spec-presence rules resolved at THIS file (`overrides`), the
|
|
805
|
+
// rest as configured — validate reads only these two per file.
|
|
806
|
+
rules: {
|
|
807
|
+
...rulesConfig,
|
|
808
|
+
"require-instructions-spec": rules.severityAt("require-instructions-spec", filePath),
|
|
809
|
+
"require-skill-spec": rules.severityAt("require-skill-spec", filePath),
|
|
810
|
+
},
|
|
760
811
|
});
|
|
761
812
|
for (const err of result.errors) {
|
|
762
813
|
log(` ✗ [${err.rule}] ${err.message}`);
|
|
@@ -768,10 +819,13 @@ function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
|
768
819
|
}
|
|
769
820
|
return allValid;
|
|
770
821
|
}
|
|
771
|
-
function check(frame, filePaths, silent = false
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
822
|
+
function check(root, frame, filePaths, silent = false,
|
|
823
|
+
// The run's config when the caller has it — so a `--preset=` the run was
|
|
824
|
+
// given reaches the spec checks too, rather than a second read of the file.
|
|
825
|
+
config) {
|
|
826
|
+
const hashes = verifyHashes(root, frame, filePaths, silent);
|
|
827
|
+
const vConfig = config ?? loadConfig(root);
|
|
828
|
+
const specsValid = validateSpecs(root, filePaths, vConfig.rules, (0, overrides_js_1.ruleResolver)(vConfig.rules, vConfig.overrides), silent);
|
|
775
829
|
return {
|
|
776
830
|
valid: hashes.valid && specsValid,
|
|
777
831
|
hashErrors: hashes.errorCount,
|
|
@@ -787,7 +841,8 @@ function check(frame, filePaths, silent = false) {
|
|
|
787
841
|
* Catches spec bloat — rules that likely say the same thing in different words.
|
|
788
842
|
* Uses information-theoretic distance (gzip-based) — no LLM, fully deterministic.
|
|
789
843
|
*/
|
|
790
|
-
async function findDuplicateRules(excludes, threshold = 0.3, silent = false, scopeFiles) {
|
|
844
|
+
async function findDuplicateRules(excludes, threshold = 0.3, silent = false, scopeFiles, severityAt = () => "warn") {
|
|
845
|
+
const root = excludes.root;
|
|
791
846
|
const log = (msg) => {
|
|
792
847
|
if (!silent)
|
|
793
848
|
console.log(msg);
|
|
@@ -810,28 +865,33 @@ async function findDuplicateRules(excludes, threshold = 0.3, silent = false, sco
|
|
|
810
865
|
for (const f of scopeFiles) {
|
|
811
866
|
let resolved;
|
|
812
867
|
try {
|
|
813
|
-
const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(
|
|
868
|
+
const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(root, f), "utf-8");
|
|
814
869
|
const m = compiledFromRe.exec(content);
|
|
815
870
|
if (m) {
|
|
816
|
-
resolved = (0, node_path_1.resolve)(
|
|
871
|
+
resolved = (0, node_path_1.resolve)(root, m[1].trim());
|
|
817
872
|
}
|
|
818
873
|
}
|
|
819
874
|
catch {
|
|
820
875
|
// File unreadable — fall through to the naming convention
|
|
821
876
|
}
|
|
822
877
|
if (!resolved) {
|
|
823
|
-
resolved = (0, node_path_1.resolve)(
|
|
878
|
+
resolved = (0, node_path_1.resolve)(root, `${f}.spec.ts`);
|
|
824
879
|
}
|
|
825
880
|
wanted.add(resolved);
|
|
826
881
|
}
|
|
827
|
-
return allSpecs.filter((specPath) => wanted.has((0, node_path_1.resolve)(
|
|
882
|
+
return allSpecs.filter((specPath) => wanted.has((0, node_path_1.resolve)(root, specPath)));
|
|
828
883
|
})()
|
|
829
884
|
: allSpecs;
|
|
830
885
|
if (specs.length === 0)
|
|
831
|
-
return { valid: true, pairCount: 0 };
|
|
886
|
+
return { valid: true, pairCount: 0, errorPairs: 0 };
|
|
832
887
|
let totalPairs = 0;
|
|
888
|
+
let errorPairs = 0;
|
|
833
889
|
let specsWithDuplicates = 0;
|
|
834
890
|
for (const specPath of specs) {
|
|
891
|
+
// Per spec: an `overrides` entry may turn `duplicate-rules` off for it.
|
|
892
|
+
const sev = severityAt(specPath);
|
|
893
|
+
if (sev === false)
|
|
894
|
+
continue;
|
|
835
895
|
const spec = await loadSpec(specPath);
|
|
836
896
|
if (!spec || spec._specType !== "claude")
|
|
837
897
|
continue;
|
|
@@ -847,6 +907,8 @@ async function findDuplicateRules(excludes, threshold = 0.3, silent = false, sco
|
|
|
847
907
|
}
|
|
848
908
|
specsWithDuplicates++;
|
|
849
909
|
totalPairs += pairs.length;
|
|
910
|
+
if (sev === "error")
|
|
911
|
+
errorPairs += pairs.length;
|
|
850
912
|
log(` ${specPath}`);
|
|
851
913
|
for (const pair of pairs.slice(0, 5)) {
|
|
852
914
|
log(` ${pair.idA} ↔ ${pair.idB} (distance: ${pair.distance.toFixed(3)})`);
|
|
@@ -857,10 +919,10 @@ async function findDuplicateRules(excludes, threshold = 0.3, silent = false, sco
|
|
|
857
919
|
}
|
|
858
920
|
if (totalPairs === 0) {
|
|
859
921
|
log("No near-duplicate rules detected.");
|
|
860
|
-
return { valid: true, pairCount: 0 };
|
|
922
|
+
return { valid: true, pairCount: 0, errorPairs: 0 };
|
|
861
923
|
}
|
|
862
924
|
log(`\n ${String(totalPairs)} duplicate pair(s) in ${String(specsWithDuplicates)} spec(s). Consider merging or rewording.`);
|
|
863
|
-
return { valid: false, pairCount: totalPairs };
|
|
925
|
+
return { valid: false, pairCount: totalPairs, errorPairs };
|
|
864
926
|
}
|
|
865
927
|
/**
|
|
866
928
|
* Verify the file-qualified symbol references (`path.ext#symbol`) in instruction
|
|
@@ -1005,12 +1067,12 @@ function lintExitCode(report) {
|
|
|
1005
1067
|
report.skillFrontmatterErrors > 0 ||
|
|
1006
1068
|
report.mcpToolErrors > 0 ||
|
|
1007
1069
|
report.hookScriptErrors > 0 ||
|
|
1070
|
+
report.handWrittenHookErrors > 0 ||
|
|
1008
1071
|
report.disallowedToolErrors > 0 ||
|
|
1009
1072
|
report.descriptionOverlapErrors > 0 ||
|
|
1010
1073
|
report.descriptionBudgetErrors > 0 ||
|
|
1011
1074
|
report.frontmatterValidErrors > 0 ||
|
|
1012
1075
|
report.mcpHookErrors > 0 ||
|
|
1013
|
-
report.preferCompiledHookErrors > 0 ||
|
|
1014
1076
|
report.lethalTrifectaErrors > 0 ||
|
|
1015
1077
|
report.skillResourceErrors > 0 ||
|
|
1016
1078
|
report.skillFenceErrors > 0 ||
|
|
@@ -1338,6 +1400,31 @@ function discoverNestedBundles(root, excludes) {
|
|
|
1338
1400
|
}
|
|
1339
1401
|
return out.sort();
|
|
1340
1402
|
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Each finding with the severity `rule` has at its path (`overrides`
|
|
1405
|
+
* applied), the ones an override turns off removed. `pathOf` returns
|
|
1406
|
+
* `undefined` for a finding about the repository as a whole, which keeps the
|
|
1407
|
+
* severity `rules` gives it.
|
|
1408
|
+
*/
|
|
1409
|
+
function graded(rules, rule, found, pathOf) {
|
|
1410
|
+
return found.flatMap((f) => {
|
|
1411
|
+
const sev = rules.severityAt(rule, pathOf(f));
|
|
1412
|
+
return sev === false ? [] : [{ ...f, sev }];
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
/** The `{ issues, errors }` a per-surface check returns, from its graded findings. */
|
|
1416
|
+
function tally(found) {
|
|
1417
|
+
return {
|
|
1418
|
+
issues: found.length,
|
|
1419
|
+
errors: found.filter((f) => f.sev === "error").length,
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
/** The terminal mark and annotation level for a severity. */
|
|
1423
|
+
function markOf(sev) {
|
|
1424
|
+
return sev === "error"
|
|
1425
|
+
? { mark: "✗", level: "error" }
|
|
1426
|
+
: { mark: "⚠", level: "warning" };
|
|
1427
|
+
}
|
|
1341
1428
|
/**
|
|
1342
1429
|
* Run one per-surface check over EVERY bundle and sum its counters.
|
|
1343
1430
|
*
|
|
@@ -1397,13 +1484,13 @@ function overBundles(fn, run, bundles) {
|
|
|
1397
1484
|
* Cost is bounded: only specs whose compiled target actually EXISTS are loaded,
|
|
1398
1485
|
* so a repo with no specs does no extra work at all.
|
|
1399
1486
|
*/
|
|
1400
|
-
async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
1401
|
-
|
|
1402
|
-
//
|
|
1403
|
-
//
|
|
1404
|
-
//
|
|
1405
|
-
|
|
1406
|
-
if (!
|
|
1487
|
+
async function checkSpecRefs(excludes, config, rules, silent, dialect) {
|
|
1488
|
+
// Both rules are resolved per compiled target below (`overrides`). The scan
|
|
1489
|
+
// is skipped only when neither can fire anywhere. `compile-budget` is
|
|
1490
|
+
// reported here only at "error": at its default it is a compile-time
|
|
1491
|
+
// warning, and `lint` printed nothing about size before the rule had a name
|
|
1492
|
+
// — the default output does not change.
|
|
1493
|
+
if (!rules.mayFire("spec-refs") && !rules.mayFire("compile-budget"))
|
|
1407
1494
|
return { issues: 0, errors: 0, budgetErrors: 0 };
|
|
1408
1495
|
const derived = [];
|
|
1409
1496
|
let knownAgents;
|
|
@@ -1422,7 +1509,12 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
|
1422
1509
|
derived.push({
|
|
1423
1510
|
target,
|
|
1424
1511
|
specPath,
|
|
1425
|
-
findings: await specCompileErrors(spec, specPath,
|
|
1512
|
+
findings: await specCompileErrors(spec, specPath, {
|
|
1513
|
+
root: excludes.root,
|
|
1514
|
+
dialect,
|
|
1515
|
+
config,
|
|
1516
|
+
knownAgents: knownAgents ?? [],
|
|
1517
|
+
}),
|
|
1426
1518
|
});
|
|
1427
1519
|
}
|
|
1428
1520
|
catch {
|
|
@@ -1431,50 +1523,32 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
|
|
|
1431
1523
|
continue;
|
|
1432
1524
|
}
|
|
1433
1525
|
}
|
|
1434
|
-
const messages = (pick) => derived.flatMap((d) => pick(d.findings).map((e) =>
|
|
1435
|
-
|
|
1436
|
-
|
|
1526
|
+
const messages = (pick) => derived.flatMap((d) => pick(d.findings).map((e) => ({
|
|
1527
|
+
target: d.target,
|
|
1528
|
+
msg: `${d.target}: ${e.message} (from ${d.specPath})`,
|
|
1529
|
+
})));
|
|
1530
|
+
const found = graded(rules, "spec-refs", messages((f) => f.errors), (m) => m.target);
|
|
1531
|
+
const overBudget = graded(rules, "compile-budget", messages((f) => f.budget), (m) => m.target).filter((m) => m.sev === "error");
|
|
1437
1532
|
if (found.length > 0 && !silent) {
|
|
1438
1533
|
console.log("\nSpec reference check:\n");
|
|
1439
|
-
for (const msg of found) {
|
|
1440
|
-
console.log(` ${sev
|
|
1441
|
-
ghAnnotate(sev
|
|
1534
|
+
for (const { msg, sev } of found) {
|
|
1535
|
+
console.log(` ${markOf(sev).mark} ${msg}`);
|
|
1536
|
+
ghAnnotate(markOf(sev).level, msg);
|
|
1442
1537
|
}
|
|
1443
1538
|
}
|
|
1444
1539
|
if (overBudget.length > 0 && !silent) {
|
|
1445
1540
|
console.log("\nCompile budget check (compile-budget):\n");
|
|
1446
|
-
overBudget.forEach((msg) => {
|
|
1541
|
+
overBudget.forEach(({ msg }) => {
|
|
1447
1542
|
console.log(` ✗ ${msg}`);
|
|
1448
1543
|
ghAnnotate("error", `compile-budget: ${msg}`);
|
|
1449
1544
|
});
|
|
1450
1545
|
}
|
|
1451
|
-
return {
|
|
1452
|
-
issues: found.length,
|
|
1453
|
-
errors: sev === "error" ? found.length : 0,
|
|
1454
|
-
budgetErrors: overBudget.length,
|
|
1455
|
-
};
|
|
1546
|
+
return { ...tally(found), budgetErrors: overBudget.length };
|
|
1456
1547
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
* SAME compilers through this, because a read must never write.
|
|
1462
|
-
*
|
|
1463
|
-
* Why it exists (#190): `checkSpecRefs` used to call `compileClaude` directly and
|
|
1464
|
-
* skip every other spec type, so a SKILL.md / subagent / railway artifact
|
|
1465
|
-
* committed with a since-deleted reference hashed cleanly against its own header
|
|
1466
|
-
* forever — `lint` green, `compile` red. Measured on this repo:
|
|
1467
|
-
* `examples/SKILL.md.spec.ts` names `skills/enforce-rules-format/SKILL.md`, which
|
|
1468
|
-
* does not exist (the skill lives under `.claude/skills/`), and `lint` reported
|
|
1469
|
-
* `hash valid`.
|
|
1470
|
-
*
|
|
1471
|
-
* A switch rather than a per-type "ref accessor": all four compilers already
|
|
1472
|
-
* return the same `errors: CompileError[]`, so there is nothing to normalize —
|
|
1473
|
-
* only the options differ. Exhaustive over `_specType`, so a FIFTH spec type is
|
|
1474
|
-
* a tsc error here instead of a silent skip, which is the failure this closes.
|
|
1475
|
-
*/
|
|
1476
|
-
async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
1477
|
-
const basePath = process.cwd();
|
|
1548
|
+
async function specCompileErrors(spec, specPath, { root, dialect, config, knownAgents }) {
|
|
1549
|
+
const basePath = root;
|
|
1550
|
+
// The compiled file this spec writes — `overrides` resolve at its path.
|
|
1551
|
+
const target = specPath.replace(/\.spec\.ts$/, "");
|
|
1478
1552
|
switch (spec._specType) {
|
|
1479
1553
|
case "claude": {
|
|
1480
1554
|
const r = await (0, compile_js_1.compileClaude)(spec, {
|
|
@@ -1486,7 +1560,7 @@ async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
|
1486
1560
|
maxSectionLines: config?.maxSectionLines,
|
|
1487
1561
|
catalogOnly: config?.catalogOnly,
|
|
1488
1562
|
linters: config?.linters,
|
|
1489
|
-
...compileBudgetOptions(config),
|
|
1563
|
+
...compileBudgetOptions(config, target),
|
|
1490
1564
|
});
|
|
1491
1565
|
// A size finding is `compile-budget`'s, not a dead reference: split by
|
|
1492
1566
|
// IDENTITY against the compiler's own list, never by the type's name.
|
|
@@ -1502,7 +1576,13 @@ async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
|
1502
1576
|
};
|
|
1503
1577
|
case "agent":
|
|
1504
1578
|
return {
|
|
1505
|
-
errors: (await (0, compile_js_1.compileAgent)(spec, {
|
|
1579
|
+
errors: (await (0, compile_js_1.compileAgent)(spec, {
|
|
1580
|
+
basePath,
|
|
1581
|
+
specFile: specPath,
|
|
1582
|
+
dialect,
|
|
1583
|
+
catalogOnly: config?.catalogOnly,
|
|
1584
|
+
linters: config?.linters,
|
|
1585
|
+
})).errors,
|
|
1506
1586
|
budget: [],
|
|
1507
1587
|
};
|
|
1508
1588
|
case "railway":
|
|
@@ -1518,17 +1598,18 @@ async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
|
|
|
1518
1598
|
}
|
|
1519
1599
|
}
|
|
1520
1600
|
/**
|
|
1521
|
-
* The `compile-budget` rule, as `compileClaude` options: its severity
|
|
1522
|
-
*
|
|
1523
|
-
* and `lint` (which re-derives)
|
|
1601
|
+
* The `compile-budget` rule, as `compileClaude` options: its severity at the
|
|
1602
|
+
* compiled `target` (`overrides` applied) and any thresholds `rules` set. The
|
|
1603
|
+
* ONE translation, so `compile` (which writes) and `lint` (which re-derives)
|
|
1604
|
+
* can never measure against different numbers.
|
|
1524
1605
|
*/
|
|
1525
|
-
function compileBudgetOptions(config) {
|
|
1606
|
+
function compileBudgetOptions(config, target) {
|
|
1526
1607
|
const rule = config?.rules?.["compile-budget"];
|
|
1527
1608
|
// No config at all (a programmatic caller, a missing file) is the default.
|
|
1528
1609
|
if (rule === undefined)
|
|
1529
1610
|
return { budgetSeverity: "warn" };
|
|
1530
1611
|
return {
|
|
1531
|
-
budgetSeverity: (0,
|
|
1612
|
+
budgetSeverity: (0, overrides_js_1.ruleResolver)(config?.rules ?? DEFAULT_CONFIG.rules, config?.overrides).severityAt("compile-budget", target),
|
|
1532
1613
|
...(0, types_js_1.ruleOptions)(rule),
|
|
1533
1614
|
};
|
|
1534
1615
|
}
|
|
@@ -1545,6 +1626,8 @@ function writeArtifact(outputPath, artifact) {
|
|
|
1545
1626
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), artifact);
|
|
1546
1627
|
}
|
|
1547
1628
|
async function runLint(restArgs, flags, excludes, config) {
|
|
1629
|
+
// The project root the CLI resolved once; the excludes are rooted at it.
|
|
1630
|
+
const root = excludes.root;
|
|
1548
1631
|
const summary = flags.includes("--summary");
|
|
1549
1632
|
const json = flags.includes("--json");
|
|
1550
1633
|
const silent = summary || json;
|
|
@@ -1561,7 +1644,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1561
1644
|
(0, node_fs_1.existsSync)(positional[0]) &&
|
|
1562
1645
|
(0, node_fs_1.lstatSync)(positional[0]).isDirectory()
|
|
1563
1646
|
? (0, node_path_1.resolve)(positional[0])
|
|
1564
|
-
:
|
|
1647
|
+
: root;
|
|
1565
1648
|
// Resolve the active harness ONCE so the harness-specific checks below run
|
|
1566
1649
|
// against the right adapter's dialect (tool/event catalogs) and surfaces —
|
|
1567
1650
|
// not a hard-coded Claude Code default. A subagent-surface rule reports n/a
|
|
@@ -1595,8 +1678,9 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1595
1678
|
// ONE frame for the whole run (#281): every path a check prints or annotates is
|
|
1596
1679
|
// relative to it, and config globs resolve from it — whichever bundle is being
|
|
1597
1680
|
// scored. Built here, once, instead of each option re-deriving its own root.
|
|
1598
|
-
const frame = (0, frame_js_1.frameFor)(
|
|
1599
|
-
const
|
|
1681
|
+
const frame = (0, frame_js_1.frameFor)(root, scanRoot);
|
|
1682
|
+
const rules = (0, overrides_js_1.ruleResolver)(config?.rules ?? DEFAULT_CONFIG.rules, config?.overrides);
|
|
1683
|
+
const run = { config, rules, silent, adapter, excludes, frame };
|
|
1600
1684
|
if (!silent && nestedBundles.length > 0) {
|
|
1601
1685
|
const rel = nestedBundles.map((b) => (0, node_path_1.relative)(scanRoot, b) || b);
|
|
1602
1686
|
console.log(scoreAll
|
|
@@ -1622,7 +1706,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1622
1706
|
}
|
|
1623
1707
|
}
|
|
1624
1708
|
const hashResult = files.length > 0
|
|
1625
|
-
? check(frame, files, silent)
|
|
1709
|
+
? check(root, frame, files, silent, config)
|
|
1626
1710
|
: { valid: true, hashErrors: 0, validationErrors: 0 };
|
|
1627
1711
|
// 1b. Verify inline + frontmatter rules in instruction files not managed
|
|
1628
1712
|
// by a spec. See verifyMarkdownModeRules / docs/markdown-mode.md.
|
|
@@ -1637,16 +1721,15 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1637
1721
|
// fail on unrelated duplicates elsewhere in the repo.
|
|
1638
1722
|
if (!silent)
|
|
1639
1723
|
console.log("\nDuplicate rule detection:\n");
|
|
1640
|
-
const dups = await findDuplicateRules(excludes, 0.3, silent, restArgs.length > 0 ? files : undefined);
|
|
1724
|
+
const dups = await findDuplicateRules(excludes, 0.3, silent, restArgs.length > 0 ? files : undefined, (specPath) => rules.severityAt("duplicate-rules", specPath));
|
|
1641
1725
|
// 4. Guidance rule count (strengthen suggestions moved to /strengthen skill)
|
|
1642
1726
|
const guidanceCount = await countGuidanceRules(excludes, silent);
|
|
1643
1727
|
// 5. Integrity check (hand-edit detection via SHA-256 hash)
|
|
1644
|
-
const integritySeverity = config?.rules.integrity ?? "warn";
|
|
1645
1728
|
let integrityErrors = 0;
|
|
1646
|
-
if (
|
|
1729
|
+
if (rules.mayFire("integrity")) {
|
|
1647
1730
|
if (!silent)
|
|
1648
1731
|
console.log("\nIntegrity check:\n");
|
|
1649
|
-
integrityErrors = checkIntegrityForFiles(files,
|
|
1732
|
+
integrityErrors = checkIntegrityForFiles(root, files, rules, silent);
|
|
1650
1733
|
}
|
|
1651
1734
|
// 6. Coverage thresholds (gates CI when severity is "error")
|
|
1652
1735
|
const coverageErrors = await checkCoverageThresholds(excludes, coverage, config, silent);
|
|
@@ -1659,13 +1742,12 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1659
1742
|
// opted into explicitly). `enforce("vigiles/orphan-docs")` in a spec only
|
|
1660
1743
|
// validates the rule NAME — the block is what drives the scan.
|
|
1661
1744
|
// 🔴 SEVERITY IS READ, not just the block's presence (#181). `orphan-docs` had
|
|
1662
|
-
// a
|
|
1745
|
+
// a declared meta and a documented severity, and nothing ever read it: both
|
|
1663
1746
|
// `"warn"` and `"off"` still exited 1, so a repo could only choose between an
|
|
1664
1747
|
// always-blocking check and deleting the `orphans` block. `warn` now reports
|
|
1665
1748
|
// without touching the exit code and `false`/`"off"` skips the scan, exactly
|
|
1666
1749
|
// like every other rule.
|
|
1667
|
-
const
|
|
1668
|
-
const orphansCfg = orphanSeverity ? config?.orphans : undefined;
|
|
1750
|
+
const orphansCfg = rules.mayFire("orphan-docs") ? config?.orphans : undefined;
|
|
1669
1751
|
if (!silent)
|
|
1670
1752
|
console.log("\nOrphan docs check:\n");
|
|
1671
1753
|
let orphanReport = {
|
|
@@ -1675,12 +1757,10 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1675
1757
|
orphans: [],
|
|
1676
1758
|
};
|
|
1677
1759
|
if (orphansCfg) {
|
|
1678
|
-
|
|
1679
|
-
basePath:
|
|
1760
|
+
const found = (0, orphans_js_1.findOrphanDocs)({
|
|
1761
|
+
basePath: root,
|
|
1680
1762
|
include: orphansCfg.include,
|
|
1681
|
-
exclude
|
|
1682
|
-
// The repo-wide `exclude` is the FLOOR under the rule's own `exclude`
|
|
1683
|
-
// (union, never override): an excluded corpus is neither an orphan
|
|
1763
|
+
// The repo-wide `exclude`: an excluded corpus is neither an orphan
|
|
1684
1764
|
// candidate nor a source of references that keep a doc alive (#192).
|
|
1685
1765
|
repoExclude: excludes.globIgnore,
|
|
1686
1766
|
// Exempt every registered harness's surface files (instruction file,
|
|
@@ -1688,6 +1768,10 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1688
1768
|
// core carries no harness literal (see src/core/orphans.ts).
|
|
1689
1769
|
layouts: adapter_registry_js_1.ADAPTERS.map((a) => a.layout),
|
|
1690
1770
|
});
|
|
1771
|
+
// Per doc: an `overrides` entry with `orphan-docs` off drops it from the
|
|
1772
|
+
// report (it still counts as a doc that references others).
|
|
1773
|
+
const orphanGraded = graded(rules, "orphan-docs", found.orphans.map((doc) => ({ doc })), (o) => o.doc);
|
|
1774
|
+
orphanReport = { ...found, orphans: orphanGraded.map((o) => o.doc) };
|
|
1691
1775
|
if (!silent) {
|
|
1692
1776
|
for (const line of (0, orphans_js_1.formatOrphanReport)(orphanReport).split("\n")) {
|
|
1693
1777
|
console.log(` ${line}`);
|
|
@@ -1702,7 +1786,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1702
1786
|
// hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
|
|
1703
1787
|
// A compiled artifact's refs, re-derived from its spec — the hash says the file
|
|
1704
1788
|
// is unchanged, not that what it names still exists (#173).
|
|
1705
|
-
const specRefs = await checkSpecRefs(excludes, config, silent, adapter.dialect);
|
|
1789
|
+
const specRefs = await checkSpecRefs(excludes, config, rules, silent, adapter.dialect);
|
|
1706
1790
|
const untested = overBundles(checkUntestedSurfaces, run, lintRoots);
|
|
1707
1791
|
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
1708
1792
|
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
@@ -1728,6 +1812,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1728
1812
|
// validate`"; measured false on 2026-09-08 (Claude Code 2.1.263) — see
|
|
1729
1813
|
// docs/rules/hook-script-exists.md and tools/measure-validate-overlap.mjs.
|
|
1730
1814
|
const hookScripts = overBundles(checkHookScriptExists, run, lintRoots);
|
|
1815
|
+
// 7i². Hand-written hooks — every hook line vigiles did not write, by name.
|
|
1816
|
+
const handWrittenHooks = overBundles(checkHandWrittenHooks, run, lintRoots);
|
|
1731
1817
|
// 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
|
|
1732
1818
|
// (the deny-side mirror of subagent-tool-contract; close-typo only).
|
|
1733
1819
|
const disallowedTools = overBundles(checkDisallowedTools, run, lintRoots);
|
|
@@ -1744,16 +1830,18 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1744
1830
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
1745
1831
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
1746
1832
|
const mcpHookTargets = overBundles(checkMcpHookTargets, run, lintRoots);
|
|
1747
|
-
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
1748
|
-
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
1749
|
-
const preferCompiledHooks = overBundles(checkPreferCompiledHooks, run, lintRoots);
|
|
1750
1833
|
// 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
|
|
1751
1834
|
// hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
|
|
1752
1835
|
// prompt-injection exfil path (Rule of Two). Capability SET-intersection.
|
|
1753
1836
|
const lethalTrifecta = overBundles(checkLethalTrifecta, run, lintRoots);
|
|
1754
|
-
// 7p.
|
|
1755
|
-
//
|
|
1756
|
-
|
|
1837
|
+
// 7p. The rules that are modules (src/rules/<id>.ts), each through the one
|
|
1838
|
+
// generic check. The LintReport keeps its per-rule wire keys until #390:
|
|
1839
|
+
// today the one module rule (the bundled-resource check) fills the
|
|
1840
|
+
// `skillResource*` pair below.
|
|
1841
|
+
const ruleModules = RULE_MODULES.map((rule) => overBundles((ctx) => checkRuleModule(ctx, rule), run, lintRoots)).reduce((acc, t) => ({
|
|
1842
|
+
issues: acc.issues + t.issues,
|
|
1843
|
+
errors: acc.errors + t.errors,
|
|
1844
|
+
}), { issues: 0, errors: 0 });
|
|
1757
1845
|
// 7q. Skill-missing-fence — a SKILL.md opening with `name:`/`description:` but no
|
|
1758
1846
|
// `---` fence loads as plain body (invisible — no name/description/trigger).
|
|
1759
1847
|
const skillFence = overBundles(checkSkillMissingFence, run, lintRoots);
|
|
@@ -1792,11 +1880,12 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1792
1880
|
// over the repository, like the ratchet: a nested bundle's spec would
|
|
1793
1881
|
// otherwise be reported again by every bundle that encloses it.
|
|
1794
1882
|
const deprecatedKeyFiles = (0, deprecated_key_files_js_1.checkDeprecatedKeyFiles)({
|
|
1795
|
-
severity:
|
|
1796
|
-
|
|
1883
|
+
severity: rules.mayFire("deprecated-key-files") ? "warn" : false,
|
|
1884
|
+
severityAt: (path) => rules.severityAt("deprecated-key-files", path),
|
|
1885
|
+
find: () => specsUsingKeyFiles(root, excludes.globIgnore),
|
|
1797
1886
|
silent,
|
|
1798
1887
|
annotate: (level, message, path) => {
|
|
1799
|
-
ghAnnotate(level, message, frame.repo((0, node_path_1.resolve)(
|
|
1888
|
+
ghAnnotate(level, message, frame.repo((0, node_path_1.resolve)(root, path)));
|
|
1800
1889
|
},
|
|
1801
1890
|
});
|
|
1802
1891
|
// 8. Validate vigiles builder calls inside markdown code blocks — the
|
|
@@ -1819,7 +1908,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1819
1908
|
// globs without `dot`, so `.claude/**` — where real skills and agents live —
|
|
1820
1909
|
// has never been scanned. The rule has never once looked at a deployed
|
|
1821
1910
|
// instruction file; every ref it has ever judged was in prose.
|
|
1822
|
-
const docRefSeverity =
|
|
1911
|
+
const docRefSeverity = rules.mayFire("doc-refs");
|
|
1823
1912
|
if (!silent && docRefSeverity)
|
|
1824
1913
|
console.log("\nMarkdown code block refs:\n");
|
|
1825
1914
|
// 🔴 `config.exclude` REACHES THIS PASS. It did not, and that single omission is
|
|
@@ -1835,8 +1924,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1835
1924
|
// With no way to reach 0 the step was made `continue-on-error: true`, and a lint
|
|
1836
1925
|
// whose exit code is discarded gates nothing — after which hand-written CI steps
|
|
1837
1926
|
// grew to do the gating instead. One unpassed argument, that whole chain.
|
|
1838
|
-
const
|
|
1839
|
-
? (0, doc_refs_js_1.findDocRefs)({ basePath:
|
|
1927
|
+
const docRefFound = docRefSeverity
|
|
1928
|
+
? (0, doc_refs_js_1.findDocRefs)({ basePath: root, ignore: excludes.globIgnore })
|
|
1840
1929
|
: {
|
|
1841
1930
|
filesScanned: 0,
|
|
1842
1931
|
filesIgnored: 0,
|
|
@@ -1846,10 +1935,24 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1846
1935
|
unverified: 0,
|
|
1847
1936
|
placeholders: 0,
|
|
1848
1937
|
};
|
|
1938
|
+
// Per file: an `overrides` entry may turn `doc-refs` off, or set its
|
|
1939
|
+
// severity, for a folder. Printed once per severity present, so a `✗` never
|
|
1940
|
+
// stands next to a finding that does not gate.
|
|
1941
|
+
const docRefErrors = graded(rules, "doc-refs", docRefFound.errors, (e) => e.file);
|
|
1942
|
+
const docRefReport = { ...docRefFound, errors: docRefErrors };
|
|
1849
1943
|
if (!silent && docRefSeverity) {
|
|
1850
|
-
const
|
|
1851
|
-
|
|
1852
|
-
|
|
1944
|
+
const groups = ["error", "warn"]
|
|
1945
|
+
.map((sev) => ({
|
|
1946
|
+
sev,
|
|
1947
|
+
errors: docRefErrors.filter((e) => e.sev === sev),
|
|
1948
|
+
}))
|
|
1949
|
+
.filter((g, i, all) => g.errors.length > 0 ||
|
|
1950
|
+
(i === 0 && all.every((x) => x.errors.length === 0)));
|
|
1951
|
+
for (const g of groups) {
|
|
1952
|
+
const rendered = (0, doc_refs_js_1.formatDocRefReport)({ ...docRefFound, errors: g.errors }, g.sev);
|
|
1953
|
+
for (const line of rendered.split("\n")) {
|
|
1954
|
+
console.log(` ${line}`);
|
|
1955
|
+
}
|
|
1853
1956
|
}
|
|
1854
1957
|
}
|
|
1855
1958
|
// Per-line GitHub annotations for each broken doc ref — each carries file+line,
|
|
@@ -1859,7 +1962,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1859
1962
|
// on the PR. CI-only (isGitHubActions); skipped under --json/--summary.
|
|
1860
1963
|
if (isGitHubActions() && !silent && docRefSeverity) {
|
|
1861
1964
|
for (const e of docRefReport.errors) {
|
|
1862
|
-
ghAnnotate(
|
|
1965
|
+
ghAnnotate(markOf(e.sev).level, `${e.kind}("${e.value}") — ${e.message}`, frame.repo((0, node_path_1.resolve)(root, e.file)), e.line);
|
|
1863
1966
|
}
|
|
1864
1967
|
}
|
|
1865
1968
|
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
@@ -1878,14 +1981,16 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1878
1981
|
specRefErrors: specRefs.errors,
|
|
1879
1982
|
compileBudgetErrors: specRefs.budgetErrors,
|
|
1880
1983
|
duplicatePairs: dups.pairCount,
|
|
1881
|
-
duplicateSeverity:
|
|
1984
|
+
duplicateSeverity: dups.errorPairs > 0 ? "error" : "warn",
|
|
1882
1985
|
coverageEnabled: coverage.enabled,
|
|
1883
1986
|
coverageDocumented: coverage.documented,
|
|
1884
1987
|
strengthenSuggestions: guidanceCount,
|
|
1885
1988
|
integrityErrors,
|
|
1886
1989
|
coverageErrors,
|
|
1887
1990
|
orphanCount: orphanReport.orphans.length,
|
|
1888
|
-
orphanSeverity:
|
|
1991
|
+
orphanSeverity: orphanReport.orphans.some((doc) => rules.severityAt("orphan-docs", doc) === "error")
|
|
1992
|
+
? "error"
|
|
1993
|
+
: "warn",
|
|
1889
1994
|
untestedSurfaces: untested.untested,
|
|
1890
1995
|
untestedErrors: untested.errors,
|
|
1891
1996
|
toolContractIssues: toolContract.issues,
|
|
@@ -1902,6 +2007,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1902
2007
|
mcpToolErrors: mcpToolResolves.errors,
|
|
1903
2008
|
hookScriptIssues: hookScripts.issues,
|
|
1904
2009
|
hookScriptErrors: hookScripts.errors,
|
|
2010
|
+
handWrittenHookIssues: handWrittenHooks.issues,
|
|
2011
|
+
handWrittenHookErrors: handWrittenHooks.errors,
|
|
1905
2012
|
disallowedToolIssues: disallowedTools.issues,
|
|
1906
2013
|
disallowedToolErrors: disallowedTools.errors,
|
|
1907
2014
|
descriptionOverlapIssues: descriptionOverlap.issues,
|
|
@@ -1912,12 +2019,10 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1912
2019
|
frontmatterValidErrors: frontmatterValid.errors,
|
|
1913
2020
|
mcpHookIssues: mcpHookTargets.issues,
|
|
1914
2021
|
mcpHookErrors: mcpHookTargets.errors,
|
|
1915
|
-
preferCompiledHookIssues: preferCompiledHooks.issues,
|
|
1916
|
-
preferCompiledHookErrors: preferCompiledHooks.errors,
|
|
1917
2022
|
lethalTrifectaIssues: lethalTrifecta.issues,
|
|
1918
2023
|
lethalTrifectaErrors: lethalTrifecta.errors,
|
|
1919
|
-
skillResourceIssues:
|
|
1920
|
-
skillResourceErrors:
|
|
2024
|
+
skillResourceIssues: ruleModules.issues,
|
|
2025
|
+
skillResourceErrors: ruleModules.errors,
|
|
1921
2026
|
skillFenceIssues: skillFence.issues,
|
|
1922
2027
|
skillFenceErrors: skillFence.errors,
|
|
1923
2028
|
pluginLayoutIssues: pluginLayout.issues,
|
|
@@ -1935,7 +2040,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1935
2040
|
// Only the "error" tier gates. At "warn" the findings are printed and
|
|
1936
2041
|
// annotated, and the exit code is untouched — same contract as every other
|
|
1937
2042
|
// opt-in rule here.
|
|
1938
|
-
docRefErrors:
|
|
2043
|
+
docRefErrors: docRefErrors.filter((e) => e.sev === "error").length,
|
|
1939
2044
|
symbolRefErrors,
|
|
1940
2045
|
mcpRefErrors,
|
|
1941
2046
|
files,
|
|
@@ -2021,10 +2126,9 @@ function collectDocumentedRules(excludes) {
|
|
|
2021
2126
|
});
|
|
2022
2127
|
for (const mdFile of mdFiles) {
|
|
2023
2128
|
const content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), mdFile), "utf-8");
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
documented.add(m[1]);
|
|
2129
|
+
// Both compiled spellings (#351): an unverified claim is still documented.
|
|
2130
|
+
for (const m of content.matchAll(new RegExp(enforce_marker_js_1.RULE_MARKER_SOURCE, "g"))) {
|
|
2131
|
+
documented.add(m[2]);
|
|
2028
2132
|
}
|
|
2029
2133
|
}
|
|
2030
2134
|
return documented;
|
|
@@ -2079,10 +2183,12 @@ function discover(excludes, silent = false) {
|
|
|
2079
2183
|
}
|
|
2080
2184
|
const totalPct = totalEnabled > 0 ? Math.round((totalDocumented / totalEnabled) * 100) : 0;
|
|
2081
2185
|
log(`Coverage: ${String(totalDocumented)}/${String(totalEnabled)} rules documented (${String(totalPct)}%)`);
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
}
|
|
2186
|
+
const advice = (0, coverage_js_1.undocumentedRulesAdvice)({
|
|
2187
|
+
enabled: totalEnabled,
|
|
2188
|
+
documented: totalDocumented,
|
|
2189
|
+
});
|
|
2190
|
+
if (advice.length > 0)
|
|
2191
|
+
log(["", ...advice].join("\n"));
|
|
2086
2192
|
return { enabled: totalEnabled, documented: totalDocumented };
|
|
2087
2193
|
}
|
|
2088
2194
|
/** This package's own version (from the installed package.json). */
|
|
@@ -2178,21 +2284,40 @@ function discoverAdoptableSurfaces(cwd) {
|
|
|
2178
2284
|
}
|
|
2179
2285
|
return out;
|
|
2180
2286
|
}
|
|
2181
|
-
/**
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
*
|
|
2185
|
-
*
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2287
|
+
/** Every hand-written instruction file under `root` — root AND nested
|
|
2288
|
+
* `CLAUDE.md` / `AGENTS.md`, the same files `gatherInstructionFiles` reads —
|
|
2289
|
+
* by the one predicate `lint`'s `require-instructions-spec` asks
|
|
2290
|
+
* (`instructionSpecState`). Measured on a repo with six hand-written files
|
|
2291
|
+
* (root + five nested): `audit` counted 1 (root only) while `lint` warned about
|
|
2292
|
+
* all six. A spec-managed or opted-out file is not listed. */
|
|
2293
|
+
function handWrittenInstructionFiles(root, instructionFile, excludes, rules) {
|
|
2294
|
+
return (gatherInstructionFiles(root, instructionFile, excludes)
|
|
2295
|
+
.filter((f) => (0, integrity_js_1.instructionSpecState)(f.text, (0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, `${f.path}.spec.ts`))) === "hand-written")
|
|
2296
|
+
// A folder an `overrides` entry turned the rule off for keeps its files
|
|
2297
|
+
// hand-written on purpose: that decline is remembered, not re-offered.
|
|
2298
|
+
.filter((f) => rules.severityAt("require-instructions-spec", f.path) !== false)
|
|
2299
|
+
.map((f) => f.path));
|
|
2300
|
+
}
|
|
2301
|
+
/** The hand-written instruction files BELOW the root — the ones `init`'s
|
|
2302
|
+
* root-level `determineTargets` does not see. Bare `init` adopts them too, so a
|
|
2303
|
+
* repo it sets up passes `require-instructions-spec` (in `vigiles:recommended`)
|
|
2304
|
+
* on its first `lint`. Read with the repo's own `exclude`. */
|
|
2305
|
+
function nestedHandWrittenInstructionFiles(cwd) {
|
|
2306
|
+
const config = loadConfig();
|
|
2307
|
+
return handWrittenInstructionFiles(cwd, adapter_registry_js_1.defaultAdapter.layout.instructionFile, (0, exclude_js_1.excludeSet)(cwd, config.exclude), (0, overrides_js_1.ruleResolver)(config.rules, config.overrides)).filter((p) => p.includes("/"));
|
|
2308
|
+
}
|
|
2309
|
+
/** The full adoptable-surface list `audit` reports: every hand-written
|
|
2310
|
+
* instruction file (nested included) PLUS every skill/subagent surface without
|
|
2311
|
+
* a spec. Same notion `init` adopts; surfaced in the AuditReport + the terminal
|
|
2312
|
+
* nudge so the report's "Create spec" / "Create all specs" affordances have
|
|
2313
|
+
* their paths. Composition-root only — CC paths are intentional here (like
|
|
2314
|
+
* discoverAdoptableSurfaces). */
|
|
2315
|
+
function discoverAdoptableForAudit(root, { instructionFile, excludes, config, }) {
|
|
2316
|
+
const rules = (0, overrides_js_1.ruleResolver)(config.rules, config.overrides);
|
|
2317
|
+
return [
|
|
2318
|
+
...handWrittenInstructionFiles(root, instructionFile, excludes, rules),
|
|
2319
|
+
...discoverAdoptableSurfaces(root),
|
|
2320
|
+
].filter((p) => !(0, exclude_js_1.excludedBy)(excludes)((0, node_path_1.resolve)(root, p)));
|
|
2196
2321
|
}
|
|
2197
2322
|
/** Lint-config file BASENAMES whose CONTENTS (textual, NEVER executed) reveal
|
|
2198
2323
|
* which rules a repo has configured — read best-effort for the rule-inventory
|
|
@@ -2401,7 +2526,7 @@ function computeRuleRouting(root, instructionFile, excludes) {
|
|
|
2401
2526
|
// routing. Enumerating EXECUTES the linter (plugin code loads), hence own-repo
|
|
2402
2527
|
// + consent — same posture for both.
|
|
2403
2528
|
const ownRepo = (0, node_path_1.resolve)(root) === (0, node_path_1.resolve)(process.cwd());
|
|
2404
|
-
const consented =
|
|
2529
|
+
const consented = loadConfig().audit?.measure === true;
|
|
2405
2530
|
const availableRules = ownRepo && consented
|
|
2406
2531
|
? (0, rule_catalog_js_1.mergeCatalogs)((0, rule_catalog_js_1.enumerateEslintCatalog)(root),
|
|
2407
2532
|
// Only spawn pylint when the repo actually has a Python surface —
|
|
@@ -2425,32 +2550,47 @@ function computeRuleRouting(root, instructionFile, excludes) {
|
|
|
2425
2550
|
return undefined;
|
|
2426
2551
|
}
|
|
2427
2552
|
}
|
|
2428
|
-
/** The terminal "adoptable surfaces" nudge
|
|
2429
|
-
*
|
|
2553
|
+
/** The terminal "adoptable surfaces" nudge. Instruction files are listed in
|
|
2554
|
+
* full (up to {@link NUDGE_LIST_CAP}) with the shared adoption remedy — they
|
|
2555
|
+
* have two adopters, `init --target` and the `adopt-spec` skill; skills and
|
|
2556
|
+
* subagents keep the per-surface `init --target` command. "" when nothing to
|
|
2430
2557
|
* adopt (a fully spec-managed repo says nothing). */
|
|
2431
2558
|
function formatAdoptableNudge(surfaces) {
|
|
2432
2559
|
if (surfaces.length === 0)
|
|
2433
2560
|
return "";
|
|
2561
|
+
const instructions = surfaces.filter((s) => surfaceKind(s) === "instruction");
|
|
2562
|
+
const others = surfaces.filter((s) => surfaceKind(s) !== "instruction");
|
|
2434
2563
|
const n = surfaces.length;
|
|
2564
|
+
const capped = (items, line) => {
|
|
2565
|
+
const shown = items.slice(0, NUDGE_LIST_CAP).map(line);
|
|
2566
|
+
const more = items.length - shown.length;
|
|
2567
|
+
return more > 0
|
|
2568
|
+
? [...shown, ` • +${String(more)} more (all in --json)`]
|
|
2569
|
+
: shown;
|
|
2570
|
+
};
|
|
2435
2571
|
// Lead with the VALUE, not the chore: a spec is the pull up from the gate to
|
|
2436
2572
|
// the richer layer, so say what it BUYS (drift-proof references + testable),
|
|
2437
2573
|
// not just "create one". The invitation is where richer-feature adoption is
|
|
2438
2574
|
// won — keep it compelling, never a bare to-do (research/adoption-design.md §1).
|
|
2439
2575
|
const lines = [
|
|
2440
2576
|
`ℹ ${String(n)} surface${n === 1 ? "" : "s"} could be spec-managed — a spec re-verifies its references on every change (a hand-edit can't silently break them) and makes it testable.`,
|
|
2441
|
-
` Adopt now, or one at a time with \`npx vigiles init --target=<path>\`:`,
|
|
2442
2577
|
];
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
lines.push(`
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2578
|
+
if (instructions.length > 0) {
|
|
2579
|
+
const k = instructions.length;
|
|
2580
|
+
lines.push(` ${String(k)} hand-written instruction file${k === 1 ? "" : "s"}:`, ...capped(instructions, (s) => ` • ${s}`), ...(0, instruction_remedies_js_1.adoptInstructionFileRemedy)("<path>")
|
|
2581
|
+
.split("\n")
|
|
2582
|
+
.map((l) => ` ${l}`));
|
|
2583
|
+
}
|
|
2584
|
+
if (others.length > 0) {
|
|
2585
|
+
lines.push(` Skills and subagents, one at a time with \`npx vigiles init --target=<path>\`:`, ...capped(others, (s) => ` • npx vigiles init --target=${s}`));
|
|
2586
|
+
}
|
|
2449
2587
|
// The remembered-decline affordance (non-evil contract): one keystroke to
|
|
2450
2588
|
// silence, stated where it's shown so it's never a surprise nag.
|
|
2451
2589
|
lines.push(` (set "nudge": "dismissed" in .vigilesrc.json to hide this)`);
|
|
2452
2590
|
return lines.join("\n");
|
|
2453
2591
|
}
|
|
2592
|
+
/** How many paths one group of the adoption nudge prints before "+N more". */
|
|
2593
|
+
const NUDGE_LIST_CAP = 10;
|
|
2454
2594
|
/** A small, terse behavioral nudge — the deterministic read can't tell whether a
|
|
2455
2595
|
* skill actually FIRES. "" when there are no model-invocable skills. */
|
|
2456
2596
|
function formatTriggerNudge(triggerableSkills) {
|
|
@@ -3168,7 +3308,11 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3168
3308
|
// file. An explicit --target stays scoped to that one surface.
|
|
3169
3309
|
const targets = targetValue
|
|
3170
3310
|
? instructionTargets
|
|
3171
|
-
: [
|
|
3311
|
+
: [
|
|
3312
|
+
...instructionTargets,
|
|
3313
|
+
...nestedHandWrittenInstructionFiles(cwd).filter((p) => !instructionTargets.includes(p)),
|
|
3314
|
+
...discoverAdoptableSurfaces(cwd),
|
|
3315
|
+
];
|
|
3172
3316
|
// Create specs. An existing hand-written target is faithfully ADOPTED into a
|
|
3173
3317
|
// spec (scaffoldSpec() does the convert), not clobbered with a blank one — so the
|
|
3174
3318
|
// compile below reproduces it (the user reviews the diff). A greenfield target
|
|
@@ -3199,6 +3343,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3199
3343
|
for (const l of typesResult.linters) {
|
|
3200
3344
|
console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
|
|
3201
3345
|
}
|
|
3346
|
+
printUnreadableLinters(typesResult.unreadable);
|
|
3202
3347
|
if (typesResult.scripts.length > 0) {
|
|
3203
3348
|
console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
|
|
3204
3349
|
}
|
|
@@ -3206,7 +3351,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3206
3351
|
written.push(GENERATED_TYPES);
|
|
3207
3352
|
const schemaResult = (0, generate_schema_js_1.generateSchema)({
|
|
3208
3353
|
basePath: cwd,
|
|
3209
|
-
linters:
|
|
3354
|
+
linters: loadConfig().linters,
|
|
3210
3355
|
});
|
|
3211
3356
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, GENERATED_SCHEMA), schemaResult.json);
|
|
3212
3357
|
console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
|
|
@@ -3223,7 +3368,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3223
3368
|
// resolve hook in src/self-resolve.mts), so compiling needs no
|
|
3224
3369
|
// `node_modules/vigiles` in the user's repo — and a repo with no
|
|
3225
3370
|
// `package.json` at all (Python, Rust, Go) has no install to run.
|
|
3226
|
-
const initConfig =
|
|
3371
|
+
const initConfig = loadConfig();
|
|
3227
3372
|
const excludes = (0, exclude_js_1.excludeSet)(cwd, initConfig.exclude);
|
|
3228
3373
|
const specs = findSpecs(excludes).filter((s) => {
|
|
3229
3374
|
const tf = (0, node_path_1.resolve)(cwd, s.replace(/\.spec\.ts$/, ""));
|
|
@@ -3758,7 +3903,7 @@ async function setup(args) {
|
|
|
3758
3903
|
// Detect project. An existing `.vigilesrc.json` `harness` (from a prior init /
|
|
3759
3904
|
// a hand-authored config) wins over auto-detection (dogfood I3).
|
|
3760
3905
|
const detected = detectProject();
|
|
3761
|
-
const harnesses = resolveHarnesses(parsed, detected, (0, adapter_registry_js_1.declaredHarnessNames)(
|
|
3906
|
+
const harnesses = resolveHarnesses(parsed, detected, (0, adapter_registry_js_1.declaredHarnessNames)(loadConfig().harnesses));
|
|
3762
3907
|
printDetection(detected, harnesses);
|
|
3763
3908
|
// Files actually written, accumulated for an honest commit hint.
|
|
3764
3909
|
const written = [];
|
|
@@ -3811,6 +3956,7 @@ async function setup(args) {
|
|
|
3811
3956
|
strict,
|
|
3812
3957
|
reportOnly: parsed.reportOnly,
|
|
3813
3958
|
lint: plan.lint,
|
|
3959
|
+
specs: plan.scaffoldSpecs,
|
|
3814
3960
|
written,
|
|
3815
3961
|
});
|
|
3816
3962
|
printSetupSummary({
|
|
@@ -3848,7 +3994,7 @@ async function setup(args) {
|
|
|
3848
3994
|
function minimalRerun(parsed, state) {
|
|
3849
3995
|
const plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
3850
3996
|
const detected = detectProject();
|
|
3851
|
-
const harnesses = resolveHarnesses(parsed, detected, (0, adapter_registry_js_1.declaredHarnessNames)(
|
|
3997
|
+
const harnesses = resolveHarnesses(parsed, detected, (0, adapter_registry_js_1.declaredHarnessNames)(loadConfig().harnesses));
|
|
3852
3998
|
console.log(`vigiles is already set up here (found ${state.evidence}) — adding only what is missing.\n`);
|
|
3853
3999
|
printDetection(detected, harnesses);
|
|
3854
4000
|
const links = plan.plugin ? linkSkillsOnly(harnesses) : [];
|
|
@@ -3856,7 +4002,7 @@ function minimalRerun(parsed, state) {
|
|
|
3856
4002
|
const written = [...links, ...devDep];
|
|
3857
4003
|
if (written.length === 0)
|
|
3858
4004
|
console.log("\nNothing was missing.");
|
|
3859
|
-
const followUps = (0, setup_plan_js_1.fullSetupFollowUps)(fullSetupGap(plan, detected, harnesses));
|
|
4005
|
+
const followUps = (0, setup_plan_js_1.fullSetupFollowUps)(fullSetupGap(plan, detected, harnesses), INIT_POLICY.preset);
|
|
3860
4006
|
if (followUps.length > 0)
|
|
3861
4007
|
console.log(`\n${followUps.join("\n")}`);
|
|
3862
4008
|
if (written.length > 0)
|
|
@@ -3930,6 +4076,7 @@ function fullSetupGap(plan, detected, harnesses) {
|
|
|
3930
4076
|
const unspecced = pillar1
|
|
3931
4077
|
? [
|
|
3932
4078
|
...determineTargets(detected, undefined, [...harnesses]),
|
|
4079
|
+
...nestedHandWrittenInstructionFiles(cwd),
|
|
3933
4080
|
...discoverAdoptableSurfaces(cwd),
|
|
3934
4081
|
].filter((t, i, all) => all.indexOf(t) === i && !has(`${t}.spec.ts`))
|
|
3935
4082
|
: [];
|
|
@@ -3954,7 +4101,12 @@ function fullSetupGap(plan, detected, harnesses) {
|
|
|
3954
4101
|
function configGap(config, lint) {
|
|
3955
4102
|
if (config === null)
|
|
3956
4103
|
return "missing";
|
|
3957
|
-
|
|
4104
|
+
// "Has the preset" means extends RECOMMENDED: `vigiles:experimental` alone
|
|
4105
|
+
// gates nothing. A value that is not a preset list is not ours to judge here.
|
|
4106
|
+
const listed = (0, presets_js_1.extendedPresets)(CONFIG.presetNames, config.extends);
|
|
4107
|
+
return listed !== null && !listed.includes(INIT_POLICY.preset) && lint
|
|
4108
|
+
? "no-preset"
|
|
4109
|
+
: "has-preset";
|
|
3958
4110
|
}
|
|
3959
4111
|
/** `.vigilesrc.json` as a plain object, or null when absent or unreadable. */
|
|
3960
4112
|
function readProjectConfig(path) {
|
|
@@ -4006,10 +4158,12 @@ function writeProjectConfig(opts) {
|
|
|
4006
4158
|
}
|
|
4007
4159
|
}
|
|
4008
4160
|
const merged = (0, setup_plan_js_1.mergeProjectConfig)(existing, {
|
|
4161
|
+
policy: INIT_POLICY,
|
|
4009
4162
|
harnesses: harnessConfigKeys(opts.harnesses),
|
|
4010
4163
|
strict: opts.strict,
|
|
4011
4164
|
reportOnly: opts.reportOnly,
|
|
4012
4165
|
lint: opts.lint,
|
|
4166
|
+
specs: opts.specs,
|
|
4013
4167
|
});
|
|
4014
4168
|
if (!merged)
|
|
4015
4169
|
return;
|
|
@@ -4022,31 +4176,34 @@ function writeProjectConfig(opts) {
|
|
|
4022
4176
|
// ---------------------------------------------------------------------------
|
|
4023
4177
|
// Strengthen: guidance() → enforce() suggestions
|
|
4024
4178
|
// ---------------------------------------------------------------------------
|
|
4025
|
-
function checkIntegrityForFiles(files,
|
|
4179
|
+
function checkIntegrityForFiles(root, files, rules, silent) {
|
|
4026
4180
|
const log = (msg) => {
|
|
4027
4181
|
if (!silent)
|
|
4028
4182
|
console.log(msg);
|
|
4029
4183
|
};
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4184
|
+
const basePath = root;
|
|
4185
|
+
// Per file: an `overrides` entry may turn `integrity` off for a folder.
|
|
4186
|
+
const checked = files.flatMap((filePath) => {
|
|
4187
|
+
const severity = rules.severityAt("integrity", filePath);
|
|
4033
4188
|
const abs = (0, node_path_1.resolve)(basePath, filePath);
|
|
4034
|
-
if (!(0, node_fs_1.existsSync)(abs))
|
|
4035
|
-
|
|
4189
|
+
if (severity === false || !(0, node_fs_1.existsSync)(abs))
|
|
4190
|
+
return [];
|
|
4036
4191
|
const result = (0, integrity_js_1.checkIntegrity)((0, node_fs_1.readFileSync)(abs, "utf-8"));
|
|
4192
|
+
return [{ filePath, severity, result }];
|
|
4193
|
+
});
|
|
4194
|
+
for (const { filePath, severity, result } of checked) {
|
|
4037
4195
|
if (!result.intact) {
|
|
4038
|
-
|
|
4039
|
-
const marker = severity === "error" ? "✗" : "⚠";
|
|
4040
|
-
log(` ${marker} ${filePath} — ${result.reason ?? "tampered"}`);
|
|
4196
|
+
log(` ${markOf(severity).mark} ${filePath} — ${result.reason ?? "tampered"}`);
|
|
4041
4197
|
}
|
|
4042
4198
|
else if (!silent) {
|
|
4043
4199
|
log(` ✓ ${filePath}`);
|
|
4044
4200
|
}
|
|
4045
4201
|
}
|
|
4046
|
-
|
|
4202
|
+
const broken = checked.filter((c) => !c.result.intact);
|
|
4203
|
+
if (broken.length === 0) {
|
|
4047
4204
|
log(" All compiled files intact.");
|
|
4048
4205
|
}
|
|
4049
|
-
return severity === "error"
|
|
4206
|
+
return broken.filter((c) => c.severity === "error").length;
|
|
4050
4207
|
}
|
|
4051
4208
|
/**
|
|
4052
4209
|
* The layout of the harness this repo actually targets — `--harness=`/config/
|
|
@@ -4093,7 +4250,7 @@ const UNTESTED_RULE = {
|
|
|
4093
4250
|
/**
|
|
4094
4251
|
* The `untested-*` rules AS THE DETECTOR TAKES THEM: which kinds this repo
|
|
4095
4252
|
* enabled, and the discovery options merged from whichever of the three rules
|
|
4096
|
-
* carries them (`include` / `
|
|
4253
|
+
* carries them (`include` / `testExtension` are shared).
|
|
4097
4254
|
*
|
|
4098
4255
|
* 🔴 ONE READER, BECAUSE THE SECOND ONE DRIFTED. `vigiles lint` read the config
|
|
4099
4256
|
* here; the PostToolUse nudge (`evalLockNudgeHookCommand`) called the same
|
|
@@ -4104,25 +4261,24 @@ const UNTESTED_RULE = {
|
|
|
4104
4261
|
* while the nudge said "no test or eval covers it". A nudge contradicting the
|
|
4105
4262
|
* linter of the same repo, in the same second, teaches people to ignore both.
|
|
4106
4263
|
*/
|
|
4107
|
-
function untestedRules(config) {
|
|
4108
|
-
const rules = config?.rules;
|
|
4109
|
-
const severity = (kind) => (0, types_js_1.ruleSeverity)(rules?.[UNTESTED_RULE[kind]]);
|
|
4264
|
+
function untestedRules(config, rules) {
|
|
4110
4265
|
const names = Object.values(UNTESTED_RULE);
|
|
4111
4266
|
const opts = names.reduce((acc, rule) => ({
|
|
4112
4267
|
...acc,
|
|
4113
|
-
...(0, types_js_1.ruleOptions)(rules?.[rule]),
|
|
4268
|
+
...(0, types_js_1.ruleOptions)(config?.rules?.[rule]),
|
|
4114
4269
|
}), {});
|
|
4115
|
-
|
|
4270
|
+
// A kind is SCANNED when its rule can fire anywhere — an override may turn
|
|
4271
|
+
// it on for one folder while `rules` has it off.
|
|
4272
|
+
const on = (kind) => rules.mayFire(UNTESTED_RULE[kind]);
|
|
4116
4273
|
return {
|
|
4117
|
-
severity,
|
|
4118
|
-
anyEnabled: names.some((rule) =>
|
|
4274
|
+
severity: (kind, path) => rules.severityAt(UNTESTED_RULE[kind], path),
|
|
4275
|
+
anyEnabled: names.some((rule) => rules.mayFire(rule)),
|
|
4119
4276
|
options: {
|
|
4120
4277
|
skills: on("skill"),
|
|
4121
4278
|
agents: on("agent"),
|
|
4122
4279
|
hooks: on("hook"),
|
|
4123
4280
|
outputStyles: on("output-style"),
|
|
4124
4281
|
include: opts.include,
|
|
4125
|
-
exclude: opts.exclude,
|
|
4126
4282
|
// Without this the `testExtension` documented on TestCoverageOptions was a
|
|
4127
4283
|
// config key nothing read: a TypeScript-shaped repo got `.ts` suggestions
|
|
4128
4284
|
// however the author configured it. Prose isn't policy, in our own CLI.
|
|
@@ -4139,33 +4295,37 @@ function untestedRules(config) {
|
|
|
4139
4295
|
*/
|
|
4140
4296
|
function checkUntestedSurfaces(ctx) {
|
|
4141
4297
|
const { config, silent, adapter, frame, bundle } = ctx;
|
|
4142
|
-
const { severity: sevFor, anyEnabled, options } = untestedRules(config);
|
|
4298
|
+
const { severity: sevFor, anyEnabled, options, } = untestedRules(config, ctx.rules);
|
|
4143
4299
|
if (!anyEnabled)
|
|
4144
4300
|
return { untested: 0, errors: 0 };
|
|
4145
|
-
const
|
|
4301
|
+
const scanned = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
4146
4302
|
...options,
|
|
4147
4303
|
basePath: bundle.abs,
|
|
4148
|
-
// #281: `include
|
|
4304
|
+
// #281: `include` and every reported path are relative to the
|
|
4149
4305
|
// frame root (where `.vigilesrc.json` lives), not to the bundle being scored.
|
|
4150
4306
|
root: frame.root,
|
|
4151
4307
|
layout: adapter.layout,
|
|
4152
|
-
// The
|
|
4153
|
-
// it. Union, never override — a rule option must not re-admit a vendored
|
|
4154
|
-
// corpus the repo excluded (#192). Passed WHOLE so it carries its own root.
|
|
4308
|
+
// The repo-wide `exclude` — passed WHOLE so it carries its own root (#192).
|
|
4155
4309
|
excludes: ctx.excludes,
|
|
4156
4310
|
});
|
|
4311
|
+
// Per surface: an `overrides` entry may turn its kind's rule off for a folder.
|
|
4312
|
+
const untested = scanned.untested.flatMap((s) => {
|
|
4313
|
+
const sev = sevFor(s.kind, s.path);
|
|
4314
|
+
return sev === false ? [] : [{ ...s, sev }];
|
|
4315
|
+
});
|
|
4316
|
+
const report = { ...scanned, untested };
|
|
4157
4317
|
if (!silent) {
|
|
4158
4318
|
console.log("\nUntested surfaces:\n");
|
|
4159
4319
|
for (const line of (0, test_coverage_js_1.formatUntestedReport)(report).split("\n")) {
|
|
4160
4320
|
console.log(` ${line}`);
|
|
4161
4321
|
}
|
|
4162
4322
|
for (const s of report.untested) {
|
|
4163
|
-
ctx.annotate(
|
|
4323
|
+
ctx.annotate(markOf(s.sev).level, `${s.kind} ${s.path} ships without a test or eval`, frame.repo((0, node_path_1.join)(frame.root, s.path)));
|
|
4164
4324
|
}
|
|
4165
4325
|
}
|
|
4166
4326
|
return {
|
|
4167
4327
|
untested: report.untested.length,
|
|
4168
|
-
errors:
|
|
4328
|
+
errors: untested.filter((s) => s.sev === "error").length,
|
|
4169
4329
|
};
|
|
4170
4330
|
}
|
|
4171
4331
|
/**
|
|
@@ -4190,9 +4350,8 @@ function reportNotApplicable(check, surface, adapter, silent) {
|
|
|
4190
4350
|
* `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
|
|
4191
4351
|
*/
|
|
4192
4352
|
function checkSubagentToolContracts(ctx) {
|
|
4193
|
-
const {
|
|
4194
|
-
|
|
4195
|
-
if (!sev)
|
|
4353
|
+
const { silent, adapter } = ctx;
|
|
4354
|
+
if (!ctx.rules.mayFire("subagent-tool-contract"))
|
|
4196
4355
|
return { issues: 0, errors: 0 };
|
|
4197
4356
|
if (!adapter.subagents) {
|
|
4198
4357
|
reportNotApplicable("Subagent tool-contract check", "subagents", adapter, silent);
|
|
@@ -4208,24 +4367,16 @@ function checkSubagentToolContracts(ctx) {
|
|
|
4208
4367
|
catch {
|
|
4209
4368
|
return { issues: 0, errors: 0 };
|
|
4210
4369
|
}
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
if (!printedHeader) {
|
|
4219
|
-
console.log("\nSubagent tool-contract check:\n");
|
|
4220
|
-
printedHeader = true;
|
|
4221
|
-
}
|
|
4222
|
-
for (const issue of agent.toolIssues) {
|
|
4223
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.bundle.scanned(agent.path)}: ${issue.message}`);
|
|
4224
|
-
ctx.annotate(sev === "error" ? "error" : "warning", issue.message, ctx.bundle.scanned(agent.path));
|
|
4225
|
-
}
|
|
4370
|
+
const found = graded(ctx.rules, "subagent-tool-contract", agents.flatMap((a) => a.toolIssues.map((i) => ({ path: a.path, message: i.message }))), (f) => ctx.bundle.scanned(f.path));
|
|
4371
|
+
if (found.length > 0 && !silent) {
|
|
4372
|
+
console.log("\nSubagent tool-contract check:\n");
|
|
4373
|
+
for (const issue of found) {
|
|
4374
|
+
const at = ctx.bundle.scanned(issue.path);
|
|
4375
|
+
console.log(` ${markOf(issue.sev).mark} ${at}: ${issue.message}`);
|
|
4376
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, at);
|
|
4226
4377
|
}
|
|
4227
4378
|
}
|
|
4228
|
-
return
|
|
4379
|
+
return tally(found);
|
|
4229
4380
|
}
|
|
4230
4381
|
/**
|
|
4231
4382
|
* Apply the `hook-events` rule: flag a hook registered under an event name the
|
|
@@ -4266,30 +4417,30 @@ function checkHookEvents(ctx) {
|
|
|
4266
4417
|
* `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
|
|
4267
4418
|
*/
|
|
4268
4419
|
function checkFrontmatterSchema(ctx) {
|
|
4269
|
-
const {
|
|
4270
|
-
|
|
4271
|
-
if (!sev)
|
|
4420
|
+
const { silent, adapter } = ctx;
|
|
4421
|
+
if (!ctx.rules.mayFire("subagent-frontmatter"))
|
|
4272
4422
|
return { issues: 0, errors: 0 };
|
|
4273
4423
|
if (!adapter.subagents) {
|
|
4274
4424
|
reportNotApplicable("Subagent-frontmatter check", "subagents", adapter, silent);
|
|
4275
4425
|
return { issues: 0, errors: 0 };
|
|
4276
4426
|
}
|
|
4277
|
-
let
|
|
4427
|
+
let raw;
|
|
4278
4428
|
try {
|
|
4279
4429
|
const r = ctx.scan();
|
|
4280
|
-
|
|
4430
|
+
raw = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
|
|
4281
4431
|
}
|
|
4282
4432
|
catch {
|
|
4283
4433
|
return { issues: 0, errors: 0 };
|
|
4284
4434
|
}
|
|
4435
|
+
const found = graded(ctx.rules, "subagent-frontmatter", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4285
4436
|
if (found.length > 0 && !silent) {
|
|
4286
4437
|
console.log("\nFrontmatter-schema check:\n");
|
|
4287
4438
|
for (const issue of found) {
|
|
4288
|
-
console.log(` ${sev
|
|
4289
|
-
ctx.annotate(sev
|
|
4439
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.label(issue.message)}`);
|
|
4440
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4290
4441
|
}
|
|
4291
4442
|
}
|
|
4292
|
-
return
|
|
4443
|
+
return tally(found);
|
|
4293
4444
|
}
|
|
4294
4445
|
/**
|
|
4295
4446
|
* Apply the `skill-frontmatter` rule: RECOMMEND (not require) that a SKILL.md
|
|
@@ -4300,54 +4451,25 @@ function checkFrontmatterSchema(ctx) {
|
|
|
4300
4451
|
* `skillMetaIssues`.
|
|
4301
4452
|
*/
|
|
4302
4453
|
function checkSkillFrontmatter(ctx) {
|
|
4303
|
-
const {
|
|
4304
|
-
|
|
4305
|
-
if (!sev)
|
|
4454
|
+
const { silent } = ctx;
|
|
4455
|
+
if (!ctx.rules.mayFire("skill-frontmatter"))
|
|
4306
4456
|
return { issues: 0, errors: 0 };
|
|
4307
|
-
let
|
|
4457
|
+
let raw;
|
|
4308
4458
|
try {
|
|
4309
|
-
|
|
4459
|
+
raw = ctx.scan().skillMetaIssues;
|
|
4310
4460
|
}
|
|
4311
4461
|
catch {
|
|
4312
4462
|
return { issues: 0, errors: 0 };
|
|
4313
4463
|
}
|
|
4464
|
+
const found = graded(ctx.rules, "skill-frontmatter", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4314
4465
|
if (found.length > 0 && !silent) {
|
|
4315
4466
|
console.log("\nSkill-frontmatter check:\n");
|
|
4316
4467
|
for (const issue of found) {
|
|
4317
|
-
console.log(` ${sev
|
|
4318
|
-
ctx.annotate(sev
|
|
4468
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.label(issue.message)}`);
|
|
4469
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4319
4470
|
}
|
|
4320
4471
|
}
|
|
4321
|
-
return
|
|
4322
|
-
}
|
|
4323
|
-
/**
|
|
4324
|
-
* Apply the `prefer-compiled-hooks` rule: a SINGLE repo-level recommendation
|
|
4325
|
-
* (one finding regardless of hook count) nudging hand-written hooks toward
|
|
4326
|
-
* compiled `vigiles/hook` artifacts. A discovery nudge, not a defect — the shell
|
|
4327
|
-
* lane stays first-class — so it fires once and the message links the guide.
|
|
4328
|
-
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
4329
|
-
*/
|
|
4330
|
-
function checkPreferCompiledHooks(ctx) {
|
|
4331
|
-
const { config, silent } = ctx;
|
|
4332
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
4333
|
-
if (!sev)
|
|
4334
|
-
return { issues: 0, errors: 0 };
|
|
4335
|
-
let count;
|
|
4336
|
-
try {
|
|
4337
|
-
count = ctx.scan().manualHookCount;
|
|
4338
|
-
}
|
|
4339
|
-
catch {
|
|
4340
|
-
return { issues: 0, errors: 0 };
|
|
4341
|
-
}
|
|
4342
|
-
if (count === 0)
|
|
4343
|
-
return { issues: 0, errors: 0 };
|
|
4344
|
-
const message = (0, scan_js_1.preferCompiledHooksMessage)(count);
|
|
4345
|
-
if (!silent) {
|
|
4346
|
-
console.log("\nCompiled-hooks check:\n");
|
|
4347
|
-
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${ctx.label(message)}`);
|
|
4348
|
-
ctx.annotate(sev === "error" ? "error" : "warning", message);
|
|
4349
|
-
}
|
|
4350
|
-
return { issues: 1, errors: sev === "error" ? 1 : 0 };
|
|
4472
|
+
return tally(found);
|
|
4351
4473
|
}
|
|
4352
4474
|
/**
|
|
4353
4475
|
* Apply the `mcp-config` rule: a declared MCP server with neither a `command`
|
|
@@ -4383,17 +4505,16 @@ function checkMcpConfig(ctx) {
|
|
|
4383
4505
|
* "error" gates CI.
|
|
4384
4506
|
*/
|
|
4385
4507
|
function checkDisallowedTools(ctx) {
|
|
4386
|
-
const {
|
|
4387
|
-
|
|
4388
|
-
if (!sev)
|
|
4508
|
+
const { silent, adapter } = ctx;
|
|
4509
|
+
if (!ctx.rules.mayFire("disallowed-tools-contract"))
|
|
4389
4510
|
return { issues: 0, errors: 0 };
|
|
4390
4511
|
if (!adapter.subagents) {
|
|
4391
4512
|
reportNotApplicable("Disallowed-tools check", "subagents", adapter, silent);
|
|
4392
4513
|
return { issues: 0, errors: 0 };
|
|
4393
4514
|
}
|
|
4394
|
-
let
|
|
4515
|
+
let raw;
|
|
4395
4516
|
try {
|
|
4396
|
-
|
|
4517
|
+
raw = ctx.scan().agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({
|
|
4397
4518
|
message: i.message,
|
|
4398
4519
|
path: a.path,
|
|
4399
4520
|
})));
|
|
@@ -4401,14 +4522,15 @@ function checkDisallowedTools(ctx) {
|
|
|
4401
4522
|
catch {
|
|
4402
4523
|
return { issues: 0, errors: 0 };
|
|
4403
4524
|
}
|
|
4525
|
+
const found = graded(ctx.rules, "disallowed-tools-contract", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4404
4526
|
if (found.length > 0 && !silent) {
|
|
4405
4527
|
console.log("\nDisallowed-tools check:\n");
|
|
4406
4528
|
for (const issue of found) {
|
|
4407
|
-
console.log(` ${sev
|
|
4408
|
-
ctx.annotate(sev
|
|
4529
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.bundle.scanned(issue.path)}: ${issue.message}`);
|
|
4530
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4409
4531
|
}
|
|
4410
4532
|
}
|
|
4411
|
-
return
|
|
4533
|
+
return tally(found);
|
|
4412
4534
|
}
|
|
4413
4535
|
/**
|
|
4414
4536
|
* Apply the `frontmatter-valid` rule: a skill/agent `---` block that EXISTS but
|
|
@@ -4419,25 +4541,25 @@ function checkDisallowedTools(ctx) {
|
|
|
4419
4541
|
* (verify before setting "error").
|
|
4420
4542
|
*/
|
|
4421
4543
|
function checkFrontmatterValid(ctx) {
|
|
4422
|
-
const {
|
|
4423
|
-
|
|
4424
|
-
if (!sev)
|
|
4544
|
+
const { silent } = ctx;
|
|
4545
|
+
if (!ctx.rules.mayFire("frontmatter-valid"))
|
|
4425
4546
|
return { issues: 0, errors: 0 };
|
|
4426
|
-
let
|
|
4547
|
+
let raw;
|
|
4427
4548
|
try {
|
|
4428
|
-
|
|
4549
|
+
raw = ctx.scan().malformedFrontmatter;
|
|
4429
4550
|
}
|
|
4430
4551
|
catch {
|
|
4431
4552
|
return { issues: 0, errors: 0 };
|
|
4432
4553
|
}
|
|
4554
|
+
const found = graded(ctx.rules, "frontmatter-valid", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4433
4555
|
if (found.length > 0 && !silent) {
|
|
4434
4556
|
console.log("\nFrontmatter-validity check:\n");
|
|
4435
4557
|
for (const issue of found) {
|
|
4436
|
-
console.log(` ${sev
|
|
4437
|
-
ctx.annotate(sev
|
|
4558
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.label(issue.message)}`);
|
|
4559
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4438
4560
|
}
|
|
4439
4561
|
}
|
|
4440
|
-
return
|
|
4562
|
+
return tally(found);
|
|
4441
4563
|
}
|
|
4442
4564
|
/**
|
|
4443
4565
|
* Apply the `description-overlap` rule: two model-invocable skills with
|
|
@@ -4512,77 +4634,81 @@ function checkDescriptionBudget(ctx) {
|
|
|
4512
4634
|
* unaffected by the collapse.
|
|
4513
4635
|
*/
|
|
4514
4636
|
function checkLethalTrifecta(ctx) {
|
|
4515
|
-
const {
|
|
4516
|
-
|
|
4517
|
-
if (!sev)
|
|
4637
|
+
const { silent } = ctx;
|
|
4638
|
+
if (!ctx.rules.mayFire("lethal-trifecta"))
|
|
4518
4639
|
return { issues: 0, errors: 0 };
|
|
4519
|
-
let
|
|
4640
|
+
let raw;
|
|
4520
4641
|
try {
|
|
4521
|
-
|
|
4642
|
+
raw = ctx.scan().trifectaFindings;
|
|
4522
4643
|
}
|
|
4523
4644
|
catch {
|
|
4524
4645
|
return { issues: 0, errors: 0 };
|
|
4525
4646
|
}
|
|
4647
|
+
const found = graded(ctx.rules, "lethal-trifecta", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4526
4648
|
if (found.length > 0 && !silent) {
|
|
4527
|
-
const mark = sev === "error" ? "✗" : "⚠";
|
|
4528
|
-
const level = sev === "error" ? "error" : "warning";
|
|
4529
4649
|
console.log("\nLethal-trifecta check:\n");
|
|
4530
4650
|
const unfenced = found.filter((t) => t.kind === "skill" && t.finding.fence === "none");
|
|
4531
4651
|
for (const t of found) {
|
|
4532
4652
|
if (unfenced.includes(t))
|
|
4533
4653
|
continue;
|
|
4534
4654
|
const msg = `${t.kind} ${t.name}: ${t.finding.message}`;
|
|
4535
|
-
console.log(` ${mark} ${ctx.bundle.scanned(t.path)}: ${msg}`);
|
|
4536
|
-
ctx.annotate(level, msg, ctx.bundle.scanned(t.path));
|
|
4655
|
+
console.log(` ${markOf(t.sev).mark} ${ctx.bundle.scanned(t.path)}: ${msg}`);
|
|
4656
|
+
ctx.annotate(markOf(t.sev).level, msg, ctx.bundle.scanned(t.path));
|
|
4537
4657
|
}
|
|
4538
4658
|
if (unfenced.length > 0) {
|
|
4539
|
-
|
|
4659
|
+
// One line for the group, at the worst severity any of its skills has.
|
|
4660
|
+
const groupSev = unfenced.some((t) => t.sev === "error")
|
|
4661
|
+
? "error"
|
|
4662
|
+
: "warn";
|
|
4663
|
+
console.log(` ${markOf(groupSev).mark} ${String(unfenced.length)} skill(s) declare no \`disallowed-tools:\` fence, so each ` +
|
|
4540
4664
|
`inherits every tool the session grants and holds all three legs. ` +
|
|
4541
4665
|
`\`allowed-tools:\` pre-approves, it does not restrict. ` +
|
|
4542
4666
|
`Add one \`disallowed-tools:\` line per skill to drop a leg: ` +
|
|
4543
4667
|
unfenced.map((t) => t.name).join(", "));
|
|
4544
4668
|
}
|
|
4545
4669
|
}
|
|
4546
|
-
return
|
|
4670
|
+
return tally(found);
|
|
4547
4671
|
}
|
|
4672
|
+
/** The rules of the set that are modules — each runs through {@link checkRuleModule}. */
|
|
4673
|
+
const RULE_MODULES = index_js_1.RULES.filter((r) => "findings" in r);
|
|
4548
4674
|
/**
|
|
4549
|
-
* Apply
|
|
4550
|
-
*
|
|
4551
|
-
*
|
|
4552
|
-
*
|
|
4553
|
-
*
|
|
4675
|
+
* Apply one rule MODULE: what its `findings` reports, each at the severity its
|
|
4676
|
+
* own path gets (`overrides` applied). A `finding` counts and prints at that
|
|
4677
|
+
* severity; a `note` (informational — e.g. a resource that resolves only from
|
|
4678
|
+
* the repository root, #382) prints with `ℹ` and a `::notice` annotation and
|
|
4679
|
+
* never counts, so it can never fail CI. The scan is the same one every other
|
|
4680
|
+
* check reads (`ctx.scan`), with the repo's `sharedDirs` at the frame root.
|
|
4681
|
+
*
|
|
4682
|
+
* The first piece of the one loop the per-rule `check*` functions become
|
|
4683
|
+
* (vigiles#390): nothing here names a rule.
|
|
4554
4684
|
*/
|
|
4555
|
-
function
|
|
4556
|
-
|
|
4557
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-resource-resolves"]);
|
|
4558
|
-
if (!sev)
|
|
4685
|
+
function checkRuleModule(ctx, rule) {
|
|
4686
|
+
if (!ctx.rules.mayFire(rule.id))
|
|
4559
4687
|
return { issues: 0, errors: 0 };
|
|
4560
4688
|
let found;
|
|
4561
4689
|
try {
|
|
4562
|
-
found = ctx.scan({
|
|
4563
|
-
sharedDirs: config?.sharedDirs,
|
|
4564
|
-
// sharedDirs live at the frame root — the repo that OWNS the scan target:
|
|
4565
|
-
// cwd for a scoped subdir or a nested bundle, the target for a foreign lint.
|
|
4690
|
+
found = rule.findings(ctx.scan({
|
|
4691
|
+
sharedDirs: ctx.config?.sharedDirs,
|
|
4566
4692
|
sharedDirsRoot: ctx.frame.root,
|
|
4567
|
-
})
|
|
4693
|
+
}));
|
|
4568
4694
|
}
|
|
4569
4695
|
catch {
|
|
4570
4696
|
return { issues: 0, errors: 0 };
|
|
4571
4697
|
}
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
console.log(` ${
|
|
4582
|
-
ctx.annotate(
|
|
4698
|
+
const pathOf = (f) => ctx.bundle.scanned(f.path);
|
|
4699
|
+
const shown = graded(ctx.rules, rule.id, found, pathOf);
|
|
4700
|
+
if (shown.length > 0 && !ctx.silent) {
|
|
4701
|
+
console.log(`\n${rule.id}:\n`);
|
|
4702
|
+
for (const f of shown) {
|
|
4703
|
+
const { mark, level } = f.kind === "note"
|
|
4704
|
+
? { mark: "ℹ", level: "notice" }
|
|
4705
|
+
: markOf(f.sev);
|
|
4706
|
+
const msg = `${f.subject}: ${f.message}`;
|
|
4707
|
+
console.log(` ${mark} ${pathOf(f)}: ${msg}`);
|
|
4708
|
+
ctx.annotate(level, msg, pathOf(f));
|
|
4583
4709
|
}
|
|
4584
4710
|
}
|
|
4585
|
-
return
|
|
4711
|
+
return tally(shown.filter((f) => f.kind === "finding"));
|
|
4586
4712
|
}
|
|
4587
4713
|
/**
|
|
4588
4714
|
* Apply the `skill-missing-fence` rule: a SKILL.md that opens with
|
|
@@ -4592,26 +4718,26 @@ function checkSkillResourceResolves(ctx) {
|
|
|
4592
4718
|
* default; "error" gates CI.
|
|
4593
4719
|
*/
|
|
4594
4720
|
function checkSkillMissingFence(ctx) {
|
|
4595
|
-
const {
|
|
4596
|
-
|
|
4597
|
-
if (!sev)
|
|
4721
|
+
const { silent } = ctx;
|
|
4722
|
+
if (!ctx.rules.mayFire("skill-missing-fence"))
|
|
4598
4723
|
return { issues: 0, errors: 0 };
|
|
4599
|
-
let
|
|
4724
|
+
let raw;
|
|
4600
4725
|
try {
|
|
4601
|
-
|
|
4726
|
+
raw = ctx.scan().skillFenceIssues;
|
|
4602
4727
|
}
|
|
4603
4728
|
catch {
|
|
4604
4729
|
return { issues: 0, errors: 0 };
|
|
4605
4730
|
}
|
|
4731
|
+
const found = graded(ctx.rules, "skill-missing-fence", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4606
4732
|
if (found.length > 0 && !silent) {
|
|
4607
4733
|
console.log("\nSkill-missing-fence check:\n");
|
|
4608
4734
|
for (const s of found) {
|
|
4609
4735
|
const msg = `${s.name}: ${s.finding.message}`;
|
|
4610
|
-
console.log(` ${sev
|
|
4611
|
-
ctx.annotate(sev
|
|
4736
|
+
console.log(` ${markOf(s.sev).mark} ${ctx.bundle.scanned(s.path)}: ${msg}`);
|
|
4737
|
+
ctx.annotate(markOf(s.sev).level, msg, ctx.bundle.scanned(s.path));
|
|
4612
4738
|
}
|
|
4613
4739
|
}
|
|
4614
|
-
return
|
|
4740
|
+
return tally(found);
|
|
4615
4741
|
}
|
|
4616
4742
|
/**
|
|
4617
4743
|
* Apply the `plugin-dir-layout` rule: functional surface dirs (skills/agents/
|
|
@@ -4621,25 +4747,25 @@ function checkSkillMissingFence(ctx) {
|
|
|
4621
4747
|
* gates CI.
|
|
4622
4748
|
*/
|
|
4623
4749
|
function checkPluginDirLayout(ctx) {
|
|
4624
|
-
const {
|
|
4625
|
-
|
|
4626
|
-
if (!sev)
|
|
4750
|
+
const { silent } = ctx;
|
|
4751
|
+
if (!ctx.rules.mayFire("plugin-dir-layout"))
|
|
4627
4752
|
return { issues: 0, errors: 0 };
|
|
4628
|
-
let
|
|
4753
|
+
let raw;
|
|
4629
4754
|
try {
|
|
4630
|
-
|
|
4755
|
+
raw = ctx.scan().pluginLayoutIssues;
|
|
4631
4756
|
}
|
|
4632
4757
|
catch {
|
|
4633
4758
|
return { issues: 0, errors: 0 };
|
|
4634
4759
|
}
|
|
4760
|
+
const found = graded(ctx.rules, "plugin-dir-layout", raw, (f) => ctx.bundle.scanned(f.dir));
|
|
4635
4761
|
if (found.length > 0 && !silent) {
|
|
4636
4762
|
console.log("\nPlugin-dir-layout check:\n");
|
|
4637
4763
|
for (const p of found) {
|
|
4638
|
-
console.log(` ${sev
|
|
4639
|
-
ctx.annotate(sev
|
|
4764
|
+
console.log(` ${markOf(p.sev).mark} ${ctx.label(p.message)}`);
|
|
4765
|
+
ctx.annotate(markOf(p.sev).level, p.message);
|
|
4640
4766
|
}
|
|
4641
4767
|
}
|
|
4642
|
-
return
|
|
4768
|
+
return tally(found);
|
|
4643
4769
|
}
|
|
4644
4770
|
/**
|
|
4645
4771
|
* Apply the `delegation-trifecta` rule: a lethal trifecta that EMERGES across a
|
|
@@ -4650,26 +4776,26 @@ function checkPluginDirLayout(ctx) {
|
|
|
4650
4776
|
* capability the way a surface-specific rule is.
|
|
4651
4777
|
*/
|
|
4652
4778
|
function checkDelegationTrifecta(ctx) {
|
|
4653
|
-
const {
|
|
4654
|
-
|
|
4655
|
-
if (!sev)
|
|
4779
|
+
const { silent } = ctx;
|
|
4780
|
+
if (!ctx.rules.mayFire("delegation-trifecta"))
|
|
4656
4781
|
return { issues: 0, errors: 0 };
|
|
4657
|
-
let
|
|
4782
|
+
let raw;
|
|
4658
4783
|
try {
|
|
4659
|
-
|
|
4784
|
+
raw = ctx.scan().delegationTrifecta;
|
|
4660
4785
|
}
|
|
4661
4786
|
catch {
|
|
4662
4787
|
return { issues: 0, errors: 0 };
|
|
4663
4788
|
}
|
|
4789
|
+
const found = graded(ctx.rules, "delegation-trifecta", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4664
4790
|
if (found.length > 0 && !silent) {
|
|
4665
4791
|
console.log("\nDelegation-trifecta check:\n");
|
|
4666
4792
|
for (const d of found) {
|
|
4667
4793
|
const msg = `${d.finding.name}: ${d.finding.message}`;
|
|
4668
|
-
console.log(` ${sev
|
|
4669
|
-
ctx.annotate(sev
|
|
4794
|
+
console.log(` ${markOf(d.sev).mark} ${ctx.bundle.scanned(d.path)}: ${msg}`);
|
|
4795
|
+
ctx.annotate(markOf(d.sev).level, msg, ctx.bundle.scanned(d.path));
|
|
4670
4796
|
}
|
|
4671
4797
|
}
|
|
4672
|
-
return
|
|
4798
|
+
return tally(found);
|
|
4673
4799
|
}
|
|
4674
4800
|
/**
|
|
4675
4801
|
* Apply the `hook-block-ineffective` rule: a hook that LOOKS like it blocks but
|
|
@@ -4787,43 +4913,57 @@ function checkMcpHookTargets(ctx) {
|
|
|
4787
4913
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
4788
4914
|
}
|
|
4789
4915
|
/**
|
|
4790
|
-
* Apply the `hook-script-exists` rule: a
|
|
4791
|
-
*
|
|
4792
|
-
*
|
|
4793
|
-
*
|
|
4794
|
-
* existence-guarded one-liners, inline commands). Matches Anthropic's own
|
|
4795
|
-
* `claude plugin validate`. Warning by default; "error" gates CI.
|
|
4916
|
+
* Apply the `hook-script-exists` rule: a file a COMPILED hook line needs is
|
|
4917
|
+
* missing — the runtime, the program or its stamp — so the hook cannot run.
|
|
4918
|
+
* The files come from the hook-line plan (`hookLinePlan`), never from reading a
|
|
4919
|
+
* line: a line is compiled only by identity with what `compile` writes.
|
|
4796
4920
|
*/
|
|
4797
4921
|
function checkHookScriptExists(ctx) {
|
|
4922
|
+
return reportHookLines(ctx, "hook-script-exists", "Hook-script existence check", (scan) => [
|
|
4923
|
+
...scan.hooks
|
|
4924
|
+
.filter((h) => h.status === "missing")
|
|
4925
|
+
// `script` is absolute (the root variable resolved) — printed as-is it
|
|
4926
|
+
// was a machine path (#281); `scanned` makes it the bundle's own.
|
|
4927
|
+
.map((h) => (0, scan_js_1.hookScriptMissingMessage)(ctx.bundle.scanned(h.script))),
|
|
4928
|
+
// A line vigiles wrote that is now wrong: its own wiring, so here.
|
|
4929
|
+
...scan.staleHooks.map(scan_js_1.staleHookMessage),
|
|
4930
|
+
]);
|
|
4931
|
+
}
|
|
4932
|
+
/**
|
|
4933
|
+
* Apply the `hand-written-hook` rule: one finding per hook line vigiles did not
|
|
4934
|
+
* write (or wrote and someone edited). vigiles never parses such a line, so it
|
|
4935
|
+
* cannot say what it runs; the finding names it and points at compiled hooks.
|
|
4936
|
+
*/
|
|
4937
|
+
function checkHandWrittenHooks(ctx) {
|
|
4938
|
+
return reportHookLines(ctx, "hand-written-hook", "Hand-written hooks", (scan) => scan.handWrittenHooks.map(scan_js_1.handWrittenHookMessage));
|
|
4939
|
+
}
|
|
4940
|
+
/** Print and count one hook-line rule's messages at its configured severity. */
|
|
4941
|
+
function reportHookLines(ctx, rule, heading, messagesOf) {
|
|
4798
4942
|
const { config, silent, adapter } = ctx;
|
|
4799
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.[
|
|
4943
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.[rule]);
|
|
4800
4944
|
if (!sev)
|
|
4801
4945
|
return { issues: 0, errors: 0 };
|
|
4802
4946
|
if (!adapter.shellHooks) {
|
|
4803
|
-
reportNotApplicable(
|
|
4947
|
+
reportNotApplicable(heading, "shell hooks", adapter, silent);
|
|
4804
4948
|
return { issues: 0, errors: 0 };
|
|
4805
4949
|
}
|
|
4806
|
-
let
|
|
4950
|
+
let messages;
|
|
4807
4951
|
try {
|
|
4808
|
-
|
|
4952
|
+
messages = messagesOf(ctx.scan());
|
|
4809
4953
|
}
|
|
4810
4954
|
catch {
|
|
4811
4955
|
return { issues: 0, errors: 0 };
|
|
4812
4956
|
}
|
|
4813
|
-
if (
|
|
4814
|
-
console.log(
|
|
4815
|
-
for (const
|
|
4816
|
-
// `script` arrives with the plugin-root token EXPANDED, i.e. absolute —
|
|
4817
|
-
// printed as-is it was a machine path (#281). A relative one is the
|
|
4818
|
-
// bundle's own, which is what `scanned` assumes.
|
|
4819
|
-
const msg = `hook script "${ctx.bundle.scanned(h.script)}" is referenced but missing — the hook never runs.`;
|
|
4957
|
+
if (messages.length > 0 && !silent) {
|
|
4958
|
+
console.log(`\n${heading}:\n`);
|
|
4959
|
+
for (const msg of messages) {
|
|
4820
4960
|
console.log(` ${sev === "error" ? "✗" : "⚠"} ${ctx.label(msg)}`);
|
|
4821
4961
|
ctx.annotate(sev === "error" ? "error" : "warning", msg);
|
|
4822
4962
|
}
|
|
4823
4963
|
}
|
|
4824
4964
|
return {
|
|
4825
|
-
issues:
|
|
4826
|
-
errors: sev === "error" ?
|
|
4965
|
+
issues: messages.length,
|
|
4966
|
+
errors: sev === "error" ? messages.length : 0,
|
|
4827
4967
|
};
|
|
4828
4968
|
}
|
|
4829
4969
|
/**
|
|
@@ -4834,31 +4974,31 @@ function checkHookScriptExists(ctx) {
|
|
|
4834
4974
|
* plugin-namespaced form skipped). Warning by default; "error" gates CI.
|
|
4835
4975
|
*/
|
|
4836
4976
|
function checkMcpToolResolves(ctx) {
|
|
4837
|
-
const {
|
|
4838
|
-
|
|
4839
|
-
if (!sev)
|
|
4977
|
+
const { silent, adapter } = ctx;
|
|
4978
|
+
if (!ctx.rules.mayFire("mcp-tool-resolves"))
|
|
4840
4979
|
return { issues: 0, errors: 0 };
|
|
4841
4980
|
if (!adapter.subagents) {
|
|
4842
4981
|
reportNotApplicable("MCP tool-resolution check", "subagents", adapter, silent);
|
|
4843
4982
|
return { issues: 0, errors: 0 };
|
|
4844
4983
|
}
|
|
4845
|
-
let
|
|
4984
|
+
let raw;
|
|
4846
4985
|
try {
|
|
4847
|
-
|
|
4986
|
+
raw = ctx
|
|
4848
4987
|
.scan()
|
|
4849
4988
|
.agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
4850
4989
|
}
|
|
4851
4990
|
catch {
|
|
4852
4991
|
return { issues: 0, errors: 0 };
|
|
4853
4992
|
}
|
|
4993
|
+
const found = graded(ctx.rules, "mcp-tool-resolves", raw, (f) => ctx.bundle.scanned(f.path));
|
|
4854
4994
|
if (found.length > 0 && !silent) {
|
|
4855
4995
|
console.log("\nMCP tool-resolution check:\n");
|
|
4856
4996
|
for (const issue of found) {
|
|
4857
|
-
console.log(` ${sev
|
|
4858
|
-
ctx.annotate(sev
|
|
4997
|
+
console.log(` ${markOf(issue.sev).mark} ${ctx.bundle.scanned(issue.path)}: ${issue.message}`);
|
|
4998
|
+
ctx.annotate(markOf(issue.sev).level, issue.message, ctx.bundle.scanned(issue.path));
|
|
4859
4999
|
}
|
|
4860
5000
|
}
|
|
4861
|
-
return
|
|
5001
|
+
return tally(found);
|
|
4862
5002
|
}
|
|
4863
5003
|
/**
|
|
4864
5004
|
* Apply the configured coverage thresholds. Returns the number of failing
|
|
@@ -5006,7 +5146,7 @@ function resolveCommandHarness(dir, harnessFlag) {
|
|
|
5006
5146
|
return (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
5007
5147
|
root: dir,
|
|
5008
5148
|
flag: harnessFlag,
|
|
5009
|
-
configHarness: (0, adapter_registry_js_1.declaredHarnessNames)(
|
|
5149
|
+
configHarness: (0, adapter_registry_js_1.declaredHarnessNames)(loadConfig().harnesses),
|
|
5010
5150
|
});
|
|
5011
5151
|
}
|
|
5012
5152
|
async function handleMeasure(restArgs, args) {
|
|
@@ -5082,6 +5222,17 @@ async function handleGenerate(restArgs, args) {
|
|
|
5082
5222
|
process.exit(2);
|
|
5083
5223
|
}
|
|
5084
5224
|
}
|
|
5225
|
+
/**
|
|
5226
|
+
* A linter whose config could not be read is missing from the generated types;
|
|
5227
|
+
* say so on stderr (and as a CI annotation) instead of a silent short list.
|
|
5228
|
+
*/
|
|
5229
|
+
function printUnreadableLinters(unreadable) {
|
|
5230
|
+
unreadable.forEach(({ linter, reason }) => {
|
|
5231
|
+
const msg = `could not read the ${linter} config: ${reason} (its rules are not listed)`;
|
|
5232
|
+
console.error(` ⚠ ${msg}`);
|
|
5233
|
+
console.error(`::warning::${msg}`);
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5085
5236
|
function handleGenerateTypes(args, restArgs) {
|
|
5086
5237
|
const checkOnly = args.includes("--check");
|
|
5087
5238
|
const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
|
|
@@ -5097,6 +5248,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
5097
5248
|
for (const l of result.linters) {
|
|
5098
5249
|
console.log(` ${l.linter}: ${String(l.rules.length)} enabled rules (via ${l.via})`);
|
|
5099
5250
|
}
|
|
5251
|
+
printUnreadableLinters(result.unreadable);
|
|
5100
5252
|
if (result.scripts.length > 0) {
|
|
5101
5253
|
console.log(` npm scripts: ${String(result.scripts.length)}`);
|
|
5102
5254
|
}
|
|
@@ -5138,7 +5290,7 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
5138
5290
|
console.log("Scanning linters...\n");
|
|
5139
5291
|
const result = (0, generate_schema_js_1.generateSchema)({
|
|
5140
5292
|
basePath: process.cwd(),
|
|
5141
|
-
linters:
|
|
5293
|
+
linters: loadConfig().linters,
|
|
5142
5294
|
});
|
|
5143
5295
|
for (const l of result.linters) {
|
|
5144
5296
|
console.log(` ${l.linter}: ${String(l.count)} rules`);
|
|
@@ -5321,7 +5473,7 @@ function resolveEvalLockEnv(args) {
|
|
|
5321
5473
|
if (wantUpdate)
|
|
5322
5474
|
env.VIGILES_EVAL_LOCK = "update";
|
|
5323
5475
|
if (wantCheck || wantUpdate) {
|
|
5324
|
-
const apiVersion =
|
|
5476
|
+
const apiVersion = loadConfig().eval?.apiVersion;
|
|
5325
5477
|
if (apiVersion !== undefined)
|
|
5326
5478
|
env.VIGILES_EVAL_API_VERSION = String(apiVersion);
|
|
5327
5479
|
}
|
|
@@ -5417,7 +5569,7 @@ function resolveRecords(cwd, runs, tier, harnessFlag) {
|
|
|
5417
5569
|
}
|
|
5418
5570
|
return [...names];
|
|
5419
5571
|
}
|
|
5420
|
-
const recordsConfig =
|
|
5572
|
+
const recordsConfig = loadConfig();
|
|
5421
5573
|
const layout = harnessLayoutFor(cwd, recordsConfig, harnessFlag);
|
|
5422
5574
|
// The repo-wide `exclude` only — the per-rule severities/include stay out
|
|
5423
5575
|
// of record resolution on purpose (a run's probes must map to a surface
|
|
@@ -5751,6 +5903,13 @@ function capabilitiesOfReport(report, dialect) {
|
|
|
5751
5903
|
}));
|
|
5752
5904
|
return (0, generate_harness_js_1.computeHarnessCapabilities)(agents, dialect);
|
|
5753
5905
|
}
|
|
5906
|
+
/**
|
|
5907
|
+
* The `--preset=` line `audit` and `lint` share. The names are the ones
|
|
5908
|
+
* `.vigilesrc.json#extends` takes; `experimental` is the group of checks not yet
|
|
5909
|
+
* measured reliable enough to grade on — on at `warn`, never scored.
|
|
5910
|
+
*/
|
|
5911
|
+
const PRESET_HELP = " --preset=<name> add a preset for this run, on top of your config: experimental · recommended\n" +
|
|
5912
|
+
" (experimental = checks not yet measured reliable enough to grade you on: warn, never scored)";
|
|
5754
5913
|
const COMMAND_HELP = {
|
|
5755
5914
|
init: {
|
|
5756
5915
|
usage: " vigiles init [flags] Set up this repo — specs, plugin, and CI.",
|
|
@@ -5765,6 +5924,7 @@ const COMMAND_HELP = {
|
|
|
5765
5924
|
},
|
|
5766
5925
|
lint: {
|
|
5767
5926
|
usage: " vigiles lint [files...] Gate it in CI. The same checks — but a finding fails the build.",
|
|
5927
|
+
detail: [PRESET_HELP],
|
|
5768
5928
|
},
|
|
5769
5929
|
audit: {
|
|
5770
5930
|
// "Reports everything, fails nothing" states always-exit-0 as the FEATURE it is. The line
|
|
@@ -5775,6 +5935,7 @@ const COMMAND_HELP = {
|
|
|
5775
5935
|
detail: [
|
|
5776
5936
|
" 2+ dirs → a leaderboard. Writes vigiles-report.html + .json (auto-gitignored).",
|
|
5777
5937
|
" --single audit this dir as ONE harness, even if it holds many bundles",
|
|
5938
|
+
PRESET_HELP,
|
|
5778
5939
|
" The executing checks (run your hooks · live MCP · do skills fire?) run only",
|
|
5779
5940
|
" interactively — audit asks once and remembers; automation uses the testing API.",
|
|
5780
5941
|
],
|
|
@@ -6381,8 +6542,9 @@ function evalLockNudgeHookCommand() {
|
|
|
6381
6542
|
// editing session, and a malformed `.vigilesrc.json` must not turn a JSON typo
|
|
6382
6543
|
// into a failed edit: the nudge not firing is the cheaper failure. The verbs
|
|
6383
6544
|
// throw on the same config; what is CHECKED is identical.
|
|
6384
|
-
const config =
|
|
6385
|
-
const
|
|
6545
|
+
const config = loadConfig(cwd, { onInvalid: "warn" });
|
|
6546
|
+
const nudgeRules = (0, overrides_js_1.ruleResolver)(config.rules, config.overrides);
|
|
6547
|
+
const { options } = untestedRules(config, nudgeRules);
|
|
6386
6548
|
// 🔴 THE SAME LAYOUT `vigiles lint` RESOLVES, for the same reason as the config
|
|
6387
6549
|
// above. This used to pass `basePath` alone, so the detector fell back to the
|
|
6388
6550
|
// Claude Code layout: in a Codex repo whose surfaces live at
|
|
@@ -6397,9 +6559,8 @@ function evalLockNudgeHookCommand() {
|
|
|
6397
6559
|
// heard nothing at all, while a repo that already tests got reminded. That is
|
|
6398
6560
|
// backwards, and `untested-skill` already stated the missing half correctly;
|
|
6399
6561
|
// it just lived in `vigiles lint`, which someone has to run by hand.
|
|
6400
|
-
//
|
|
6401
|
-
//
|
|
6402
|
-
// vendored skill the linter would never list.
|
|
6562
|
+
// The repo-wide exclude `vigiles lint` applies (#192) — the nudge must not
|
|
6563
|
+
// report a vendored skill the linter would never list.
|
|
6403
6564
|
const excludes = (0, exclude_js_1.excludeSet)(cwd, config.exclude);
|
|
6404
6565
|
// 🔴 THE BUNDLE THAT HOLDS THE EDIT, found the way `lint` finds it (#281, D6).
|
|
6405
6566
|
// The nudge scanned the root bundle only and matched by SUFFIX, so an edit to
|
|
@@ -6416,13 +6577,18 @@ function evalLockNudgeHookCommand() {
|
|
|
6416
6577
|
return r !== "" && !r.startsWith("..") && !(0, node_path_1.isAbsolute)(r);
|
|
6417
6578
|
})
|
|
6418
6579
|
: undefined) ?? cwd;
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6580
|
+
// The same per-path answer `lint` gives: no untested nudge for a skill whose
|
|
6581
|
+
// folder an `overrides` entry turned `untested-skill` off for.
|
|
6582
|
+
const untestedHere = nudgeRules.severityAt("untested-skill", frame.repo(edited)) !== false;
|
|
6583
|
+
const msg = (untestedHere
|
|
6584
|
+
? (0, test_coverage_js_1.skillTestNudge)(frame.repo(edited), {
|
|
6585
|
+
...options,
|
|
6586
|
+
basePath: home,
|
|
6587
|
+
root: cwd,
|
|
6588
|
+
layout,
|
|
6589
|
+
excludes,
|
|
6590
|
+
})
|
|
6591
|
+
: undefined) ?? (0, eval_lock_js_1.evalLockNudge)(target, (0, node_path_1.resolve)(cwd, eval_lock_js_1.DEFAULT_LOCK_DIR));
|
|
6426
6592
|
if (!msg)
|
|
6427
6593
|
return;
|
|
6428
6594
|
process.stdout.write(JSON.stringify({
|
|
@@ -6462,7 +6628,10 @@ async function refsHookCommand() {
|
|
|
6462
6628
|
// from the process's directory is how a disabled rule comes back to life.
|
|
6463
6629
|
const cwd = runtimeRoot(eventRoot(raw));
|
|
6464
6630
|
// A hook rail — see `evalLockNudgeHookCommand` for why it warns, not throws.
|
|
6465
|
-
|
|
6631
|
+
// Resolved at the edited file's own path, so an `overrides` entry that turns
|
|
6632
|
+
// `unmarked-refs` off for a folder is honoured at edit time too.
|
|
6633
|
+
const refsConfig = loadConfig(cwd, { onInvalid: "warn" });
|
|
6634
|
+
const severity = (0, overrides_js_1.ruleResolver)(refsConfig.rules, refsConfig.overrides).severityAt("unmarked-refs", (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)));
|
|
6466
6635
|
if (severity === false)
|
|
6467
6636
|
return;
|
|
6468
6637
|
const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
|
|
@@ -6542,7 +6711,7 @@ async function compileProviders() {
|
|
|
6542
6711
|
* the string the user typed — `x.hook.ts` and `./x.hook.ts` are the same wiring),
|
|
6543
6712
|
* so recompiling updates in place and never clobbers the user's own hooks.
|
|
6544
6713
|
*/
|
|
6545
|
-
async function installHookFile(file, adapter, registeredProviders
|
|
6714
|
+
async function installHookFile(file, adapter, registeredProviders, runtime) {
|
|
6546
6715
|
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
|
|
6547
6716
|
const program = await (0, hook_runtime_js_1.loadHookProgram)(file);
|
|
6548
6717
|
// Emit the CANONICAL path so two spellings of the same file produce the same
|
|
@@ -6561,16 +6730,17 @@ async function installHookFile(file, adapter, registeredProviders = []) {
|
|
|
6561
6730
|
// cache, thirteen times, on every tool call. The `|| exit N` ending is what
|
|
6562
6731
|
// the shell does when that file cannot start at all. `compileHookProgram`
|
|
6563
6732
|
// adds it (see `hookRuntimeMissingExit`), so it is not written here.
|
|
6564
|
-
gateCommand:
|
|
6565
|
-
(0, hook_install_js_1.hookGateRef)(ref, adapter.layout.projectRootTokens),
|
|
6733
|
+
gateCommand: (0, hook_command_js_1.runProgramCommand)((0, hook_command_js_1.hookRuntimeRef)(adapter.layout.projectRootTokens, runtime), (0, hook_command_js_1.hookGateRef)(ref, adapter.layout.projectRootTokens)),
|
|
6566
6734
|
dialect: adapter.dialect,
|
|
6567
6735
|
hookProtocol: adapter.hookProtocol,
|
|
6568
6736
|
settings: adapter.layout.settings,
|
|
6569
6737
|
registeredProviders,
|
|
6570
6738
|
});
|
|
6571
|
-
// Tamper-evident stamp beside the source (one dir → basename is unique).
|
|
6739
|
+
// Tamper-evident stamp beside the source (one dir → basename is unique). The
|
|
6740
|
+
// role is recorded with it: `lint` rebuilds the exact line compile writes
|
|
6741
|
+
// from it (`hookLinePlan`), so a wired line is read by identity, not parsed.
|
|
6572
6742
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)((0, hook_runtime_js_1.hookStampPath)(file)), { recursive: true });
|
|
6573
|
-
(0, node_fs_1.writeFileSync)((0, hook_runtime_js_1.hookStampPath)(file), JSON.stringify({ file, stamp: compiled.stamp }, null, 2) + "\n");
|
|
6743
|
+
(0, node_fs_1.writeFileSync)((0, hook_runtime_js_1.hookStampPath)(file), JSON.stringify({ file, stamp: compiled.stamp, role: (0, hook_program_js_1.dispatchKind)(program) }, null, 2) + "\n");
|
|
6574
6744
|
// Merge into the harness's native config, idempotently.
|
|
6575
6745
|
//
|
|
6576
6746
|
// 🔴 THREE FORMAT BRANCHES BECAME ZERO, and they were three because the ONE
|
|
@@ -6646,7 +6816,14 @@ async function installHookFile(file, adapter, registeredProviders = []) {
|
|
|
6646
6816
|
`yet automatic).`;
|
|
6647
6817
|
}
|
|
6648
6818
|
}
|
|
6649
|
-
|
|
6819
|
+
// 🔴 `compiled.warnings` WAS COMPUTED AND NEVER PRINTED until 2026-10-11
|
|
6820
|
+
// (#356): a degraded role/event fit reached the author's terminal by no
|
|
6821
|
+
// path at all — a nudge with no delivery, inside the check for exactly that.
|
|
6822
|
+
return {
|
|
6823
|
+
role,
|
|
6824
|
+
settingsPath: adapter.layout.settingsPath,
|
|
6825
|
+
warnings: [...(compiled.warnings ?? []), ...(warning ? [warning] : [])],
|
|
6826
|
+
};
|
|
6650
6827
|
}
|
|
6651
6828
|
/**
|
|
6652
6829
|
* Compile + install every hook (explicit paths, else discovered under
|
|
@@ -6680,15 +6857,29 @@ async function installHooks(hookFiles, harnessFlag, configHarness, validateProvi
|
|
|
6680
6857
|
}
|
|
6681
6858
|
if (hookFiles.length === 0)
|
|
6682
6859
|
return true; // providers validated; nothing to wire
|
|
6860
|
+
// WHICH runtime the wiring starts — the installed package for a consumer, the
|
|
6861
|
+
// package's own built bin when this IS vigiles (see `hookRuntimeFor`).
|
|
6862
|
+
const runtime = (0, hook_install_js_1.readHookRuntime)(process.cwd());
|
|
6863
|
+
// 🔴 SAY IT NOW, because nothing will later. A nudge whose runtime cannot
|
|
6864
|
+
// start ends in `|| exit 0` by design, so from then on it is indistinguishable
|
|
6865
|
+
// from a nudge with nothing to say. Measured 2026-10-11 on this repository:
|
|
6866
|
+
// dead for three weeks, every gate green. Not an error — the file may arrive
|
|
6867
|
+
// with the next install or build — but never silent.
|
|
6868
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), runtime.path))) {
|
|
6869
|
+
console.warn(`⚠ the hook runtime ${runtime.path} is not on disk — every hook compiled ` +
|
|
6870
|
+
`here fails to load until it is: a reminder is skipped without a word, a ` +
|
|
6871
|
+
`gate refuses every command. Run ${runtime.cure}.`);
|
|
6872
|
+
}
|
|
6683
6873
|
let ok = true;
|
|
6684
6874
|
for (const file of hookFiles) {
|
|
6685
6875
|
try {
|
|
6686
6876
|
// Fan out: the same compiled hook lands in each enabled harness's config.
|
|
6687
6877
|
for (const adapter of adapters) {
|
|
6688
|
-
const r = await installHookFile(file, adapter, registeredProviders);
|
|
6878
|
+
const r = await installHookFile(file, adapter, registeredProviders, runtime);
|
|
6689
6879
|
console.log(`✓ ${file} → ${r.settingsPath} (role: ${r.role}, harness: ${adapter.name})`);
|
|
6690
|
-
|
|
6691
|
-
console.warn(`⚠ ${
|
|
6880
|
+
r.warnings.forEach((w) => {
|
|
6881
|
+
console.warn(`⚠ ${w}`);
|
|
6882
|
+
});
|
|
6692
6883
|
}
|
|
6693
6884
|
}
|
|
6694
6885
|
catch (e) {
|
|
@@ -7091,6 +7282,30 @@ async function askOnce(q) {
|
|
|
7091
7282
|
rl.close();
|
|
7092
7283
|
}
|
|
7093
7284
|
}
|
|
7285
|
+
/**
|
|
7286
|
+
* 🔴 THE PROJECT ROOT, resolved ONCE (#371) — the only place the CLI asks where
|
|
7287
|
+
* it was started. Every command then runs from the root, so `vigiles lint` in
|
|
7288
|
+
* `src/` reads the root's `.vigilesrc.json`, its overrides and its compiled
|
|
7289
|
+
* files exactly as it does at the root, and passes the root on explicitly. A
|
|
7290
|
+
* path the user typed means "from where I typed it", so path arguments are
|
|
7291
|
+
* re-expressed from the root first (`PATH_ARGUMENTS` says which arguments are
|
|
7292
|
+
* paths). `hook-runtime` is exempt: its cwd and argv come from the harness.
|
|
7293
|
+
*/
|
|
7294
|
+
function enterProjectRoot() {
|
|
7295
|
+
const typed = process.argv.slice(2);
|
|
7296
|
+
const [command = "", ...rest] = typed;
|
|
7297
|
+
const invocationDir = process.cwd();
|
|
7298
|
+
if (!(0, cli_flag_check_js_1.runsFromProjectRoot)(command))
|
|
7299
|
+
return { command, projectRoot: invocationDir, args: typed };
|
|
7300
|
+
const projectRoot = (0, frame_js_1.findProjectRoot)(invocationDir, node_fs_1.existsSync);
|
|
7301
|
+
const args = [
|
|
7302
|
+
command,
|
|
7303
|
+
...(0, cli_flag_check_js_1.rebasePathArgs)(command, rest, (p) => (0, frame_js_1.rebaseArg)(p, invocationDir, projectRoot)),
|
|
7304
|
+
];
|
|
7305
|
+
if (projectRoot !== invocationDir)
|
|
7306
|
+
process.chdir(projectRoot);
|
|
7307
|
+
return { command, projectRoot, args };
|
|
7308
|
+
}
|
|
7094
7309
|
/**
|
|
7095
7310
|
* Persist the audit consent (`audit.measure`) into `.vigilesrc.json` without
|
|
7096
7311
|
* clobbering existing keys — the "ask once, remember" sticky choice. Best-effort:
|
|
@@ -7119,9 +7334,49 @@ function rememberAuditMeasure(value) {
|
|
|
7119
7334
|
/* non-fatal — the run already happened; only the remembered choice is lost */
|
|
7120
7335
|
}
|
|
7121
7336
|
}
|
|
7337
|
+
/**
|
|
7338
|
+
* The config this run uses: `.vigilesrc.json`, with any `--preset=<name>`
|
|
7339
|
+
* (audit, lint) applied ON TOP of what the file extends — through the one
|
|
7340
|
+
* validator `extends` uses, so an unknown name is "could not do what you
|
|
7341
|
+
* asked": exit 2, before anything runs. Then the shared flags (--max-rules,
|
|
7342
|
+
* --catalog-only) override it, so every GitHub Action input maps to a real CLI
|
|
7343
|
+
* flag (src/cli-flags.ts).
|
|
7344
|
+
*/
|
|
7345
|
+
function loadRunConfig(projectRoot, args) {
|
|
7346
|
+
const presetFlag = (0, config_schema_js_2.parsePresetFlags)(CONFIG.presetNames, args
|
|
7347
|
+
.filter((a) => a.startsWith("--preset="))
|
|
7348
|
+
.map((a) => a.slice("--preset=".length)));
|
|
7349
|
+
if (!presetFlag.ok) {
|
|
7350
|
+
console.error(`✗ ${presetFlag.message}`);
|
|
7351
|
+
process.exit(2);
|
|
7352
|
+
}
|
|
7353
|
+
return (0, cli_flags_js_1.applyConfigFlags)(loadConfig(projectRoot, { presets: presetFlag.presets }), args);
|
|
7354
|
+
}
|
|
7355
|
+
/**
|
|
7356
|
+
* The scan `audit` reports on, with this run's config applied (`applyRules`):
|
|
7357
|
+
* an off rule's findings are gone before anything renders.
|
|
7358
|
+
*/
|
|
7359
|
+
function auditScan(target, adapter, config, at) {
|
|
7360
|
+
const scan = (0, scan_js_1.scanPlugin)(target, adapter.layout, adapter.dialect, {
|
|
7361
|
+
deprecatedKeyFiles: specsUsingKeyFiles(at.root, at.excludes.globIgnore),
|
|
7362
|
+
sharedDirs: config.sharedDirs,
|
|
7363
|
+
sharedDirsRoot: at.frameRoot,
|
|
7364
|
+
// `.vigilesrc.json#exclude` reaches surface DISCOVERY, not just the
|
|
7365
|
+
// instruction file. Measured 2026-09-21 before this line existed: a repo
|
|
7366
|
+
// with `{"exclude": [".claude"]}` and one skill at `.claude/skills/demo`
|
|
7367
|
+
// still printed `Skills (1): ✓ demo` and docked Safety to 90 for it — the
|
|
7368
|
+
// grade was computed over a tree the user had told the tool to ignore.
|
|
7369
|
+
excludes: at.excludes,
|
|
7370
|
+
// The repo owner's answer to "N skills no harness reads": these are
|
|
7371
|
+
// mine, grade them — each root under the harness whose layout reads
|
|
7372
|
+
// it, so no array order decides which half of the repo is seen.
|
|
7373
|
+
// `exclude` still wins over it; the walk drops an excluded path first.
|
|
7374
|
+
harnesses: at.harnesses,
|
|
7375
|
+
});
|
|
7376
|
+
return (0, apply_rules_js_1.applyRules)(index_js_1.RULES, index_js_1.RULE_GROUPS, (0, overrides_js_1.ruleResolver)(config.rules, config.overrides), scan);
|
|
7377
|
+
}
|
|
7122
7378
|
async function main() {
|
|
7123
|
-
const args =
|
|
7124
|
-
const command = args[0];
|
|
7379
|
+
const { command, projectRoot, args } = enterProjectRoot();
|
|
7125
7380
|
// `--version` / `-v` / `version` prints the version number, not the help
|
|
7126
7381
|
// banner — so `npx vigiles --version` reports e.g. `3.0.0`.
|
|
7127
7382
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -7154,13 +7409,11 @@ async function main() {
|
|
|
7154
7409
|
}
|
|
7155
7410
|
}
|
|
7156
7411
|
const restArgs = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
7157
|
-
|
|
7158
|
-
// every GitHub Action input maps to a real CLI flag. See src/cli-flags.ts.
|
|
7159
|
-
const config = (0, cli_flags_js_1.applyConfigFlags)((0, validate_js_1.loadConfig)(), args);
|
|
7412
|
+
const config = loadRunConfig(projectRoot, args);
|
|
7160
7413
|
// `.vigilesrc.json#exclude`, parsed ONCE here and threaded to every walk that
|
|
7161
7414
|
// polices the repo (src/exclude.ts). Built where the config is loaded so a
|
|
7162
7415
|
// command cannot re-derive it differently — the drift #192 measured.
|
|
7163
|
-
const excludes = (0, exclude_js_1.excludeSet)(
|
|
7416
|
+
const excludes = (0, exclude_js_1.excludeSet)(projectRoot, config.exclude);
|
|
7164
7417
|
switch (command) {
|
|
7165
7418
|
// --- Primary commands ---
|
|
7166
7419
|
case "init": {
|
|
@@ -7193,7 +7446,7 @@ async function main() {
|
|
|
7193
7446
|
// is refused below, out loud, with the command that fixes it.
|
|
7194
7447
|
const found = restArgs.length > 0
|
|
7195
7448
|
? (0, hook_install_js_1.partitionHookArgs)(restArgs.filter((f) => !f.endsWith(".spec.ts")))
|
|
7196
|
-
: (0, hook_install_js_1.discoveredHookArgs)(
|
|
7449
|
+
: (0, hook_install_js_1.discoveredHookArgs)(projectRoot);
|
|
7197
7450
|
const hooks = [...found.hooks];
|
|
7198
7451
|
const unclaimed = found.unclaimed;
|
|
7199
7452
|
if (specs.length === 0 &&
|
|
@@ -7228,8 +7481,8 @@ async function main() {
|
|
|
7228
7481
|
// committed baseline — shown at the moment of change. Report-only:
|
|
7229
7482
|
// `lint` is the gate (rule `instruction-weight`).
|
|
7230
7483
|
if (specs.length > 0) {
|
|
7231
|
-
const weightLine = (0, instruction_weight_ratchet_js_1.compileWeightLine)(
|
|
7232
|
-
root:
|
|
7484
|
+
const weightLine = (0, instruction_weight_ratchet_js_1.compileWeightLine)(projectRoot, (0, scan_js_1.measureInstructionWeight)(projectRoot, (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
7485
|
+
root: projectRoot,
|
|
7233
7486
|
flag: harnessFlag,
|
|
7234
7487
|
configHarness: (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses),
|
|
7235
7488
|
}).adapter));
|
|
@@ -7432,21 +7685,15 @@ async function main() {
|
|
|
7432
7685
|
}
|
|
7433
7686
|
// The same single owner of "relative to which directory" `lint` uses
|
|
7434
7687
|
// (#281): cwd, unless the audited dir is somebody else's repository.
|
|
7435
|
-
const frame = (0, frame_js_1.frameFor)(
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
// still printed `Skills (1): ✓ demo` and docked Safety to 90 for it — the
|
|
7444
|
-
// grade was computed over a tree the user had told the tool to ignore.
|
|
7688
|
+
const frame = (0, frame_js_1.frameFor)(projectRoot, root);
|
|
7689
|
+
// The resolved config, applied once at the source: what an off rule
|
|
7690
|
+
// found is gone from everything below (terminal, JSON, HTML).
|
|
7691
|
+
// The resolved config is applied once, at the source: what an off
|
|
7692
|
+
// rule found is gone from everything below (terminal, JSON, HTML).
|
|
7693
|
+
const report = auditScan(targets[0], adapter, config, {
|
|
7694
|
+
root,
|
|
7695
|
+
frameRoot: frame.root,
|
|
7445
7696
|
excludes,
|
|
7446
|
-
// The repo owner's answer to "N skills no harness reads": these are
|
|
7447
|
-
// mine, grade them — each root under the harness whose layout reads
|
|
7448
|
-
// it, so no array order decides which half of the repo is seen.
|
|
7449
|
-
// `exclude` still wins over it; the walk drops an excluded path first.
|
|
7450
7697
|
harnesses: scanHarnesses,
|
|
7451
7698
|
});
|
|
7452
7699
|
if (!json) {
|
|
@@ -7478,9 +7725,11 @@ async function main() {
|
|
|
7478
7725
|
// --target=skills/x/SKILL.md` named a file that does not exist from where
|
|
7479
7726
|
// you run it, and offered to adopt a skill `exclude` had removed.
|
|
7480
7727
|
const auditedBundle = frame.bundle(root);
|
|
7481
|
-
const adoptableSurfaces = discoverAdoptableForAudit(root,
|
|
7482
|
-
|
|
7483
|
-
|
|
7728
|
+
const adoptableSurfaces = discoverAdoptableForAudit(root, {
|
|
7729
|
+
instructionFile: adapter.layout.instructionFile,
|
|
7730
|
+
excludes,
|
|
7731
|
+
config,
|
|
7732
|
+
}).map((p) => auditedBundle.scanned(p));
|
|
7484
7733
|
// Read the local flight recorder ONCE — feeds both the JSON report
|
|
7485
7734
|
// (structured summary, the product boundary) and the terminal render.
|
|
7486
7735
|
const ledgerRecords = (0, observe_js_1.readObservations)(root);
|
|
@@ -7488,7 +7737,7 @@ async function main() {
|
|
|
7488
7737
|
// report is built (it's a pure read off the scan) because the report's
|
|
7489
7738
|
// `Evaluated` ring needs to know whether this run will ever ask the
|
|
7490
7739
|
// skill-firing question — see `firingMeasured` below.
|
|
7491
|
-
const isForeign = root !==
|
|
7740
|
+
const isForeign = root !== projectRoot;
|
|
7492
7741
|
// ONE read of "can we reach a model, and on whose bill", from the driving
|
|
7493
7742
|
// adapter. It replaces a name check OR-ed with one harness's env vars at
|
|
7494
7743
|
// four sites below, and the two hard-coded "authenticate the `claude` CLI"
|
|
@@ -7574,7 +7823,7 @@ async function main() {
|
|
|
7574
7823
|
console.error(formatNothingToAudit(root, adapter.name, market));
|
|
7575
7824
|
process.exit(2);
|
|
7576
7825
|
}
|
|
7577
|
-
const plan = (0, optimize_js_1.optimize)(report);
|
|
7826
|
+
const plan = (0, optimize_js_1.optimize)((0, apply_rules_js_1.gradedView)(report));
|
|
7578
7827
|
// The deterministic READ leads: rings + report + fixes + nudges print
|
|
7579
7828
|
// BEFORE the consent prompt, so a plain `audit` shows its findings first.
|
|
7580
7829
|
// (JSON stays silent until the single blob below, after consent.)
|
|
@@ -7583,6 +7832,8 @@ async function main() {
|
|
|
7583
7832
|
// shown before the detailed report so the headline signal leads.
|
|
7584
7833
|
console.log((0, audit_score_js_1.formatAuditScore)(sc));
|
|
7585
7834
|
console.log("");
|
|
7835
|
+
// The ungraded blocks come off the filtered scan — the same data
|
|
7836
|
+
// the JSON and HTML render — so the three cannot drift.
|
|
7586
7837
|
console.log((0, scan_js_1.formatScanReport)(report));
|
|
7587
7838
|
// Fold each finding's fix inline (replaces the former --fix-plan/--explain
|
|
7588
7839
|
// flags): the deterministic, free recommendation list under the report.
|
|
@@ -7601,8 +7852,7 @@ async function main() {
|
|
|
7601
7852
|
console.log("\n" + adoptNudge);
|
|
7602
7853
|
// A small behavioral nudge — the deterministic read can't tell whether
|
|
7603
7854
|
// skills actually FIRE; point at the interactive measure + the API.
|
|
7604
|
-
const fireNudge = formatTriggerNudge(
|
|
7605
|
-
.length);
|
|
7855
|
+
const fireNudge = formatTriggerNudge(surfaces.triggerableSkills);
|
|
7606
7856
|
if (fireNudge)
|
|
7607
7857
|
console.log("\n" + fireNudge);
|
|
7608
7858
|
}
|
|
@@ -7758,8 +8008,8 @@ async function main() {
|
|
|
7758
8008
|
// CI upload / a custom location). The dir is created if missing.
|
|
7759
8009
|
const outFlag = args.find((a) => a.startsWith("--out="));
|
|
7760
8010
|
const outDir = outFlag
|
|
7761
|
-
? (0, node_path_1.resolve)(
|
|
7762
|
-
:
|
|
8011
|
+
? (0, node_path_1.resolve)(projectRoot, outFlag.slice("--out=".length))
|
|
8012
|
+
: projectRoot;
|
|
7763
8013
|
const openReport = !args.includes("--no-open");
|
|
7764
8014
|
const wroteReports = [];
|
|
7765
8015
|
// The versioned JSON artifact — the upload/CI boundary (a hosted dashboard
|
|
@@ -7805,14 +8055,14 @@ async function main() {
|
|
|
7805
8055
|
// `--out` pointing elsewhere yields nothing to write rather than a
|
|
7806
8056
|
// dead `../../..` entry.
|
|
7807
8057
|
const rel = wroteReports
|
|
7808
|
-
.map((f) => (0, repo_path_js_1.repoRelative)(
|
|
8058
|
+
.map((f) => (0, repo_path_js_1.repoRelative)(projectRoot, (0, node_path_1.resolve)(outDir, f), {
|
|
7809
8059
|
relative: node_path_1.relative,
|
|
7810
8060
|
resolve: node_path_1.resolve,
|
|
7811
8061
|
isAbsolute: node_path_1.isAbsolute,
|
|
7812
8062
|
sep: node_path_1.sep,
|
|
7813
8063
|
}))
|
|
7814
8064
|
.filter((p) => p !== null);
|
|
7815
|
-
ensureReportGitignored(
|
|
8065
|
+
ensureReportGitignored(projectRoot, rel);
|
|
7816
8066
|
}
|
|
7817
8067
|
// A shareable deep-link for a public GitHub repo: the in-browser demo
|
|
7818
8068
|
// re-runs the audit LIVE for whoever opens it, so a local result is
|