vigiles 29.1.0 → 30.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/adapter-conformance.d.ts +1 -1
  2. package/dist/adapter-conformance.js +106 -25
  3. package/dist/adapter-registry.d.ts +61 -14
  4. package/dist/adapter-registry.js +78 -10
  5. package/dist/adapter.d.ts +23 -2
  6. package/dist/adapter.js +13 -1
  7. package/dist/adapters/claude-code/adapter.d.ts +32 -2
  8. package/dist/adapters/claude-code/adapter.js +44 -23
  9. package/dist/adapters/claude-code/agent-runtime.js +3 -1
  10. package/dist/adapters/claude-code/dialect.js +87 -21
  11. package/dist/adapters/claude-code/effect-region.js +3 -1
  12. package/dist/adapters/claude-code/hook-protocol.js +16 -0
  13. package/dist/adapters/claude-code/instruction-chain.d.ts +25 -0
  14. package/dist/adapters/claude-code/instruction-chain.js +626 -0
  15. package/dist/adapters/claude-code/layout.d.ts +2 -2
  16. package/dist/adapters/claude-code/layout.js +42 -8
  17. package/dist/adapters/claude-code/model-access.d.ts +41 -0
  18. package/dist/adapters/claude-code/model-access.js +46 -0
  19. package/dist/adapters/claude-code/skill-reachability.d.ts +125 -0
  20. package/dist/adapters/claude-code/skill-reachability.js +111 -0
  21. package/dist/adapters/claude-code/skill-runtime.js +3 -1
  22. package/dist/adapters/codex/adapter.d.ts +39 -2
  23. package/dist/adapters/codex/adapter.js +29 -29
  24. package/dist/adapters/codex/dialect.js +11 -6
  25. package/dist/adapters/codex/eval.d.ts +10 -0
  26. package/dist/adapters/codex/eval.js +48 -1
  27. package/dist/adapters/codex/hook-protocol.d.ts +2 -1
  28. package/dist/adapters/codex/hook-protocol.js +10 -0
  29. package/dist/adapters/codex/instruction-chain.d.ts +40 -0
  30. package/dist/adapters/codex/instruction-chain.js +105 -0
  31. package/dist/adapters/codex/layout.d.ts +1 -1
  32. package/dist/adapters/codex/layout.js +41 -14
  33. package/dist/adapters/opencode/adapter.d.ts +33 -2
  34. package/dist/adapters/opencode/adapter.js +36 -36
  35. package/dist/adapters/opencode/dialect.js +2 -2
  36. package/dist/adapters/opencode/instruction-chain.d.ts +37 -0
  37. package/dist/adapters/opencode/instruction-chain.js +70 -0
  38. package/dist/adapters/opencode/layout.d.ts +19 -0
  39. package/dist/adapters/opencode/layout.js +34 -15
  40. package/dist/adoptability.d.ts +31 -1
  41. package/dist/adoptability.js +57 -0
  42. package/dist/cli-main.js +185 -102
  43. package/dist/core/adapter.d.ts +213 -61
  44. package/dist/core/compile.d.ts +2 -2
  45. package/dist/core/compile.js +57 -46
  46. package/dist/core/compose.d.ts +5 -3
  47. package/dist/core/compose.js +5 -3
  48. package/dist/core/config-schema.d.ts +14 -2
  49. package/dist/core/config-schema.js +20 -7
  50. package/dist/core/dialect.d.ts +54 -12
  51. package/dist/core/dialect.js +56 -0
  52. package/dist/core/eval-driver.d.ts +194 -0
  53. package/dist/core/eval-driver.js +3 -0
  54. package/dist/core/frontmatter-read.d.ts +10 -0
  55. package/dist/core/frontmatter-read.js +30 -3
  56. package/dist/core/guards.js +3 -1
  57. package/dist/core/hook-program.d.ts +27 -2
  58. package/dist/core/hook-program.js +29 -24
  59. package/dist/core/hook-protocol.d.ts +54 -0
  60. package/dist/core/install-reader.d.ts +18 -0
  61. package/dist/core/install-reader.js +88 -0
  62. package/dist/core/instruction-chain.d.ts +444 -0
  63. package/dist/core/instruction-chain.js +292 -0
  64. package/dist/core/instruction-weight.d.ts +96 -14
  65. package/dist/core/instruction-weight.js +65 -30
  66. package/dist/core/layout.d.ts +220 -33
  67. package/dist/core/layout.js +115 -1
  68. package/dist/core/lethal-trifecta.d.ts +12 -7
  69. package/dist/core/lethal-trifecta.js +13 -13
  70. package/dist/core/live-driver.d.ts +137 -0
  71. package/dist/core/live-driver.js +14 -0
  72. package/dist/core/markdown.d.ts +23 -0
  73. package/dist/core/markdown.js +77 -28
  74. package/dist/core/orphans.js +9 -7
  75. package/dist/core/settings-codec.d.ts +17 -0
  76. package/dist/core/settings-codec.js +56 -0
  77. package/dist/core/surface-discovery.d.ts +2 -2
  78. package/dist/core/surface-discovery.js +24 -12
  79. package/dist/core/surface-scopes.d.ts +26 -6
  80. package/dist/core/surface-scopes.js +52 -11
  81. package/dist/core/validate.js +16 -3
  82. package/dist/coverage-artifact.d.ts +3 -2
  83. package/dist/coverage-artifact.js +6 -5
  84. package/dist/eval-cache.d.ts +6 -1
  85. package/dist/eval-cache.js +11 -1
  86. package/dist/eval.d.ts +16 -108
  87. package/dist/eval.js +36 -2
  88. package/dist/harness-test.d.ts +3 -63
  89. package/dist/hook-install.d.ts +12 -1
  90. package/dist/hook-install.js +12 -1
  91. package/dist/hook-runtime.js +4 -2
  92. package/dist/hook-state-store.js +3 -1
  93. package/dist/local-files-tracked.d.ts +17 -0
  94. package/dist/local-files-tracked.js +70 -0
  95. package/dist/local-files.d.ts +62 -0
  96. package/dist/local-files.js +183 -0
  97. package/dist/observe.d.ts +3 -2
  98. package/dist/observe.js +7 -6
  99. package/dist/plugin-loader.d.ts +1 -1
  100. package/dist/plugin-loader.js +43 -36
  101. package/dist/scan-behavioral.d.ts +34 -25
  102. package/dist/scan-behavioral.js +122 -58
  103. package/dist/scan-core.js +37 -18
  104. package/dist/scan-files.d.ts +1 -1
  105. package/dist/scan-files.js +53 -33
  106. package/dist/scan-trigger-suggest.d.ts +0 -21
  107. package/dist/scan-trigger-suggest.js +0 -23
  108. package/dist/scan.d.ts +4 -4
  109. package/dist/scan.js +120 -84
  110. package/dist/skill-harness.d.ts +21 -5
  111. package/dist/skill-harness.js +29 -11
  112. package/dist/surface-discovery-fs.d.ts +2 -0
  113. package/dist/surface-discovery-fs.js +108 -6
  114. package/dist/test-coverage-files.js +24 -17
  115. package/dist/test-coverage.d.ts +9 -3
  116. package/dist/test-coverage.js +32 -22
  117. package/dist/verify-plugin-guards.js +1 -1
  118. package/package.json +1 -1
  119. package/dist/skill-reachability.d.ts +0 -68
  120. package/dist/skill-reachability.js +0 -205
  121. /package/dist/{dialect-drift.d.ts → adapters/claude-code/dialect-drift.d.ts} +0 -0
  122. /package/dist/{dialect-drift.js → adapters/claude-code/dialect-drift.js} +0 -0
package/dist/cli-main.js CHANGED
@@ -36,9 +36,9 @@ const test_coverage_js_1 = require("./test-coverage.js");
36
36
  const scan_js_1 = require("./scan.js");
37
37
  const surface_discovery_js_1 = require("./core/surface-discovery.js");
38
38
  const scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
39
- const dialect_drift_js_1 = require("./dialect-drift.js");
40
- const skill_reachability_js_1 = require("./skill-reachability.js");
39
+ const install_reader_js_1 = require("./core/install-reader.js");
41
40
  const plugin_declaration_js_1 = require("./plugin-declaration.js");
41
+ const local_files_tracked_js_1 = require("./local-files-tracked.js");
42
42
  const scan_behavioral_js_1 = require("./scan-behavioral.js");
43
43
  const adapter_registry_js_1 = require("./adapter-registry.js");
44
44
  const skill_harness_js_1 = require("./skill-harness.js");
@@ -1500,7 +1500,7 @@ async function runLint(restArgs, flags, excludes, config) {
1500
1500
  // hand-edit of a compiled subagent slipped past `lint`. A hand-written agent
1501
1501
  // (no hash header) stays a no-op, and require-instructions-spec is filename-
1502
1502
  // scoped to CLAUDE/AGENTS so agents are never falsely flagged as spec-less.
1503
- const files = findInstructionFiles(restArgs, excludes, adapter.layout.agentDir);
1503
+ const files = findInstructionFiles(restArgs, excludes, adapter.layout.surfaces.agent ?? "");
1504
1504
  // 1. Verify hashes and structure
1505
1505
  if (!silent) {
1506
1506
  if (files.length > 0) {
@@ -2822,7 +2822,26 @@ async function promptSetup() {
2822
2822
  rl.close();
2823
2823
  }
2824
2824
  }
2825
- const KNOWN_INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
2825
+ /**
2826
+ * The instruction filenames `init` probes for on disk — DERIVED from the
2827
+ * registered adapters, never listed.
2828
+ *
2829
+ * 🔴 THIS IS THE SAME DEFECT CLASS THE PORT REDESIGN REMOVES: a fact restated
2830
+ * by hand beside the registry that owns it. The literal read `["CLAUDE.md",
2831
+ * "AGENTS.md"]` and was correct only because the adapters happen to say that
2832
+ * today — it could not have noticed a third adapter, and it did not notice when
2833
+ * Claude Code's `instructionTargets` GAINED `AGENTS.md` (it was already there
2834
+ * by coincidence).
2835
+ *
2836
+ * `instructionTargets` rather than `layout.instructionFile`, because the
2837
+ * question here is "which files might exist and want a spec", which is every
2838
+ * name a registered harness READS — a repository with only an `AGENTS.md` is
2839
+ * one `init` must still see. Verified identical before and after over the
2840
+ * registry as it stands: union = `["CLAUDE.md", "AGENTS.md"]`.
2841
+ */
2842
+ const KNOWN_INSTRUCTION_FILES = [
2843
+ ...new Set(adapter_registry_js_1.ADAPTERS.flatMap((a) => a.dialect.instructionTargets)),
2844
+ ];
2826
2845
  const KNOWN_OTHER_CONFIGS = {
2827
2846
  ".cursorrules": "Cursor",
2828
2847
  ".github/copilot-instructions.md": "GitHub Copilot",
@@ -2903,13 +2922,20 @@ function detectProject() {
2903
2922
  function determineTargets(detected, targetValue, harnesses) {
2904
2923
  if (targetValue)
2905
2924
  return [targetValue];
2906
- const targets = [];
2907
- if (harnesses.includes("claude"))
2908
- targets.push("CLAUDE.md");
2909
- if (harnesses.includes("codex"))
2910
- targets.push("AGENTS.md");
2925
+ // 🔴 WHICH FILE A HARNESS COMPILES INTO IS THE LAYOUT'S ANSWER, not a pair of
2926
+ // name literals here. This read `harnesses.includes("claude") -> "CLAUDE.md"`
2927
+ // and `includes("codex") -> "AGENTS.md"` — the registry restated, in the one
2928
+ // place a third adapter would be forgotten. `getAdapter` is alias-aware, so
2929
+ // the short `"claude"` that `init` uses resolves exactly as it did.
2930
+ //
2931
+ // ITERATING THE REGISTRY, NOT `harnesses`, KEEPS THE ORDER FIXED: the old
2932
+ // code always produced CLAUDE.md before AGENTS.md whatever order the config
2933
+ // listed, and that order reaches the printed spec targets. Verified over all
2934
+ // five inputs (each harness alone, both orders, and none): identical.
2935
+ const selected = new Set(harnesses.map((h) => (0, adapter_registry_js_1.getAdapter)(h)?.name).filter((n) => n !== undefined));
2936
+ const targets = adapter_registry_js_1.ADAPTERS.filter((a) => selected.has(a.name)).map((a) => a.layout.instructionFile);
2911
2937
  if (targets.length === 0)
2912
- targets.push("CLAUDE.md");
2938
+ targets.push(adapter_registry_js_1.defaultAdapter.layout.instructionFile);
2913
2939
  // Any existing instruction file without a spec also gets one.
2914
2940
  for (const f of detected.instructionFiles) {
2915
2941
  if (!f.hasSpec && !targets.includes(f.path))
@@ -3257,6 +3283,7 @@ function ensureVigilesDevDep() {
3257
3283
  * (`claude`). Everything else passes through trimmed + lowercased. */
3258
3284
  function shortHarness(name) {
3259
3285
  const n = name.trim().toLowerCase();
3286
+ // eslint-disable-next-line no-restricted-syntax -- COMPOSITION ROOT'S OWN UI, not a harness decision: `init` keys its install/plan path on a short form, and this is the one place the two spellings are related. Nothing about the harness is read from the answer.
3260
3287
  return n === "claude-code" ? "claude" : n;
3261
3288
  }
3262
3289
  /** Which harnesses to set up, in precedence order: an explicit `--harness=`
@@ -3631,7 +3658,7 @@ function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
3631
3658
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
3632
3659
  if (!sev)
3633
3660
  return { issues: 0, errors: 0 };
3634
- if (!adapter.capabilities.subagents) {
3661
+ if (!adapter.subagents) {
3635
3662
  reportNotApplicable("Subagent tool-contract check", "subagents", adapter, silent);
3636
3663
  return { issues: 0, errors: 0 };
3637
3664
  }
@@ -3674,7 +3701,7 @@ function checkHookEvents(config, silent, adapter, scanRoot) {
3674
3701
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
3675
3702
  if (!sev)
3676
3703
  return { issues: 0, errors: 0 };
3677
- if (!adapter.capabilities.shellHooks) {
3704
+ if (!adapter.shellHooks) {
3678
3705
  reportNotApplicable("Hook-event check", "shell hooks", adapter, silent);
3679
3706
  return { issues: 0, errors: 0 };
3680
3707
  }
@@ -3705,7 +3732,7 @@ function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
3705
3732
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
3706
3733
  if (!sev)
3707
3734
  return { issues: 0, errors: 0 };
3708
- if (!adapter.capabilities.subagents) {
3735
+ if (!adapter.subagents) {
3709
3736
  reportNotApplicable("Subagent-frontmatter check", "subagents", adapter, silent);
3710
3737
  return { issues: 0, errors: 0 };
3711
3738
  }
@@ -3818,7 +3845,7 @@ function checkDisallowedTools(config, silent, adapter, scanRoot) {
3818
3845
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
3819
3846
  if (!sev)
3820
3847
  return { issues: 0, errors: 0 };
3821
- if (!adapter.capabilities.subagents) {
3848
+ if (!adapter.subagents) {
3822
3849
  reportNotApplicable("Disallowed-tools check", "subagents", adapter, silent);
3823
3850
  return { issues: 0, errors: 0 };
3824
3851
  }
@@ -4102,7 +4129,7 @@ function checkHookBlockIneffective(config, silent, adapter, scanRoot) {
4102
4129
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-block-ineffective"]);
4103
4130
  if (!sev)
4104
4131
  return { issues: 0, errors: 0 };
4105
- if (!adapter.capabilities.shellHooks) {
4132
+ if (!adapter.shellHooks) {
4106
4133
  reportNotApplicable("Hook-block check", "shell hooks", adapter, silent);
4107
4134
  return { issues: 0, errors: 0 };
4108
4135
  }
@@ -4136,7 +4163,7 @@ function checkHookMatcher(config, silent, adapter, scanRoot) {
4136
4163
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-matcher"]);
4137
4164
  if (!sev)
4138
4165
  return { issues: 0, errors: 0 };
4139
- if (!adapter.capabilities.shellHooks) {
4166
+ if (!adapter.shellHooks) {
4140
4167
  reportNotApplicable("Hook-matcher check", "shell hooks", adapter, silent);
4141
4168
  return { issues: 0, errors: 0 };
4142
4169
  }
@@ -4167,7 +4194,7 @@ function checkMcpHookTargets(config, silent, adapter, scanRoot) {
4167
4194
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
4168
4195
  if (!sev)
4169
4196
  return { issues: 0, errors: 0 };
4170
- if (!adapter.capabilities.shellHooks) {
4197
+ if (!adapter.shellHooks) {
4171
4198
  reportNotApplicable("MCP hook-target check", "shell hooks", adapter, silent);
4172
4199
  return { issues: 0, errors: 0 };
4173
4200
  }
@@ -4199,7 +4226,7 @@ function checkHookScriptExists(config, silent, adapter, scanRoot) {
4199
4226
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
4200
4227
  if (!sev)
4201
4228
  return { issues: 0, errors: 0 };
4202
- if (!adapter.capabilities.shellHooks) {
4229
+ if (!adapter.shellHooks) {
4203
4230
  reportNotApplicable("Hook-script existence check", "shell hooks", adapter, silent);
4204
4231
  return { issues: 0, errors: 0 };
4205
4232
  }
@@ -4234,7 +4261,7 @@ function checkMcpToolResolves(config, silent, adapter, scanRoot) {
4234
4261
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
4235
4262
  if (!sev)
4236
4263
  return { issues: 0, errors: 0 };
4237
- if (!adapter.capabilities.subagents) {
4264
+ if (!adapter.subagents) {
4238
4265
  reportNotApplicable("MCP tool-resolution check", "subagents", adapter, silent);
4239
4266
  return { issues: 0, errors: 0 };
4240
4267
  }
@@ -4408,7 +4435,6 @@ async function handleMeasure(restArgs, args) {
4408
4435
  const json = args.includes("--json");
4409
4436
  const harnessFlag = harnessFlagFrom(args);
4410
4437
  const adapter = resolveCommandHarness(dir, harnessFlag).adapter;
4411
- const harness = adapter.name === "codex" ? "codex" : "claude-code";
4412
4438
  const promptsPath = flagValue(args, "--prompts");
4413
4439
  if (!promptsPath) {
4414
4440
  console.error("measure needs --prompts=<file.json> (a map of skill name → { prompts, irrelevant }).");
@@ -4437,13 +4463,13 @@ async function handleMeasure(restArgs, args) {
4437
4463
  concurrency,
4438
4464
  minPrompts: num("--min-prompts"),
4439
4465
  model,
4440
- harness,
4466
+ adapter,
4441
4467
  });
4442
4468
  const collisions = await (0, scan_behavioral_js_1.measurePluginSelection)(dir, promptSet, {
4443
4469
  concurrency,
4444
4470
  trials: num("--trials"),
4445
4471
  model,
4446
- harness,
4472
+ adapter,
4447
4473
  });
4448
4474
  if (json) {
4449
4475
  console.log(JSON.stringify({ trigger, collisions }, null, 2));
@@ -5009,6 +5035,10 @@ async function handleRunScripts(kind, args, restArgs, excludes) {
5009
5035
  // a flag: the run already happened, and this is the runner recording what it
5010
5036
  // saw — the same shape as the flight-recorder ledger it already appends to.
5011
5037
  recordRunCoverage(cwd, results, kind, harnessFlagFrom(args));
5038
+ // The ignore file keeps NEW copies of `.vigiles/` local files out of git, but
5039
+ // cannot untrack one a repo already committed. Said here, on the CLI, because
5040
+ // it spawns `git` — never from a hook runtime.
5041
+ (0, local_files_tracked_js_1.warnTrackedLocalFiles)(cwd);
5012
5042
  console.log("\n" + (0, run_scripts_js_1.formatScriptSummary)(results));
5013
5043
  if ((0, run_scripts_js_1.anyFailed)(results))
5014
5044
  process.exit(1);
@@ -5857,25 +5887,34 @@ async function installHookFile(file, adapter, registeredProviders = []) {
5857
5887
  `${(0, hook_install_js_1.hookGateRef)(ref, adapter.layout.projectRootTokens)} || exit ${(0, hook_install_js_1.hookRuntimeMissingExit)((0, hook_program_js_1.dispatchKind)(program))}`,
5858
5888
  dialect: adapter.dialect,
5859
5889
  hookProtocol: adapter.hookProtocol,
5860
- settingsFormat: adapter.layout.settingsFormat,
5890
+ settings: adapter.layout.settings,
5861
5891
  registeredProviders,
5862
5892
  });
5863
5893
  // Tamper-evident stamp beside the source (one dir → basename is unique).
5864
5894
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)((0, hook_runtime_js_1.hookStampPath)(file)), { recursive: true });
5865
5895
  (0, node_fs_1.writeFileSync)((0, hook_runtime_js_1.hookStampPath)(file), JSON.stringify({ file, stamp: compiled.stamp }, null, 2) + "\n");
5866
5896
  // Merge into the harness's native config, idempotently.
5867
- const format = adapter.layout.settingsFormat;
5897
+ //
5898
+ // 🔴 THREE FORMAT BRANCHES BECAME ZERO, and they were three because the ONE
5899
+ // enum they all read was standing in for two different questions. Reading
5900
+ // and writing the file is the ENCODING (`layout.settings`, a codec); folding
5901
+ // our registrations into what is already there is the entry SHAPE
5902
+ // (`hookProtocol.mergeRegistrations` — CC nests several commands under one
5903
+ // matcher, Codex carries one command per entry). A TOML harness with
5904
+ // CC-shaped entries was expressible under the enum and would have been
5905
+ // merged wrong; neither port can be wrong about its own half.
5868
5906
  const settingsAbs = (0, node_path_1.resolve)(process.cwd(), adapter.layout.settingsPath);
5869
5907
  const existing = (0, node_fs_1.existsSync)(settingsAbs)
5870
- ? format === "toml"
5871
- ? (0, toml_1.parse)((0, node_fs_1.readFileSync)(settingsAbs, "utf-8"))
5872
- : JSON.parse((0, node_fs_1.readFileSync)(settingsAbs, "utf-8"))
5908
+ ? adapter.layout.settings.parse((0, node_fs_1.readFileSync)(settingsAbs, "utf-8"))
5873
5909
  : {};
5874
- const merged = format === "toml"
5875
- ? (0, hook_install_js_1.mergeHooksToml)(existing, compiled.hooks, ref)
5876
- : (0, hook_install_js_1.mergeHooksJson)(existing, compiled.hooks, ref);
5910
+ // `shellHooks` narrows the adapter union: a harness whose hooks are code
5911
+ // modules has no settings block to install into, and the type says so.
5912
+ if (!adapter.shellHooks) {
5913
+ throw new Error(`Harness "${adapter.name}" has no shell-hook settings to install into (hooks are code modules).`);
5914
+ }
5915
+ const merged = adapter.hookProtocol.mergeRegistrations(existing, compiled.hooks, ref);
5877
5916
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(settingsAbs), { recursive: true });
5878
- (0, node_fs_1.writeFileSync)(settingsAbs, (0, hook_install_js_1.serializeConfig)(merged, format));
5917
+ (0, node_fs_1.writeFileSync)(settingsAbs, adapter.layout.settings.render(merged));
5879
5918
  // No silent skips: warn loudly only where a hook's OUTPUT genuinely may not
5880
5919
  // apply on this harness. INJECT's `additionalContext` shape is now CONFIRMED
5881
5920
  // shared with Codex (per the official hooks docs), so an inject hook only
@@ -5900,6 +5939,7 @@ async function installHookFile(file, adapter, registeredProviders = []) {
5900
5939
  `(stderr at exit 0 goes to the debug log, not the transcript, and the model ` +
5901
5940
  `never sees it). Injectable here: ${injectable.join(", ") || "(none)"}. ` +
5902
5941
  `Move the hook to one of those events, or use run() if you meant an action.`;
5942
+ // eslint-disable-next-line no-restricted-syntax -- KEPT ON PURPOSE (design §2 "the eighth site" / §6). This is a per-channel MEASUREMENT STATUS — react output is CONFIRMED only on Claude Code — and there is no port to read it from: `EventCapability` carries `carries`/`honours`/`matcher`/`denyShape` and no verified-status column. The fix is that column, owned by the capability table, not by this port. Until it exists, a name check that says so is honest and a fabricated capability field would not be.
5903
5943
  }
5904
5944
  else if (adapter.name !== "claude-code") {
5905
5945
  if (role === "inject" && !injectable.includes(event)) {
@@ -6191,7 +6231,6 @@ async function runAuditServe(report, adoptable, cliErr) {
6191
6231
  */
6192
6232
  async function runAutoTrigger(dir, report, adapter, args) {
6193
6233
  const json = args.includes("--json");
6194
- const harness = adapter.name === "codex" ? "codex" : "claude-code";
6195
6234
  const skills = report.skills
6196
6235
  .filter((s) => s.hasDescription && !s.userInvoked && s.description)
6197
6236
  .map((s) => ({ name: s.name, description: s.description ?? "" }));
@@ -6210,7 +6249,7 @@ async function runAutoTrigger(dir, report, adapter, args) {
6210
6249
  minPrompts: audit_prompts_js_1.AUTO_RECALL_COUNT,
6211
6250
  minDistance: audit_prompts_js_1.AUTO_MIN_DISTANCE,
6212
6251
  model,
6213
- harness,
6252
+ adapter,
6214
6253
  // Discover candidates with the resolved adapter's layout/dialect — a Codex
6215
6254
  // repo's skills live under the Codex layout, not the default CC one.
6216
6255
  layout: adapter.layout,
@@ -6222,14 +6261,14 @@ async function runAutoTrigger(dir, report, adapter, args) {
6222
6261
  // this proves the wrong one actually fires). Only meaningful with ≥2 model-
6223
6262
  // invocable skills (a lone skill can't collide); reuses the same auto prompts.
6224
6263
  const collisions = skills.length >= 2
6225
- ? await (0, scan_behavioral_js_1.measurePluginSelection)(dir, promptSet, { model, harness })
6264
+ ? await (0, scan_behavioral_js_1.measurePluginSelection)(dir, promptSet, { model, adapter })
6226
6265
  : null;
6227
6266
  // Third behavioral eval (same consent): adversarial-gate — do enforcement-gate
6228
6267
  // skills HOLD when the agent is told to violate them? Auto-derives its own
6229
6268
  // attacks; a no-op (no model calls) when the plugin declares no gate skills.
6230
6269
  const gates = await (0, scan_behavioral_js_1.measureGateAdversarial)(dir, {
6231
6270
  model,
6232
- harness,
6271
+ adapter,
6233
6272
  layout: adapter.layout,
6234
6273
  dialect: adapter.dialect,
6235
6274
  });
@@ -6264,7 +6303,7 @@ async function runAutoTrigger(dir, report, adapter, args) {
6264
6303
  *
6265
6304
  * Returns `execute` (run the executing checks?) + a `note` to print at the end.
6266
6305
  */
6267
- async function resolveExecution(s, decision, json, harness) {
6306
+ async function resolveExecution(s, decision, json, access) {
6268
6307
  if (decision.kind === "run")
6269
6308
  return { execute: true, note: null };
6270
6309
  if (decision.kind === "skip")
@@ -6273,7 +6312,7 @@ async function resolveExecution(s, decision, json, harness) {
6273
6312
  note: json ? null : (0, scan_trigger_suggest_js_1.formatExecuteSkip)(decision.reason),
6274
6313
  };
6275
6314
  // ask — prompt once, then remember the answer.
6276
- const answer = await askOnce(buildExecuteDisclosure(s, harness));
6315
+ const answer = await askOnce(buildExecuteDisclosure(s, access));
6277
6316
  const yes = /^y(es)?$/i.test(answer); // default NO (executes your hooks / servers)
6278
6317
  rememberAuditMeasure(yes);
6279
6318
  return {
@@ -6284,10 +6323,11 @@ async function resolveExecution(s, decision, json, harness) {
6284
6323
  };
6285
6324
  }
6286
6325
  /** The bundled consent prompt — discloses exactly what will execute (and what it
6287
- * costs) so the yes is informed. Default NO. Harness-aware: a Codex repo measures
6288
- * via the codex CLI (not a Claude env var), so the cost wording must not falsely
6289
- * read "no model access" in exactly the case the prompt is meant to disclose. */
6290
- function buildExecuteDisclosure(s, harness) {
6326
+ * costs) so the yes is informed. Default NO. Harness-aware WITHOUT knowing which
6327
+ * harness: the cost wording comes from the driving adapter's own `access`, so a
6328
+ * repo on any harness is disclosed in that harness's terms — the old version had
6329
+ * to name Codex to stop the prompt falsely reading "no model access". */
6330
+ function buildExecuteDisclosure(s, access) {
6291
6331
  const lines = ["\nRun the executing checks against your harness?"];
6292
6332
  if (s.hasMcp)
6293
6333
  lines.push(" · start your MCP servers — connects to their backends");
@@ -6297,29 +6337,51 @@ function buildExecuteDisclosure(s, harness) {
6297
6337
  const what = s.triggerableSkills >= 2
6298
6338
  ? "measure whether skills fire and collide"
6299
6339
  : "measure whether skills fire";
6300
- lines.push(` · ${what} (${triggerCostWording(harness)})`);
6340
+ lines.push(` · ${what} (${triggerCostWording(access)})`);
6301
6341
  }
6302
6342
  if (s.gateSkills > 0) {
6303
6343
  // The adversarial-gate eval runs the FULL (unstubbed) skill — the most
6304
6344
  // expensive check — so disclose it separately when gate skills are present.
6305
- lines.push(` · test whether ${String(s.gateSkills)} enforcement-gate skill${s.gateSkills === 1 ? "" : "s"} hold under pressure — runs the full skill (${triggerCostWording(harness)})`);
6345
+ lines.push(` · test whether ${String(s.gateSkills)} enforcement-gate skill${s.gateSkills === 1 ? "" : "s"} hold under pressure — runs the full skill (${triggerCostWording(access)})`);
6306
6346
  }
6307
6347
  if (s.adoptableRefs) {
6308
- lines.push(` · draft + verify your instruction file's references (${triggerCostWording(harness)})`);
6348
+ lines.push(` · draft + verify your instruction file's references (${triggerCostWording(access)})`);
6309
6349
  }
6310
6350
  lines.push("Asked once — remembered in .vigilesrc.json. [y/N] ");
6311
6351
  return lines.join("\n");
6312
6352
  }
6313
- /** Cost/availability wording for the trigger tier, per harness. Codex runs on the
6314
- * codex CLI (its own auth/plan), so it's never gated on a Claude env var. */
6315
- function triggerCostWording(harness) {
6316
- if (harness === "codex")
6317
- return "your Codex CLI, $0 metered — skips if `codex` isn't on PATH";
6318
- return !(0, scan_trigger_suggest_js_1.hasModelAccess)(process.env)
6319
- ? "needs model access — none detected, will skip"
6320
- : (0, scan_trigger_suggest_js_1.isMeteredAccess)(process.env)
6321
- ? "⚠ spends API credits"
6322
- : "your subscription, $0 metered";
6353
+ /**
6354
+ * Cost/availability wording for the trigger tier, read off the DRIVING HARNESS's
6355
+ * own answer instead of branching on its name.
6356
+ *
6357
+ * 🔴 IT REPLACES THREE THINGS AT ONCE: `harness === "codex"`, and the pair of
6358
+ * Claude-env predicates that ran for every other harness. The old Codex arm was a
6359
+ * guess ("skips if `codex` isn't on PATH") because nothing had asked; `access`
6360
+ * asks, so the prompt now discloses what will actually happen.
6361
+ */
6362
+ function triggerCostWording(access) {
6363
+ switch (access.kind) {
6364
+ case "none":
6365
+ return `needs model access — none detected, will skip (${access.fix})`;
6366
+ case "metered":
6367
+ return "⚠ spends API credits";
6368
+ case "subscription":
6369
+ return "your subscription, $0 metered";
6370
+ }
6371
+ }
6372
+ /**
6373
+ * This adapter's answer to "is a real model reachable, and on whose bill".
6374
+ *
6375
+ * A reference-only harness has no executing tier at all, which is a `none` whose
6376
+ * `fix` says so rather than a Claude-shaped credential hint.
6377
+ */
6378
+ async function modelAccessFor(adapter) {
6379
+ if (!adapter.harnessTesting)
6380
+ return {
6381
+ kind: "none",
6382
+ fix: `${adapter.name} has no executing-tier driver in this build`,
6383
+ };
6384
+ return (await adapter.liveDriver()).access(process.env);
6323
6385
  }
6324
6386
  /** Run the trigger tier: a curated `--prompts` file, else auto-generated probes. */
6325
6387
  async function runTriggerTier(dir, report, adapter, args) {
@@ -6509,7 +6571,14 @@ async function main() {
6509
6571
  const json = args.includes("--json");
6510
6572
  // A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
6511
6573
  // under one marketplace.json) expands into its members and ranks them.
6512
- const market = dirs.length === 1 ? (0, scan_js_1.inspectMarketplace)((0, node_path_1.resolve)(dirs[0])) : null;
6574
+ // The layout is passed rather than defaulted: `inspectMarketplace` reads
6575
+ // `dirname(layout.manifestPath)`, which is a different directory per
6576
+ // harness, and it used to fall back to the Claude Code layout inside the
6577
+ // harness-agnostic detector. `harnessLayoutFor` is the same resolution
6578
+ // the audit itself uses a few lines down.
6579
+ const market = dirs.length === 1
6580
+ ? (0, scan_js_1.inspectMarketplace)((0, node_path_1.resolve)(dirs[0]), harnessLayoutFor((0, node_path_1.resolve)(dirs[0]), config, harnessFlagFrom(args)))
6581
+ : null;
6513
6582
  // A marketplace whose members are all EXTERNAL expands to nothing, and the
6514
6583
  // fallback below already says what to do about that: the target is the
6515
6584
  // directory itself.
@@ -6659,21 +6728,13 @@ async function main() {
6659
6728
  if (selection.kind === "notice") {
6660
6729
  console.log(`⚠ ${selection.notice}`);
6661
6730
  }
6662
- // Freshness: warn if our hand-maintained CC catalog drifted from the
6663
- // user's INSTALLED claude-code (read-local, best-effort, never throws).
6664
- if (adapter.name === "claude-code") {
6665
- const drift = (0, dialect_drift_js_1.formatDialectDrift)((0, dialect_drift_js_1.checkDialectDrift)());
6666
- if (drift)
6667
- console.log(drift);
6668
- // Adoption: this repo depends on vigiles, but can the agent SEE the
6669
- // skills it ships? `npm install` drops them in node_modules, which
6670
- // Claude Code never scans — the plugin install is what wires them,
6671
- // and until it runs the whole teaching surface is silently absent.
6672
- // Advisory only (machine state, not repo state) — never scored.
6673
- const reach = (0, skill_reachability_js_1.formatSkillReachability)((0, skill_reachability_js_1.checkSkillReachability)(root));
6674
- if (reach)
6675
- console.log(reach);
6676
- }
6731
+ // Install diagnostics from the DRIVING harness — "is what vigiles
6732
+ // assumes about this harness's install true on this machine". Advisory
6733
+ // only (machine state, not repo state), never scored, printed as-is.
6734
+ // This used to be `if (adapter.name === "claude-code") { …two Claude
6735
+ // Code checks… }`; the checks were right and the gate was the defect.
6736
+ for (const line of adapter.advisories((0, install_reader_js_1.buildInstallReader)(adapter, root)))
6737
+ console.log(line);
6677
6738
  console.log("");
6678
6739
  }
6679
6740
  // The versioned AuditReport is the product boundary — the same JSON the
@@ -6693,12 +6754,19 @@ async function main() {
6693
6754
  // `Evaluated` ring needs to know whether this run will ever ask the
6694
6755
  // skill-firing question — see `firingMeasured` below.
6695
6756
  const isForeign = root !== process.cwd();
6757
+ // ONE read of "can we reach a model, and on whose bill", from the driving
6758
+ // adapter. It replaces a name check OR-ed with one harness's env vars at
6759
+ // four sites below, and the two hard-coded "authenticate the `claude` CLI"
6760
+ // strings that printed whatever harness was driving.
6761
+ const modelAccess = await modelAccessFor(adapter);
6696
6762
  const surfaces = {
6697
6763
  hasMcp: report.mcp && !isForeign,
6698
6764
  triggerableSkills: report.skills.filter((s) => s.hasDescription && !s.userInvoked).length,
6699
6765
  gateSkills: (0, scan_behavioral_js_1.detectGateSkills)(report.skills).length,
6700
- adoptableRefs: adapter.name === "claude-code" &&
6701
- (0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, adapter.layout.instructionFile)),
6766
+ // NOT gated on a harness name any more. The drafter is the adapter's
6767
+ // own eval driver and the verifier is deterministic and harness-free,
6768
+ // so an instruction file is an instruction file: prose, on any harness.
6769
+ adoptableRefs: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, adapter.layout.instructionFile)),
6702
6770
  };
6703
6771
  // `decideExecute` is PURE and prompt-free, so the read-vs-run outcome can
6704
6772
  // be known before anything prints. Only a settled `run` (a remembered yes)
@@ -6718,7 +6786,7 @@ async function main() {
6718
6786
  });
6719
6787
  const firingMeasured = execDecision.kind === "run" &&
6720
6788
  surfaces.triggerableSkills > 0 &&
6721
- (adapter.name === "codex" || (0, scan_trigger_suggest_js_1.hasModelAccess)(process.env));
6789
+ modelAccess.kind !== "none";
6722
6790
  // The report scaffold WITHOUT the rule map — the map's catalog
6723
6791
  // enrichment (enabled-state / "documented but OFF") enumerates the repo's
6724
6792
  // linter, which is gated on the SAME audit.measure consent as the
@@ -6814,7 +6882,7 @@ async function main() {
6814
6882
  // that isn't shipped, so it lives in the vigiles testing API.)
6815
6883
  // `surfaces` + `execDecision` were resolved above (the ring needed them);
6816
6884
  // this only turns an `ask` into a prompt and remembers the answer.
6817
- const { execute, note: execNote } = await resolveExecution(surfaces, execDecision, json, adapter.name);
6885
+ const { execute, note: execNote } = await resolveExecution(surfaces, execDecision, json, modelAccess);
6818
6886
  // Consent is now settled (and remembered via .vigilesrc.json, which
6819
6887
  // computeRuleRouting re-reads) — route the prose rules, enumerating the
6820
6888
  // live catalog when consented + own-repo. Feeds the JSON report, the
@@ -6881,17 +6949,14 @@ async function main() {
6881
6949
  // of the same consent (`execute`), and only when a model is reachable;
6882
6950
  // otherwise it's a one-line note (never a hang).
6883
6951
  if (execute && surfaces.triggerableSkills > 0) {
6884
- // Model-access detection is per-harness: `hasModelAccess` reads Claude
6885
- // env (claude CLI / ANTHROPIC_API_KEY). A Codex repo authenticates the
6886
- // codex CLI instead, so we DON'T gate it on a Claude var — the Codex
6887
- // probe checks `codexDriver.available()` internally and self-reports
6888
- // unavailable. (harness-parity: never block Codex behind a CC check.)
6889
- const modelReachable = adapter.name === "codex" || (0, scan_trigger_suggest_js_1.hasModelAccess)(process.env);
6890
- if (modelReachable) {
6952
+ // The adapter answered this once, above. `fix` is THIS harness's own
6953
+ // sentence: the string here used to say "authenticate the `claude` CLI
6954
+ // or set ANTHROPIC_API_KEY" whatever harness the repo was on.
6955
+ if (modelAccess.kind !== "none") {
6891
6956
  await runTriggerTier(targets[0], report, adapter, args);
6892
6957
  }
6893
6958
  else if (!json) {
6894
- console.log("\nℹ skill firing not measured — no model access (authenticate the `claude` CLI or set ANTHROPIC_API_KEY).");
6959
+ console.log(`\nℹ skill firing not measured — no model access (${modelAccess.fix}).`);
6895
6960
  }
6896
6961
  }
6897
6962
  // The adoption preview — "what would vigiles catch in YOUR repo?" The model
@@ -6899,34 +6964,52 @@ async function main() {
6899
6964
  // VERIFIES each, so "M broken right now" is trustworthy though the extraction
6900
6965
  // is probabilistic. Same consent as the trigger tier (`surfaces.adoptableRefs`
6901
6966
  // makes a bare instruction-file repo consent-eligible — the prime adoption
6902
- // target). v1: instruction-file only; drafting drives the `claude` CLI, so
6903
- // `adoptableRefs` is Claude Code only (the Codex deferral note is printed
6904
- // below as a LOUD harness-parity deferral, never a silent CC-only path).
6967
+ // target).
6968
+ //
6969
+ // 🔴 NO LONGER CLAUDE CODE ONLY, and the feature gate that said so is
6970
+ // gone rather than widened. The drafter is the driving adapter's own eval
6971
+ // driver (`draftWith`); the verifier was always deterministic and
6972
+ // harness-free. What made this CC-only was that `defaultDraft` defaulted
6973
+ // to the Claude runner and parser and nobody had passed anything else —
6974
+ // a TODO, which the old `adapter.name === "claude-code"` froze into the
6975
+ // report as if it were a vendor fact.
6905
6976
  let adoptabilityResult;
6906
6977
  if (execute && surfaces.adoptableRefs) {
6907
- if ((0, scan_trigger_suggest_js_1.hasModelAccess)(process.env)) {
6978
+ if (modelAccess.kind !== "none" && adapter.harnessTesting) {
6908
6979
  const instrPath = (0, node_path_1.resolve)(root, adapter.layout.instructionFile);
6909
- adoptabilityResult = await (0, adoptability_js_1.runAdoptabilityTier)({
6910
- instructionContent: (0, node_fs_1.readFileSync)(instrPath, "utf-8"),
6911
- basePath: root,
6912
- });
6913
- if (!json)
6914
- console.log("\n" +
6915
- (0, adoptability_js_1.formatAdoptability)(adoptabilityResult, adapter.layout.instructionFile));
6980
+ // 🔴 CAUGHT, because a FAILED RUN and an EMPTY RESULT are different
6981
+ // answers and this tier used to print the second for both. It
6982
+ // reaches the harness binary directly — no behavioral probe in
6983
+ // front of it to self-report unavailability — so on a machine
6984
+ // without that CLI the drafter would exit non-zero with no output
6985
+ // and the report would say "no machine-verifiable references
6986
+ // found". `DraftRunFailed` is thrown for exactly that; anything
6987
+ // else is a real bug and is left to propagate.
6988
+ try {
6989
+ adoptabilityResult = await (0, adoptability_js_1.runAdoptabilityTier)({
6990
+ instructionContent: (0, node_fs_1.readFileSync)(instrPath, "utf-8"),
6991
+ basePath: root,
6992
+ draft: (0, adoptability_js_1.draftWith)((await adapter.liveDriver()).evalDriver),
6993
+ });
6994
+ if (!json)
6995
+ console.log("\n" +
6996
+ (0, adoptability_js_1.formatAdoptability)(adoptabilityResult, adapter.layout.instructionFile));
6997
+ }
6998
+ catch (e) {
6999
+ if (!(e instanceof adoptability_js_1.DraftRunFailed))
7000
+ throw e;
7001
+ if (!json)
7002
+ console.log(`\nℹ adoptability not measured — ${e.message}.`);
7003
+ }
6916
7004
  }
6917
7005
  else if (!json && surfaces.triggerableSkills === 0) {
6918
7006
  // Only when the trigger tier didn't already print the same note.
6919
- console.log("\nℹ adoptability not measured — no model access (authenticate the `claude` CLI or set ANTHROPIC_API_KEY).");
7007
+ console.log(`\nℹ adoptability not measured — no model access (${modelAccess.kind === "none" ? modelAccess.fix : `${adapter.name} has no executing-tier driver`}).`);
6920
7008
  }
6921
7009
  }
6922
- // LOUD harness-parity deferral: adoptability drafting drives the `claude`
6923
- // CLI, so a Codex repo with an instruction file is told it's a follow-up,
6924
- // never silently skipped (research/adoption-gateway-preview.md, increment 4).
6925
- if (!json &&
6926
- adapter.name === "codex" &&
6927
- (0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, adapter.layout.instructionFile))) {
6928
- console.log("\nℹ adoptability preview (what vigiles would catch in your repo) is Claude Code only for now — Codex support is a follow-up.");
6929
- }
7010
+ // The "adoptability preview is Claude Code only for now — Codex support
7011
+ // is a follow-up" note used to print here. It is DELETED, not reworded:
7012
+ // Codex is not a follow-up, so there is nothing to defer.
6930
7013
  // The loud read-vs-run nudge for a headless / remembered-no skip — printed
6931
7014
  // AFTER the report so the deterministic read leads.
6932
7015
  if (execNote)