vigiles 10.0.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +9 -0
- package/README.md +121 -86
- package/action.yml +13 -2
- package/dist/adapter-conformance.js +6 -0
- package/dist/adapter-registry.d.ts +20 -0
- package/dist/adapter-registry.js +27 -0
- package/dist/adapters/claude-code/dialect.js +15 -0
- package/dist/adapters/claude-code/hook-protocol.js +4 -0
- package/dist/adapters/claude-code/runtime.js +12 -0
- package/dist/adapters/codex/eval.js +3 -0
- package/dist/adapters/codex/hook-protocol.d.ts +9 -1
- package/dist/adapters/codex/hook-protocol.js +10 -0
- package/dist/adapters/codex/runtime.js +10 -0
- package/dist/adapters/opencode/runtime.js +4 -0
- package/dist/audit-report.d.ts +1 -1
- package/dist/audit-report.template.html +1 -1
- package/dist/audit-score.d.ts +19 -12
- package/dist/audit-score.js +65 -11
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +460 -29
- package/dist/core/CLAUDE.md.spec.d.ts +3 -0
- package/dist/core/CLAUDE.md.spec.js +26 -0
- package/dist/core/delegation-trifecta.d.ts +64 -0
- package/dist/core/delegation-trifecta.js +124 -0
- package/dist/core/dialect.d.ts +18 -0
- package/dist/core/hook-block-ineffective.d.ts +62 -0
- package/dist/core/hook-block-ineffective.js +153 -0
- package/dist/core/hook-matcher.d.ts +66 -0
- package/dist/core/hook-matcher.js +182 -0
- package/dist/core/hook-normalize.d.ts +43 -0
- package/dist/core/hook-normalize.js +78 -0
- package/dist/core/hook-protocol.d.ts +15 -0
- package/dist/core/lethal-trifecta.d.ts +100 -0
- package/dist/core/lethal-trifecta.js +197 -0
- package/dist/core/plugin-dir-layout.d.ts +30 -0
- package/dist/core/plugin-dir-layout.js +73 -0
- package/dist/core/rule-meta.d.ts +82 -0
- package/dist/core/rule-meta.js +266 -0
- package/dist/core/runtime.d.ts +20 -0
- package/dist/core/skill-missing-fence.d.ts +47 -0
- package/dist/core/skill-missing-fence.js +119 -0
- package/dist/core/skill-resources.d.ts +27 -0
- package/dist/core/skill-resources.js +167 -0
- package/dist/core/types.d.ts +83 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +26 -4
- package/dist/eval-cache.d.ts +6 -0
- package/dist/eval-cache.js +2 -0
- package/dist/eval-lock.d.ts +192 -0
- package/dist/eval-lock.js +286 -0
- package/dist/eval.d.ts +33 -20
- package/dist/eval.js +199 -51
- package/dist/leaderboard.d.ts +1 -0
- package/dist/leaderboard.js +42 -4
- package/dist/scan.d.ts +106 -0
- package/dist/scan.js +251 -45
- package/dist/setup-plan.d.ts +43 -3
- package/dist/setup-plan.js +78 -6
- package/hooks/eval-lock-nudge.sh +21 -0
- package/package.json +1 -1
- package/skills/test-harness/SKILL.md +27 -0
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ const generate_types_js_1 = require("./core/generate-types.js");
|
|
|
17
17
|
const generate_harness_js_1 = require("./core/generate-harness.js");
|
|
18
18
|
const capability_diff_js_1 = require("./core/capability-diff.js");
|
|
19
19
|
const validate_js_1 = require("./core/validate.js");
|
|
20
|
+
const eval_lock_js_1 = require("./eval-lock.js");
|
|
20
21
|
const cli_flags_js_1 = require("./cli-flags.js");
|
|
21
22
|
const setup_plan_js_1 = require("./setup-plan.js");
|
|
22
23
|
const types_js_1 = require("./core/types.js");
|
|
@@ -678,6 +679,13 @@ function lintExitCode(report) {
|
|
|
678
679
|
report.frontmatterValidErrors > 0 ||
|
|
679
680
|
report.mcpHookErrors > 0 ||
|
|
680
681
|
report.preferCompiledHookErrors > 0 ||
|
|
682
|
+
report.lethalTrifectaErrors > 0 ||
|
|
683
|
+
report.skillResourceErrors > 0 ||
|
|
684
|
+
report.skillFenceErrors > 0 ||
|
|
685
|
+
report.pluginLayoutErrors > 0 ||
|
|
686
|
+
report.delegationTrifectaErrors > 0 ||
|
|
687
|
+
report.hookBlockErrors > 0 ||
|
|
688
|
+
report.hookMatcherErrors > 0 ||
|
|
681
689
|
report.symbolRefErrors > 0 ||
|
|
682
690
|
report.mcpRefErrors > 0)
|
|
683
691
|
return 2;
|
|
@@ -1028,6 +1036,29 @@ async function runLint(restArgs, flags, config) {
|
|
|
1028
1036
|
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
1029
1037
|
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
1030
1038
|
const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter);
|
|
1039
|
+
// 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
|
|
1040
|
+
// hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
|
|
1041
|
+
// prompt-injection exfil path (Rule of Two). Capability SET-intersection.
|
|
1042
|
+
const lethalTrifecta = checkLethalTrifecta(config, silent, adapter);
|
|
1043
|
+
// 7p. Skill-resource — a SKILL.md body referencing a bundled file that doesn't
|
|
1044
|
+
// exist on disk under the skill dir (the agent gets nothing). FP-safe.
|
|
1045
|
+
const skillResources = checkSkillResourceResolves(config, silent, adapter);
|
|
1046
|
+
// 7q. Skill-missing-fence — a SKILL.md opening with `name:`/`description:` but no
|
|
1047
|
+
// `---` fence loads as plain body (invisible — no name/description/trigger).
|
|
1048
|
+
const skillFence = checkSkillMissingFence(config, silent, adapter);
|
|
1049
|
+
// 7r. Plugin-dir-layout — functional surface dirs (skills/agents/commands) nested
|
|
1050
|
+
// inside the `.claude-plugin/` manifest dir where the harness can't see them.
|
|
1051
|
+
const pluginLayout = checkPluginDirLayout(config, silent, adapter);
|
|
1052
|
+
// 7s. Delegation-trifecta — a lethal trifecta that emerges across a delegation
|
|
1053
|
+
// edge (a subagent's own ∪ delegated-to capability) though no single unit trips it.
|
|
1054
|
+
const delegationTrifecta = checkDelegationTrifecta(config, silent, adapter);
|
|
1055
|
+
// 7t. Hook-block-ineffective — a hook that looks like it blocks but silently
|
|
1056
|
+
// doesn't (block decision on a non-blocking event, or the legacy `decision`
|
|
1057
|
+
// field on a permission-gated event). The #1 verified hook pain (#19009).
|
|
1058
|
+
const hookBlock = checkHookBlockIneffective(config, silent, adapter);
|
|
1059
|
+
// 7u. Hook-matcher — a hook `matcher` that never fires (tool-name typo, or a
|
|
1060
|
+
// malformed/undeclared MCP form).
|
|
1061
|
+
const hookMatcher = checkHookMatcher(config, silent, adapter);
|
|
1031
1062
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
1032
1063
|
// is to validate every ref; illustrative blocks opt out via
|
|
1033
1064
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -1095,6 +1126,20 @@ async function runLint(restArgs, flags, config) {
|
|
|
1095
1126
|
mcpHookErrors: mcpHookTargets.errors,
|
|
1096
1127
|
preferCompiledHookIssues: preferCompiledHooks.issues,
|
|
1097
1128
|
preferCompiledHookErrors: preferCompiledHooks.errors,
|
|
1129
|
+
lethalTrifectaIssues: lethalTrifecta.issues,
|
|
1130
|
+
lethalTrifectaErrors: lethalTrifecta.errors,
|
|
1131
|
+
skillResourceIssues: skillResources.issues,
|
|
1132
|
+
skillResourceErrors: skillResources.errors,
|
|
1133
|
+
skillFenceIssues: skillFence.issues,
|
|
1134
|
+
skillFenceErrors: skillFence.errors,
|
|
1135
|
+
pluginLayoutIssues: pluginLayout.issues,
|
|
1136
|
+
pluginLayoutErrors: pluginLayout.errors,
|
|
1137
|
+
delegationTrifectaIssues: delegationTrifecta.issues,
|
|
1138
|
+
delegationTrifectaErrors: delegationTrifecta.errors,
|
|
1139
|
+
hookBlockIssues: hookBlock.issues,
|
|
1140
|
+
hookBlockErrors: hookBlock.errors,
|
|
1141
|
+
hookMatcherIssues: hookMatcher.issues,
|
|
1142
|
+
hookMatcherErrors: hookMatcher.errors,
|
|
1098
1143
|
docRefErrors: docRefReport.errors.length,
|
|
1099
1144
|
symbolRefErrors,
|
|
1100
1145
|
mcpRefErrors,
|
|
@@ -1557,7 +1602,19 @@ function specReferencedElsewhere(specFile, ejectedFile) {
|
|
|
1557
1602
|
/** Full GitHub Actions workflow that wires the production `zernie/vigiles@v1`
|
|
1558
1603
|
* Action (lint pillar) and, when the test pillar is set up, a deterministic
|
|
1559
1604
|
* harness job. */
|
|
1560
|
-
|
|
1605
|
+
/** The npm package(s) that provide each harness's CLI binary — the deterministic
|
|
1606
|
+
* harness tier spawns the real agent CLI against a mock model (no API key). A repo
|
|
1607
|
+
* targeting both harnesses installs both. */
|
|
1608
|
+
function harnessTestBinaries(harnesses) {
|
|
1609
|
+
const pkgs = [];
|
|
1610
|
+
if (harnesses.includes("claude"))
|
|
1611
|
+
pkgs.push("@anthropic-ai/claude-code");
|
|
1612
|
+
if (harnesses.includes("codex"))
|
|
1613
|
+
pkgs.push("@openai/codex");
|
|
1614
|
+
// Fall back to Claude Code if the set is somehow empty (back-compatible default).
|
|
1615
|
+
return (pkgs.length > 0 ? pkgs : ["@anthropic-ai/claude-code"]).join(" ");
|
|
1616
|
+
}
|
|
1617
|
+
function vigilesWorkflow(plan, harnesses) {
|
|
1561
1618
|
const harness = plan.test
|
|
1562
1619
|
? `
|
|
1563
1620
|
harness:
|
|
@@ -1571,8 +1628,23 @@ function vigilesWorkflow(plan) {
|
|
|
1571
1628
|
with:
|
|
1572
1629
|
node-version: "20"
|
|
1573
1630
|
- run: npm install
|
|
1574
|
-
- run: npm i -g
|
|
1631
|
+
- run: npm i -g ${harnessTestBinaries(harnesses)} # mock tier needs the binary, no API key
|
|
1575
1632
|
- run: npx vigiles test
|
|
1633
|
+
|
|
1634
|
+
eval-check:
|
|
1635
|
+
# Eval staleness gate — real-model evals run LOCALLY on your subscription
|
|
1636
|
+
# (\`npx vigiles eval --update\`, which commits a lock); this job VERIFIES those
|
|
1637
|
+
# committed results against the current inputs with NO model call. It stays a
|
|
1638
|
+
# green no-op until you commit your first lock. See docs/harness-testing.md.
|
|
1639
|
+
runs-on: ubuntu-latest
|
|
1640
|
+
steps:
|
|
1641
|
+
- uses: actions/checkout@v4
|
|
1642
|
+
- uses: actions/setup-node@v4
|
|
1643
|
+
with:
|
|
1644
|
+
node-version: "20"
|
|
1645
|
+
- uses: zernie/vigiles@v1
|
|
1646
|
+
with:
|
|
1647
|
+
command: eval-check
|
|
1576
1648
|
`
|
|
1577
1649
|
: "";
|
|
1578
1650
|
return `name: vigiles
|
|
@@ -1643,7 +1715,7 @@ function rewriteRemovedSubcommands(content) {
|
|
|
1643
1715
|
* commit hint). An existing workflow is never clobbered unless `--force`, but a
|
|
1644
1716
|
* STALE one (old bare-`npx vigiles` API, or a removed subcommand) is reported
|
|
1645
1717
|
* loudly instead of silently skipped — and rewritten in place with `--force`. */
|
|
1646
|
-
function wireGha(plan) {
|
|
1718
|
+
function wireGha(plan, harnesses) {
|
|
1647
1719
|
const dir = (0, node_path_1.resolve)(process.cwd(), ".github", "workflows");
|
|
1648
1720
|
const path = (0, node_path_1.resolve)(dir, "vigiles.yml");
|
|
1649
1721
|
const rel = ".github/workflows/vigiles.yml";
|
|
@@ -1664,7 +1736,7 @@ function wireGha(plan) {
|
|
|
1664
1736
|
}
|
|
1665
1737
|
else if (workflowUsesStaleApi(content)) {
|
|
1666
1738
|
if (plan.force) {
|
|
1667
|
-
(0, node_fs_1.writeFileSync)(path, vigilesWorkflow(plan));
|
|
1739
|
+
(0, node_fs_1.writeFileSync)(path, vigilesWorkflow(plan, harnesses));
|
|
1668
1740
|
console.log(`✓ Regenerated ${rel} (was a stale bare \`npx vigiles\`)`);
|
|
1669
1741
|
return [rel];
|
|
1670
1742
|
}
|
|
@@ -1680,7 +1752,7 @@ function wireGha(plan) {
|
|
|
1680
1752
|
}
|
|
1681
1753
|
if (!(0, node_fs_1.existsSync)(dir))
|
|
1682
1754
|
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
1683
|
-
(0, node_fs_1.writeFileSync)(path, vigilesWorkflow(plan));
|
|
1755
|
+
(0, node_fs_1.writeFileSync)(path, vigilesWorkflow(plan, harnesses));
|
|
1684
1756
|
console.log("✓ Created .github/workflows/vigiles.yml (uses zernie/vigiles@v1)");
|
|
1685
1757
|
return [".github/workflows/vigiles.yml"];
|
|
1686
1758
|
}
|
|
@@ -2086,6 +2158,36 @@ function installPlugins(harnesses) {
|
|
|
2086
2158
|
console.log("");
|
|
2087
2159
|
reportInstall(plan, runInstall(plan, exec));
|
|
2088
2160
|
}
|
|
2161
|
+
// Claude Code gets its hooks from the global marketplace plugin; Codex has no
|
|
2162
|
+
// global store, so wire vigiles's proactive nudge hooks into the repo's
|
|
2163
|
+
// .codex/config.toml directly (the idiomatic, repo-committed place).
|
|
2164
|
+
if (harnesses.includes("codex"))
|
|
2165
|
+
wireCodexHooks();
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Wire vigiles's proactive nudge hooks into `.codex/config.toml` (idempotently).
|
|
2169
|
+
* Codex honors `additionalContext` on `PostToolUse`, and these run as direct
|
|
2170
|
+
* `npx vigiles hook-runtime …` commands (no plugin root / vendored script), so a
|
|
2171
|
+
* Codex user gets the same eval-lock + refs nudges a Claude Code user gets from
|
|
2172
|
+
* the marketplace plugin. The pure merge is `applyCodexPluginHooks` (unit-tested
|
|
2173
|
+
* in setup-plan.test.ts) — this only does the read/parse/write IO.
|
|
2174
|
+
*/
|
|
2175
|
+
function wireCodexHooks() {
|
|
2176
|
+
const path = (0, node_path_1.resolve)(process.cwd(), ".codex", "config.toml");
|
|
2177
|
+
let config = {};
|
|
2178
|
+
if ((0, node_fs_1.existsSync)(path)) {
|
|
2179
|
+
try {
|
|
2180
|
+
config = (0, toml_1.parse)((0, node_fs_1.readFileSync)(path, "utf-8"));
|
|
2181
|
+
}
|
|
2182
|
+
catch {
|
|
2183
|
+
console.log("⚠ .codex/config.toml is not valid TOML — skipping Codex hook wiring (fix it, then re-run `vigiles init`).");
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
const merged = (0, setup_plan_js_1.applyCodexPluginHooks)(config);
|
|
2188
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
2189
|
+
(0, node_fs_1.writeFileSync)(path, (0, hook_install_js_1.serializeConfig)(merged, "toml"));
|
|
2190
|
+
console.log("✓ Wired the eval-lock + refs nudge hooks into .codex/config.toml (commit it)");
|
|
2089
2191
|
}
|
|
2090
2192
|
/** Add/upgrade `vigiles` in the project's `devDependencies` (and move it out of
|
|
2091
2193
|
* `dependencies` if it's there). Returns the files it wrote (for the commit
|
|
@@ -2249,7 +2351,7 @@ async function setup(args) {
|
|
|
2249
2351
|
// CI — the production Action (+ a harness job when Pillar 2 is on).
|
|
2250
2352
|
if (plan.gha) {
|
|
2251
2353
|
console.log("");
|
|
2252
|
-
written.push(...wireGha(plan));
|
|
2354
|
+
written.push(...wireGha(plan, harnesses));
|
|
2253
2355
|
}
|
|
2254
2356
|
// Plugin/skill install — per-harness (Claude marketplace / Codex direct).
|
|
2255
2357
|
if (plan.plugin) {
|
|
@@ -2673,6 +2775,211 @@ function checkDescriptionOverlap(config, silent, adapter) {
|
|
|
2673
2775
|
}
|
|
2674
2776
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2675
2777
|
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Apply the `lethal-trifecta` rule: a unit (subagent / model-invocable skill)
|
|
2780
|
+
* whose declared tools hold all three legs (read-private + ingest-untrusted +
|
|
2781
|
+
* exfiltrate) is a prompt-injection exfil path (Meta's Rule of Two). Reuses
|
|
2782
|
+
* `scanPlugin`'s `trifectaFindings` (a capability SET-intersection, one detector,
|
|
2783
|
+
* no drift). Warning by default; "error" gates CI. Surfaces across BOTH subagents
|
|
2784
|
+
* and skills, so it is NOT gated on the `subagents` capability — a skill-only
|
|
2785
|
+
* harness still has the surface.
|
|
2786
|
+
*/
|
|
2787
|
+
function checkLethalTrifecta(config, silent, adapter) {
|
|
2788
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["lethal-trifecta"]);
|
|
2789
|
+
if (!sev)
|
|
2790
|
+
return { issues: 0, errors: 0 };
|
|
2791
|
+
let found;
|
|
2792
|
+
try {
|
|
2793
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).trifectaFindings;
|
|
2794
|
+
}
|
|
2795
|
+
catch {
|
|
2796
|
+
return { issues: 0, errors: 0 };
|
|
2797
|
+
}
|
|
2798
|
+
if (found.length > 0 && !silent) {
|
|
2799
|
+
console.log("\nLethal-trifecta check:\n");
|
|
2800
|
+
for (const t of found) {
|
|
2801
|
+
const msg = `${t.kind} ${t.name}: ${t.finding.message}`;
|
|
2802
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${t.path}: ${msg}`);
|
|
2803
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, t.path);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Apply the `skill-resource-resolves` rule: a SKILL.md body referencing a bundled
|
|
2810
|
+
* file (`scripts/`/`references/`/`assets/` or a relative markdown link with an
|
|
2811
|
+
* extension) that doesn't exist on disk — the agent reads the instruction and gets
|
|
2812
|
+
* nothing. Reuses `scanPlugin`'s `skillResourceIssues` (high-precision / FP-safe,
|
|
2813
|
+
* one detector, no drift). Warning by default; "error" gates CI.
|
|
2814
|
+
*/
|
|
2815
|
+
function checkSkillResourceResolves(config, silent, adapter) {
|
|
2816
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-resource-resolves"]);
|
|
2817
|
+
if (!sev)
|
|
2818
|
+
return { issues: 0, errors: 0 };
|
|
2819
|
+
let found;
|
|
2820
|
+
try {
|
|
2821
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillResourceIssues;
|
|
2822
|
+
}
|
|
2823
|
+
catch {
|
|
2824
|
+
return { issues: 0, errors: 0 };
|
|
2825
|
+
}
|
|
2826
|
+
if (found.length > 0 && !silent) {
|
|
2827
|
+
console.log("\nSkill-resource check:\n");
|
|
2828
|
+
for (const s of found) {
|
|
2829
|
+
const msg = `${s.name}: bundled resource "${s.finding.ref}" (line ${String(s.finding.line)}) is referenced but missing — the agent reads the instruction and gets nothing.`;
|
|
2830
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
2831
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, s.path);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Apply the `skill-missing-fence` rule: a SKILL.md that opens with
|
|
2838
|
+
* frontmatter-looking keys (`name:`/`description:`) but no `---` fence loads as
|
|
2839
|
+
* pure body — no name, no description, no trigger (the skill is invisible).
|
|
2840
|
+
* Reuses `scanPlugin`'s `skillFenceIssues` (one detector, no drift). Warning by
|
|
2841
|
+
* default; "error" gates CI.
|
|
2842
|
+
*/
|
|
2843
|
+
function checkSkillMissingFence(config, silent, adapter) {
|
|
2844
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-missing-fence"]);
|
|
2845
|
+
if (!sev)
|
|
2846
|
+
return { issues: 0, errors: 0 };
|
|
2847
|
+
let found;
|
|
2848
|
+
try {
|
|
2849
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillFenceIssues;
|
|
2850
|
+
}
|
|
2851
|
+
catch {
|
|
2852
|
+
return { issues: 0, errors: 0 };
|
|
2853
|
+
}
|
|
2854
|
+
if (found.length > 0 && !silent) {
|
|
2855
|
+
console.log("\nSkill-missing-fence check:\n");
|
|
2856
|
+
for (const s of found) {
|
|
2857
|
+
const msg = `${s.name}: ${s.finding.message}`;
|
|
2858
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
2859
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, s.path);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Apply the `plugin-dir-layout` rule: functional surface dirs (skills/agents/
|
|
2866
|
+
* commands) nested inside the `.claude-plugin/` manifest dir where the harness
|
|
2867
|
+
* can't see them (the #1 plugin-author mistake). Reuses `scanPlugin`'s
|
|
2868
|
+
* `pluginLayoutIssues` (one detector, no drift). Warning by default; "error"
|
|
2869
|
+
* gates CI.
|
|
2870
|
+
*/
|
|
2871
|
+
function checkPluginDirLayout(config, silent, adapter) {
|
|
2872
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["plugin-dir-layout"]);
|
|
2873
|
+
if (!sev)
|
|
2874
|
+
return { issues: 0, errors: 0 };
|
|
2875
|
+
let found;
|
|
2876
|
+
try {
|
|
2877
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).pluginLayoutIssues;
|
|
2878
|
+
}
|
|
2879
|
+
catch {
|
|
2880
|
+
return { issues: 0, errors: 0 };
|
|
2881
|
+
}
|
|
2882
|
+
if (found.length > 0 && !silent) {
|
|
2883
|
+
console.log("\nPlugin-dir-layout check:\n");
|
|
2884
|
+
for (const p of found) {
|
|
2885
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${p.message}`);
|
|
2886
|
+
ghAnnotate(sev === "error" ? "error" : "warning", p.message);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2890
|
+
}
|
|
2891
|
+
/**
|
|
2892
|
+
* Apply the `delegation-trifecta` rule: a lethal trifecta that EMERGES across a
|
|
2893
|
+
* delegation edge — a subagent whose effective (own ∪ delegated-to) capability
|
|
2894
|
+
* holds all three legs though no single unit does. Reuses `scanPlugin`'s
|
|
2895
|
+
* `delegationTrifecta` (one detector, no drift). Warning by default; "error"
|
|
2896
|
+
* gates CI. Surfaces across the subagent graph, so it is NOT gated on a
|
|
2897
|
+
* capability the way a surface-specific rule is.
|
|
2898
|
+
*/
|
|
2899
|
+
function checkDelegationTrifecta(config, silent, adapter) {
|
|
2900
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["delegation-trifecta"]);
|
|
2901
|
+
if (!sev)
|
|
2902
|
+
return { issues: 0, errors: 0 };
|
|
2903
|
+
let found;
|
|
2904
|
+
try {
|
|
2905
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).delegationTrifecta;
|
|
2906
|
+
}
|
|
2907
|
+
catch {
|
|
2908
|
+
return { issues: 0, errors: 0 };
|
|
2909
|
+
}
|
|
2910
|
+
if (found.length > 0 && !silent) {
|
|
2911
|
+
console.log("\nDelegation-trifecta check:\n");
|
|
2912
|
+
for (const d of found) {
|
|
2913
|
+
const msg = `${d.finding.name}: ${d.finding.message}`;
|
|
2914
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${d.path}: ${msg}`);
|
|
2915
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, d.path);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2919
|
+
}
|
|
2920
|
+
/**
|
|
2921
|
+
* Apply the `hook-block-ineffective` rule: a hook that LOOKS like it blocks but
|
|
2922
|
+
* silently doesn't — a block decision (`exit 2` / `decision` / `permissionDecision`)
|
|
2923
|
+
* on a non-blocking event, or the legacy top-level `decision` field on a
|
|
2924
|
+
* permission-gated event (#19009, the #1 verified hook pain). Reuses `scanPlugin`'s
|
|
2925
|
+
* `hookBlockFindings` (one detector, no drift). Warning by default; "error" gates CI.
|
|
2926
|
+
*/
|
|
2927
|
+
function checkHookBlockIneffective(config, silent, adapter) {
|
|
2928
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-block-ineffective"]);
|
|
2929
|
+
if (!sev)
|
|
2930
|
+
return { issues: 0, errors: 0 };
|
|
2931
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2932
|
+
reportNotApplicable("Hook-block check", "shell hooks", adapter, silent);
|
|
2933
|
+
return { issues: 0, errors: 0 };
|
|
2934
|
+
}
|
|
2935
|
+
let found;
|
|
2936
|
+
try {
|
|
2937
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookBlockFindings;
|
|
2938
|
+
}
|
|
2939
|
+
catch {
|
|
2940
|
+
return { issues: 0, errors: 0 };
|
|
2941
|
+
}
|
|
2942
|
+
if (found.length > 0 && !silent) {
|
|
2943
|
+
console.log("\nHook-block check:\n");
|
|
2944
|
+
for (const h of found) {
|
|
2945
|
+
const where = h.scriptPath ?? "(inline)";
|
|
2946
|
+
const msg = `[${h.event}] ${where}: ${h.message}`;
|
|
2947
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${msg}`);
|
|
2948
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg, h.scriptPath ?? undefined);
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* Apply the `hook-matcher` rule: a hook `matcher` string that silently never
|
|
2955
|
+
* fires — a tool-name typo (`bash`→`Bash`) or a malformed/undeclared MCP form.
|
|
2956
|
+
* Reuses `scanPlugin`'s `hookMatcherFindings` (one detector, no drift). Warning
|
|
2957
|
+
* by default; "error" gates CI.
|
|
2958
|
+
*/
|
|
2959
|
+
function checkHookMatcher(config, silent, adapter) {
|
|
2960
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-matcher"]);
|
|
2961
|
+
if (!sev)
|
|
2962
|
+
return { issues: 0, errors: 0 };
|
|
2963
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2964
|
+
reportNotApplicable("Hook-matcher check", "shell hooks", adapter, silent);
|
|
2965
|
+
return { issues: 0, errors: 0 };
|
|
2966
|
+
}
|
|
2967
|
+
let found;
|
|
2968
|
+
try {
|
|
2969
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookMatcherFindings;
|
|
2970
|
+
}
|
|
2971
|
+
catch {
|
|
2972
|
+
return { issues: 0, errors: 0 };
|
|
2973
|
+
}
|
|
2974
|
+
if (found.length > 0 && !silent) {
|
|
2975
|
+
console.log("\nHook-matcher check:\n");
|
|
2976
|
+
for (const m of found) {
|
|
2977
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${m.message}`);
|
|
2978
|
+
ghAnnotate(sev === "error" ? "error" : "warning", m.message);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2982
|
+
}
|
|
2676
2983
|
/**
|
|
2677
2984
|
* Apply the `mcp-hook-target-resolves` rule: a `type: "mcp_tool"` hook action
|
|
2678
2985
|
* that's incomplete (no `server`/`tool`) or targets a server the plugin doesn't
|
|
@@ -3157,10 +3464,55 @@ async function handleGenerateHarness(args, restArgs) {
|
|
|
3157
3464
|
* tier needs it, just like the node:test suite). `--trials=N` is forwarded to
|
|
3158
3465
|
* eval scripts via the `VIGILES_TRIALS` env var.
|
|
3159
3466
|
*/
|
|
3467
|
+
/**
|
|
3468
|
+
* Resolve the eval LOCK env from the `eval` flags. `--update` records each named
|
|
3469
|
+
* eval's report to a committed `.vigiles/eval-locks/<name>.lock.json` (run locally
|
|
3470
|
+
* on your subscription); `--check` (CI) verifies the committed result against the
|
|
3471
|
+
* current inputs WITHOUT a model call. `--check` is a green NO-OP until the first
|
|
3472
|
+
* lock is committed (smooth adoption). Returns the env to thread, or `"skip"` to
|
|
3473
|
+
* exit green now. `--check`+`--update` together is a usage error (exit 2). The
|
|
3474
|
+
* behavior epoch comes from `.vigilesrc.json` `eval.apiVersion` (committed).
|
|
3475
|
+
*/
|
|
3476
|
+
function resolveEvalLockEnv(args) {
|
|
3477
|
+
const wantCheck = args.includes("--check");
|
|
3478
|
+
const wantUpdate = args.includes("--update");
|
|
3479
|
+
if (wantCheck && wantUpdate) {
|
|
3480
|
+
console.error("vigiles eval: --check and --update are mutually exclusive (one verifies, one records).");
|
|
3481
|
+
process.exit(2);
|
|
3482
|
+
}
|
|
3483
|
+
if (wantCheck &&
|
|
3484
|
+
!(0, eval_lock_js_1.anyLocksCommitted)((0, node_path_1.resolve)(process.cwd(), eval_lock_js_1.DEFAULT_LOCK_DIR))) {
|
|
3485
|
+
console.log("ℹ vigiles eval --check: no committed eval locks found — nothing to verify.\n" +
|
|
3486
|
+
" Run `vigiles eval --update` locally (on your subscription) and commit the\n" +
|
|
3487
|
+
" lock to enable the CI staleness gate.");
|
|
3488
|
+
return "skip";
|
|
3489
|
+
}
|
|
3490
|
+
const env = {};
|
|
3491
|
+
if (wantCheck)
|
|
3492
|
+
env.VIGILES_EVAL_LOCK = "check";
|
|
3493
|
+
if (wantUpdate)
|
|
3494
|
+
env.VIGILES_EVAL_LOCK = "update";
|
|
3495
|
+
if (wantCheck || wantUpdate) {
|
|
3496
|
+
const apiVersion = (0, validate_js_1.loadConfig)().eval?.apiVersion;
|
|
3497
|
+
if (apiVersion !== undefined)
|
|
3498
|
+
env.VIGILES_EVAL_API_VERSION = String(apiVersion);
|
|
3499
|
+
}
|
|
3500
|
+
return env;
|
|
3501
|
+
}
|
|
3160
3502
|
function handleRunScripts(kind, args, restArgs) {
|
|
3161
3503
|
const cwd = process.cwd();
|
|
3162
3504
|
// Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
|
|
3163
3505
|
const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
|
|
3506
|
+
// The eval LOCK flags (`--check`/`--update`) are resolved BEFORE file discovery
|
|
3507
|
+
// so mutual-exclusion + the cold-start no-op are honored regardless of file
|
|
3508
|
+
// count. Returns the env to thread to scripts, or `"skip"` to exit green now.
|
|
3509
|
+
let lockEnv = {};
|
|
3510
|
+
if (kind === "eval") {
|
|
3511
|
+
const r = resolveEvalLockEnv(args);
|
|
3512
|
+
if (r === "skip")
|
|
3513
|
+
return;
|
|
3514
|
+
lockEnv = r;
|
|
3515
|
+
}
|
|
3164
3516
|
const files = (0, run_scripts_js_1.discoverScripts)(restArgs, defaultGlob, cwd);
|
|
3165
3517
|
// `--min=N`: a CI gate asserts at least N scripts actually RAN — so a bad path,
|
|
3166
3518
|
// a renamed file, or a glob that matched nothing fails LOUD instead of passing
|
|
@@ -3189,7 +3541,7 @@ function handleRunScripts(kind, args, restArgs) {
|
|
|
3189
3541
|
// it's part of the measurement definition, so it belongs in the spec
|
|
3190
3542
|
// (`model` / `minModel`), version-controlled, not a hidden override.
|
|
3191
3543
|
const trialsFlag = args.find((a) => a.startsWith("--trials="));
|
|
3192
|
-
const env = {};
|
|
3544
|
+
const env = { ...lockEnv };
|
|
3193
3545
|
if (trialsFlag)
|
|
3194
3546
|
env.VIGILES_TRIALS = trialsFlag.split("=")[1];
|
|
3195
3547
|
console.log(`Running ${String(files.length)} ${kind} file(s):\n`);
|
|
@@ -3368,6 +3720,8 @@ function printUsage(command) {
|
|
|
3368
3720
|
console.log(" --serve opens a LIVE local report whose buttons create specs in one click (own repo only; loopback + token-guarded) · --no-serve to skip the prompt");
|
|
3369
3721
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
3370
3722
|
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
|
|
3723
|
+
console.log(" --update records each named eval's result to a committed lock (run locally on your subscription)");
|
|
3724
|
+
console.log(" --check verifies committed eval results against current inputs WITHOUT a model — the CI staleness gate");
|
|
3371
3725
|
console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
|
|
3372
3726
|
console.log("");
|
|
3373
3727
|
console.log("Examples:");
|
|
@@ -3683,6 +4037,9 @@ async function handleHookRuntime(kind, restArgs) {
|
|
|
3683
4037
|
case "refs":
|
|
3684
4038
|
refsHookCommand();
|
|
3685
4039
|
return;
|
|
4040
|
+
case "eval-lock-nudge":
|
|
4041
|
+
evalLockNudgeHookCommand();
|
|
4042
|
+
return;
|
|
3686
4043
|
case "effect-enter":
|
|
3687
4044
|
(0, effect_region_js_1.setEffectActive)(process.cwd());
|
|
3688
4045
|
console.log("Effect boundary entered.");
|
|
@@ -3729,6 +4086,45 @@ const INSTRUCTION_FILE = /^(SKILL|CLAUDE|AGENTS)\.md$/;
|
|
|
3729
4086
|
function isInstructionFile(file) {
|
|
3730
4087
|
return INSTRUCTION_FILE.test((0, node_path_1.basename)(file));
|
|
3731
4088
|
}
|
|
4089
|
+
/**
|
|
4090
|
+
* PostToolUse-hook entrypoint: when the agent edits an eval input (a `SKILL.md`
|
|
4091
|
+
* trigger surface or an `*.eval.*` script), and committed eval locks exist, inject
|
|
4092
|
+
* a NON-BLOCKING reminder to re-run `vigiles eval --update`. Self-gating (silent
|
|
4093
|
+
* until a lock is committed), never blocks, never runs an eval — a reminder, not a
|
|
4094
|
+
* gate (the gate is `eval --check` in CI). The harness-neutral nudge lives in
|
|
4095
|
+
* `evalLockNudge`; both CC and Codex deliver it as `additionalContext` on
|
|
4096
|
+
* `PostToolUse` (confirmed — see docs/harness-testing-codex.md).
|
|
4097
|
+
*/
|
|
4098
|
+
function evalLockNudgeHookCommand() {
|
|
4099
|
+
let raw = "";
|
|
4100
|
+
try {
|
|
4101
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
4102
|
+
}
|
|
4103
|
+
catch {
|
|
4104
|
+
/* no stdin → nothing to do */
|
|
4105
|
+
}
|
|
4106
|
+
let file = "";
|
|
4107
|
+
try {
|
|
4108
|
+
const j = JSON.parse(raw);
|
|
4109
|
+
file = j.tool_input?.file_path ?? "";
|
|
4110
|
+
}
|
|
4111
|
+
catch {
|
|
4112
|
+
/* malformed → nothing to do */
|
|
4113
|
+
}
|
|
4114
|
+
if (!file)
|
|
4115
|
+
return;
|
|
4116
|
+
const cwd = process.cwd();
|
|
4117
|
+
const target = (0, node_path_1.relative)(cwd, (0, node_path_1.resolve)(cwd, file)) || file;
|
|
4118
|
+
const msg = (0, eval_lock_js_1.evalLockNudge)(target, (0, node_path_1.resolve)(cwd, eval_lock_js_1.DEFAULT_LOCK_DIR));
|
|
4119
|
+
if (!msg)
|
|
4120
|
+
return;
|
|
4121
|
+
process.stdout.write(JSON.stringify({
|
|
4122
|
+
hookSpecificOutput: {
|
|
4123
|
+
hookEventName: "PostToolUse",
|
|
4124
|
+
additionalContext: msg,
|
|
4125
|
+
},
|
|
4126
|
+
}) + "\n");
|
|
4127
|
+
}
|
|
3732
4128
|
/**
|
|
3733
4129
|
* PostToolUse-hook entrypoint: when the agent edits an instruction file, force
|
|
3734
4130
|
* every code reference to carry a file-qualified mark (`path.ext#symbol`) and
|
|
@@ -3904,31 +4300,63 @@ async function installHookFile(file, adapter, registeredProviders = []) {
|
|
|
3904
4300
|
: (0, hook_install_js_1.mergeHooksJson)(existing, compiled.hooks, file);
|
|
3905
4301
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(settingsAbs), { recursive: true });
|
|
3906
4302
|
(0, node_fs_1.writeFileSync)(settingsAbs, (0, hook_install_js_1.serializeConfig)(merged, format));
|
|
3907
|
-
//
|
|
3908
|
-
//
|
|
3909
|
-
//
|
|
3910
|
-
//
|
|
4303
|
+
// No silent skips: warn loudly only where a hook's OUTPUT genuinely may not
|
|
4304
|
+
// apply on this harness. INJECT's `additionalContext` shape is now CONFIRMED
|
|
4305
|
+
// shared with Codex (per the official hooks docs), so an inject hook only
|
|
4306
|
+
// warns when its event isn't in the harness's `injectableEvents`. REACT's
|
|
4307
|
+
// output is still Claude-Code-confirmed only. The gate (deny→exit 2) path is
|
|
4308
|
+
// cross-harness and never warns.
|
|
3911
4309
|
const role = (0, hook_program_js_1.dispatchKind)(program);
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4310
|
+
const event = typeof program.on === "string" ? program.on : "";
|
|
4311
|
+
const injectable = adapter.hookProtocol?.injectableEvents ?? [];
|
|
4312
|
+
const matcher = (0, hook_program_js_1.hookRouting)(program).matcher;
|
|
4313
|
+
let warning;
|
|
4314
|
+
if (adapter.name !== "claude-code") {
|
|
4315
|
+
if (role === "inject" && !injectable.includes(event)) {
|
|
4316
|
+
warning =
|
|
4317
|
+
`this inject hook targets "${event}", which ${adapter.name} does not ` +
|
|
4318
|
+
`honor for additionalContext — the injected text won't reach the agent. ` +
|
|
4319
|
+
`Use an event ${adapter.name} supports: ${injectable.join(", ")}.`;
|
|
4320
|
+
}
|
|
4321
|
+
else if (role === "react") {
|
|
4322
|
+
warning =
|
|
4323
|
+
`react output is confirmed only for Claude Code; on ${adapter.name} this ` +
|
|
4324
|
+
`hook's react output is unverified (the gate deny→exit 2 path IS ` +
|
|
4325
|
+
`cross-harness). Confirm against the real binary first.`;
|
|
4326
|
+
}
|
|
4327
|
+
else if (matcher !== undefined) {
|
|
4328
|
+
// A tool-matched gate carries TOOL NAMES in its matcher. vigiles does not
|
|
4329
|
+
// yet translate tool vocabularies across dialects, so a matcher authored
|
|
4330
|
+
// with Claude Code names (`Edit`/`Write`/`Bash`) won't fire on a harness
|
|
4331
|
+
// that names the same tools differently (Codex: `apply_patch`/`shell`).
|
|
4332
|
+
// Warn LOUDLY rather than report a silently-non-firing success.
|
|
4333
|
+
warning =
|
|
4334
|
+
`this hook matches tool(s) "${matcher}" — if those are Claude Code tool ` +
|
|
4335
|
+
`names, they may not match ${adapter.name}'s vocabulary (e.g. ` +
|
|
4336
|
+
`apply_patch/shell), so the hook may not fire. Verify the matcher uses ` +
|
|
4337
|
+
`${adapter.name}'s tool names (cross-dialect matcher translation is not ` +
|
|
4338
|
+
`yet automatic).`;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
3919
4341
|
return { role, settingsPath: adapter.layout.settingsPath, warning };
|
|
3920
4342
|
}
|
|
3921
4343
|
/**
|
|
3922
4344
|
* Compile + install every hook (explicit paths, else discovered under
|
|
3923
|
-
* `.vigiles/hooks/`) into
|
|
3924
|
-
* hook
|
|
4345
|
+
* `.vigiles/hooks/`) into EVERY enabled harness's config. A typed hook is
|
|
4346
|
+
* harness-neutral, so when a repo targets both harnesses the SAME hook is merged
|
|
4347
|
+
* into `.claude/settings.json` AND `.codex/config.toml` (each in its native
|
|
4348
|
+
* format, with per-harness warnings) — never just the first. The harness set is
|
|
4349
|
+
* resolved from the `--harness=` flag, else `config.harness`, else auto-detect.
|
|
4350
|
+
* Returns false if any hook failed to compile for any harness.
|
|
3925
4351
|
*/
|
|
3926
|
-
async function installHooks(hookFiles, harnessFlag) {
|
|
4352
|
+
async function installHooks(hookFiles, harnessFlag, configHarness) {
|
|
3927
4353
|
if (hookFiles.length === 0)
|
|
3928
4354
|
return true;
|
|
3929
|
-
const
|
|
3930
|
-
|
|
3931
|
-
:
|
|
4355
|
+
const adapters = (0, adapter_registry_js_1.resolveHarnessAdapters)({
|
|
4356
|
+
root: process.cwd(),
|
|
4357
|
+
flag: harnessFlag,
|
|
4358
|
+
configHarness,
|
|
4359
|
+
});
|
|
3932
4360
|
// Validate registered providers first → the names a hook's provider() ref may
|
|
3933
4361
|
// resolve to (an unsafe provider fails the whole compile, like a bad hook).
|
|
3934
4362
|
let registeredProviders;
|
|
@@ -3945,10 +4373,13 @@ async function installHooks(hookFiles, harnessFlag) {
|
|
|
3945
4373
|
let ok = true;
|
|
3946
4374
|
for (const file of hookFiles) {
|
|
3947
4375
|
try {
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
console.
|
|
4376
|
+
// Fan out: the same compiled hook lands in each enabled harness's config.
|
|
4377
|
+
for (const adapter of adapters) {
|
|
4378
|
+
const r = await installHookFile(file, adapter, registeredProviders);
|
|
4379
|
+
console.log(`✓ ${file} → ${r.settingsPath} (role: ${r.role}, harness: ${adapter.name})`);
|
|
4380
|
+
if (r.warning)
|
|
4381
|
+
console.warn(`⚠ ${r.warning}`);
|
|
4382
|
+
}
|
|
3952
4383
|
}
|
|
3953
4384
|
catch (e) {
|
|
3954
4385
|
if (e instanceof hook_program_js_1.HookCompileError) {
|
|
@@ -4532,7 +4963,7 @@ async function main() {
|
|
|
4532
4963
|
let valid = true;
|
|
4533
4964
|
if (specs.length > 0)
|
|
4534
4965
|
valid = (await compile(specs, config, { harnessFlag })) && valid;
|
|
4535
|
-
valid = (await installHooks(hooks, harnessFlag)) && valid;
|
|
4966
|
+
valid = (await installHooks(hooks, harnessFlag, config.harness)) && valid;
|
|
4536
4967
|
// Keep an existing whole-harness registry in sync (cheap, opt-in) so the
|
|
4537
4968
|
// user never hand-runs `generate-harness`. Skipped when no harness.gen.ts.
|
|
4538
4969
|
if (specs.length > 0)
|