vigiles 5.1.0 → 5.2.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.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/claude-code/adapter.js +1 -0
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +20 -6
  4. package/dist/adapters/claude-code/agent-runtime.js +51 -8
  5. package/dist/adapters/claude-code/dialect.js +19 -0
  6. package/dist/adapters/claude-code/effect-region.d.ts +9 -0
  7. package/dist/adapters/claude-code/effect-region.js +45 -0
  8. package/dist/adapters/claude-code/layout.js +3 -0
  9. package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
  10. package/dist/adapters/claude-code/skill-runtime.js +48 -0
  11. package/dist/adapters/codex/adapter.js +3 -0
  12. package/dist/adapters/codex/layout.js +3 -0
  13. package/dist/adapters/opencode/adapter.js +1 -0
  14. package/dist/adapters/opencode/layout.js +3 -0
  15. package/dist/check.d.ts +8 -0
  16. package/dist/check.js +27 -3
  17. package/dist/cli.js +323 -88
  18. package/dist/core/adapter.d.ts +10 -0
  19. package/dist/core/bash-effects.d.ts +41 -0
  20. package/dist/core/bash-effects.js +405 -0
  21. package/dist/core/compile.d.ts +3 -1
  22. package/dist/core/compile.js +162 -39
  23. package/dist/core/dialect.d.ts +10 -0
  24. package/dist/core/effects.d.ts +172 -0
  25. package/dist/core/effects.js +245 -0
  26. package/dist/core/layout.d.ts +6 -0
  27. package/dist/core/mcp-tool.d.ts +1 -1
  28. package/dist/core/orphans.js +21 -0
  29. package/dist/core/spec.d.ts +142 -3
  30. package/dist/core/spec.js +48 -0
  31. package/dist/core/tool-contract.d.ts +1 -1
  32. package/dist/core/types.d.ts +6 -6
  33. package/dist/core/validate.js +4 -4
  34. package/dist/harness-test.d.ts +7 -0
  35. package/dist/harness-test.js +19 -7
  36. package/dist/leaderboard.d.ts +2 -0
  37. package/dist/leaderboard.js +2 -0
  38. package/dist/optimize.d.ts +74 -0
  39. package/dist/optimize.js +94 -0
  40. package/dist/scaffold-test.d.ts +30 -0
  41. package/dist/scaffold-test.js +158 -0
  42. package/dist/scan.d.ts +40 -0
  43. package/dist/scan.js +91 -43
  44. package/dist/score-explainer.d.ts +69 -0
  45. package/dist/score-explainer.js +169 -0
  46. package/dist/test-coverage.d.ts +7 -0
  47. package/dist/test-coverage.js +39 -24
  48. package/package.json +2 -1
  49. package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -4
  50. package/skills/edit-spec/SKILL.md +1 -1
package/dist/cli.js CHANGED
@@ -19,14 +19,14 @@ const cli_flags_js_1 = require("./cli-flags.js");
19
19
  const setup_plan_js_1 = require("./setup-plan.js");
20
20
  const types_js_1 = require("./core/types.js");
21
21
  const test_coverage_js_1 = require("./test-coverage.js");
22
+ const scaffold_test_js_1 = require("./scaffold-test.js");
22
23
  const scan_js_1 = require("./scan.js");
23
- const tool_contract_js_1 = require("./core/tool-contract.js");
24
- const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
25
- const dialect_js_1 = require("./adapters/claude-code/dialect.js");
24
+ const score_explainer_js_1 = require("./score-explainer.js");
26
25
  const scan_behavioral_js_1 = require("./scan-behavioral.js");
27
26
  const adapter_registry_js_1 = require("./adapter-registry.js");
28
27
  const skill_harness_js_1 = require("./skill-harness.js");
29
28
  const leaderboard_js_1 = require("./leaderboard.js");
29
+ const optimize_js_1 = require("./optimize.js");
30
30
  const compile_js_1 = require("./core/compile.js");
31
31
  const proofs_js_1 = require("./core/proofs.js");
32
32
  const inline_js_1 = require("./core/inline.js");
@@ -35,7 +35,8 @@ const generate_schema_js_1 = require("./core/generate-schema.js");
35
35
  const compose_js_1 = require("./core/compose.js");
36
36
  const compile_generator_js_1 = require("./core/compile-generator.js");
37
37
  const action_gate_js_1 = require("./action-gate.js");
38
- const agent_runtime_js_2 = require("./adapters/claude-code/agent-runtime.js");
38
+ const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
39
+ const effect_region_js_1 = require("./adapters/claude-code/effect-region.js");
39
40
  const tool_intercept_js_1 = require("./tool-intercept.js");
40
41
  const refs_js_1 = require("./core/refs.js");
41
42
  const mcp_js_1 = require("./core/mcp.js");
@@ -869,6 +870,17 @@ async function runLint(restArgs, flags, config) {
869
870
  const json = flags.includes("--json");
870
871
  const silent = summary || json;
871
872
  const files = findInstructionFiles(restArgs);
873
+ // Resolve the active harness ONCE so the harness-specific checks below run
874
+ // against the right adapter's dialect (tool/event catalogs) and surfaces —
875
+ // not a hard-coded Claude Code default. A subagent-surface rule reports n/a
876
+ // on a harness without subagents (Codex) rather than scanning nothing.
877
+ const harnessFlag = harnessFlagFrom(flags);
878
+ const lintSelection = (0, adapter_registry_js_1.resolveHarnessSelection)({
879
+ root: process.cwd(),
880
+ flag: harnessFlag,
881
+ configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
882
+ });
883
+ const adapter = lintSelection.adapter;
872
884
  // 1. Verify hashes and structure
873
885
  if (!silent) {
874
886
  if (files.length > 0) {
@@ -927,40 +939,41 @@ async function runLint(restArgs, flags, config) {
927
939
  // 7b. Untested-surface check — skills/agents/hooks shipping without a test or
928
940
  // eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
929
941
  // hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
930
- const untested = checkUntestedSurfaces(config, silent);
931
- // 7c. Agent tool-contract check — cross-reference each subagent's `tools:` rail
932
- // against the harness catalog (the moat). Off by default unless a severity is
933
- // configured; warning surfaces a typo/never-available tool, error gates CI.
934
- const toolContract = checkAgentToolContracts(config, silent);
942
+ const untested = checkUntestedSurfaces(config, silent, adapter);
943
+ // 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
944
+ // rail against the harness catalog (the moat). n/a on a harness with no
945
+ // subagents. Off by default unless a severity is configured; warning surfaces
946
+ // a typo/never-available tool, error gates CI.
947
+ const toolContract = checkSubagentToolContracts(config, silent, adapter);
935
948
  // 7d. Hook-event check — a hook registered under an event the harness doesn't
936
949
  // define never fires. High-precision (close typos only). Off unless configured.
937
- const hookEvents = checkHookEvents(config, silent);
938
- // 7e. Frontmatter-schema check — a skill/agent missing required frontmatter
939
- // (name; agents also description) won't load/register. High-confidence.
940
- const frontmatter = checkFrontmatterSchema(config, silent);
950
+ const hookEvents = checkHookEvents(config, silent, adapter);
951
+ // 7e. Subagent-frontmatter check — a subagent missing required frontmatter
952
+ // (name + description) won't register. n/a on a harness with no subagents.
953
+ const frontmatter = checkFrontmatterSchema(config, silent, adapter);
941
954
  // 7f. MCP-config check — a declared MCP server with no command/url can't start.
942
- const mcpConfig = checkMcpConfig(config, silent);
955
+ const mcpConfig = checkMcpConfig(config, silent, adapter);
943
956
  // 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
944
957
  // reliable trigger surface). Best-practice nudge; skills load without it.
945
- const skillFm = checkSkillFrontmatter(config, silent);
958
+ const skillFm = checkSkillFrontmatter(config, silent, adapter);
946
959
  // 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
947
960
  // the plugin doesn't declare can't resolve (the MCP half of the tool moat).
948
- const mcpToolResolves = checkMcpToolResolves(config, silent);
961
+ const mcpToolResolves = checkMcpToolResolves(config, silent, adapter);
949
962
  // 7i. Hook-script existence — a hook command referencing a missing script file
950
963
  // never runs (matches Anthropic's own `claude plugin validate`).
951
- const hookScripts = checkHookScriptExists(config, silent);
964
+ const hookScripts = checkHookScriptExists(config, silent, adapter);
952
965
  // 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
953
- // (the deny-side mirror of agent-tool-contract; close-typo only).
954
- const disallowedTools = checkDisallowedTools(config, silent);
966
+ // (the deny-side mirror of subagent-tool-contract; close-typo only).
967
+ const disallowedTools = checkDisallowedTools(config, silent, adapter);
955
968
  // 7k. Description-overlap — two model-invocable skills with near-identical
956
969
  // descriptions collide in the selector (deterministic NCD precision proxy).
957
- const descriptionOverlap = checkDescriptionOverlap(config, silent);
970
+ const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter);
958
971
  // 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
959
972
  // stricter than some loaders, so verify before enforcing).
960
- const frontmatterValid = checkFrontmatterValid(config, silent);
973
+ const frontmatterValid = checkFrontmatterValid(config, silent, adapter);
961
974
  // 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
962
975
  // targets an undeclared server (the moat applied to the hook surface).
963
- const mcpHookTargets = checkMcpHookTargets(config, silent);
976
+ const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
964
977
  // 8. Validate vigiles builder calls inside markdown code blocks. Default
965
978
  // is to validate every ref; illustrative blocks opt out via
966
979
  // `<!-- vigiles:ignore -->` (single block) or
@@ -1594,7 +1607,7 @@ function redirectSyncToolTargets(cwd, targets) {
1594
1607
  }
1595
1608
  /** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
1596
1609
  * instruction file (so `--lint` always delivers a spec), but never compiles
1597
- * OVER a hand-written file — that is left to the migrate-to-spec skill. */
1610
+ * OVER a hand-written file — that is left to the adopt-spec skill. */
1598
1611
  async function setupPillar1(detected, targetValue, harnesses) {
1599
1612
  const cwd = process.cwd();
1600
1613
  const written = [];
@@ -1619,7 +1632,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
1619
1632
  }
1620
1633
  if (targetExists && !targetHasHash((0, node_path_1.resolve)(cwd, target))) {
1621
1634
  needsMigration.push(target);
1622
- console.log(` ${target} already has content — port it into the spec with the migrate-to-spec skill, then \`vigiles compile\`.`);
1635
+ console.log(` ${target} already has content — adopt it into a spec with the adopt-spec skill, then \`vigiles compile\`.`);
1623
1636
  }
1624
1637
  }
1625
1638
  // Generate types + schema.
@@ -1811,7 +1824,7 @@ function printSetupSummary(opts) {
1811
1824
  console.log("\n---\nSetup complete.\n");
1812
1825
  const nextSteps = [];
1813
1826
  if (needsMigration.length > 0) {
1814
- nextSteps.push(`Port ${needsMigration.join(", ")} into its spec with the migrate-to-spec skill, then \`npx vigiles compile\``);
1827
+ nextSteps.push(`Adopt ${needsMigration.join(", ")} into a spec with the adopt-spec skill, then \`npx vigiles compile\``);
1815
1828
  }
1816
1829
  else if (specPathsList.length > 0) {
1817
1830
  nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
@@ -1958,16 +1971,16 @@ function checkIntegrityForFiles(files, severity, silent) {
1958
1971
  return severity === "error" ? errorCount : 0;
1959
1972
  }
1960
1973
  /**
1961
- * Apply the per-kind `untested-skill` / `untested-agent` / `untested-hook` rules:
1974
+ * Apply the per-kind `untested-skill` / `untested-subagent` / `untested-hook` rules:
1962
1975
  * find skills/agents/hooks with no test or eval (see src/test-coverage.ts). Each
1963
1976
  * kind is gated by its OWN rule severity — a kind set to `false` is not scanned;
1964
1977
  * "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
1965
1978
  * untested count plus the severity-gated error count.
1966
1979
  */
1967
- function checkUntestedSurfaces(config, silent) {
1980
+ function checkUntestedSurfaces(config, silent, adapter) {
1968
1981
  const rules = config?.rules;
1969
1982
  const skillSev = (0, types_js_1.ruleSeverity)(rules?.["untested-skill"]);
1970
- const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-agent"]);
1983
+ const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-subagent"]);
1971
1984
  const hookSev = (0, types_js_1.ruleSeverity)(rules?.["untested-hook"]);
1972
1985
  if (!skillSev && !agentSev && !hookSev)
1973
1986
  return { untested: 0, errors: 0 };
@@ -1976,11 +1989,12 @@ function checkUntestedSurfaces(config, silent) {
1976
1989
  // whichever of the three rules carries them.
1977
1990
  const opts = {
1978
1991
  ...(0, types_js_1.ruleOptions)(rules?.["untested-skill"]),
1979
- ...(0, types_js_1.ruleOptions)(rules?.["untested-agent"]),
1992
+ ...(0, types_js_1.ruleOptions)(rules?.["untested-subagent"]),
1980
1993
  ...(0, types_js_1.ruleOptions)(rules?.["untested-hook"]),
1981
1994
  };
1982
1995
  const report = (0, test_coverage_js_1.findUntestedSurfaces)({
1983
1996
  basePath: process.cwd(),
1997
+ layout: adapter.layout,
1984
1998
  skills: skillSev !== false,
1985
1999
  agents: agentSev !== false,
1986
2000
  hooks: hookSev !== false,
@@ -2002,47 +2016,58 @@ function checkUntestedSurfaces(config, silent) {
2002
2016
  };
2003
2017
  }
2004
2018
  /**
2005
- * Apply the `agent-tool-contract` rule: cross-reference every subagent's `tools:`
2019
+ * A surface-scoped rule (subagent / shell-hook) is configured, but the active
2020
+ * harness doesn't have that surface. Report it as **n/a** — loud, not silent (the
2021
+ * no-silent-skips ethos): the rule isn't failing and isn't passing, it simply
2022
+ * doesn't apply to this harness. Never counts toward issues/errors.
2023
+ */
2024
+ function reportNotApplicable(check, surface, adapter, silent) {
2025
+ if (silent)
2026
+ return;
2027
+ console.log(`\n${check}:\n`);
2028
+ console.log(` – n/a — ${adapter.name} has no ${surface}`);
2029
+ }
2030
+ /**
2031
+ * Apply the `subagent-tool-contract` rule: cross-reference every subagent's `tools:`
2006
2032
  * rail against the harness tool catalog (the moat — "valid is not true"). Flags
2007
2033
  * only the HIGH-CONFIDENCE issues (a never-available tool, or a close typo) via
2008
2034
  * the shared `confidentToolIssues` detector — the same code `scan` and
2009
2035
  * `compileAgent` use (one-detector-no-drift), so a bare unrecognized tool
2010
2036
  * (plugin/MCP-provided) is never a false alarm. Warning by default; set
2011
- * `agent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
2037
+ * `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
2012
2038
  */
2013
- function checkAgentToolContracts(config, silent) {
2014
- const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["agent-tool-contract"]);
2039
+ function checkSubagentToolContracts(config, silent, adapter) {
2040
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
2015
2041
  if (!sev)
2016
2042
  return { issues: 0, errors: 0 };
2017
- const files = (0, glob_1.globSync)(["agents/*.md", ".claude/agents/*.md"], {
2018
- cwd: process.cwd(),
2019
- ignore: ["**/*.spec.ts"],
2020
- });
2043
+ if (!adapter.capabilities.subagents) {
2044
+ reportNotApplicable("Subagent tool-contract check", "subagents", adapter, silent);
2045
+ return { issues: 0, errors: 0 };
2046
+ }
2047
+ // Reuse the loader's already-resolved, layout+dialect-driven agents (the same
2048
+ // `scan` detector — one-detector-no-drift) instead of re-globbing a hard-coded
2049
+ // `agents/` path, so a harness with a different subagent dir Just Works.
2050
+ let agents;
2051
+ try {
2052
+ agents = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents;
2053
+ }
2054
+ catch {
2055
+ return { issues: 0, errors: 0 };
2056
+ }
2021
2057
  let issues = 0;
2022
2058
  let printedHeader = false;
2023
- for (const rel of files.sort()) {
2024
- let md;
2025
- try {
2026
- md = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), rel), "utf-8");
2027
- }
2028
- catch {
2029
- continue;
2030
- }
2031
- const tools = (0, agent_runtime_js_1.parseAgentTools)(md);
2032
- if (tools === null)
2033
- continue; // no contract → inherits all (a different rule)
2034
- const found = (0, tool_contract_js_1.confidentToolIssues)((0, tool_contract_js_1.verifyToolContract)(tools, dialect_js_1.claudeCodeDialect));
2035
- if (found.length === 0)
2059
+ for (const agent of agents) {
2060
+ if (agent.toolIssues.length === 0)
2036
2061
  continue;
2037
- issues += found.length;
2062
+ issues += agent.toolIssues.length;
2038
2063
  if (!silent) {
2039
2064
  if (!printedHeader) {
2040
- console.log("\nAgent tool-contract check:\n");
2065
+ console.log("\nSubagent tool-contract check:\n");
2041
2066
  printedHeader = true;
2042
2067
  }
2043
- for (const issue of found) {
2044
- console.log(` ${sev === "error" ? "✗" : "⚠"} ${rel}: ${issue.message}`);
2045
- ghAnnotate(sev === "error" ? "error" : "warning", issue.message, rel);
2068
+ for (const issue of agent.toolIssues) {
2069
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${agent.path}: ${issue.message}`);
2070
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message, agent.path);
2046
2071
  }
2047
2072
  }
2048
2073
  }
@@ -2054,13 +2079,17 @@ function checkAgentToolContracts(config, silent) {
2054
2079
  * `hookEventIssues` (the shared detector, high-precision: close typos only, never
2055
2080
  * a framework/custom event). Warning by default; "error" gates CI.
2056
2081
  */
2057
- function checkHookEvents(config, silent) {
2082
+ function checkHookEvents(config, silent, adapter) {
2058
2083
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
2059
2084
  if (!sev)
2060
2085
  return { issues: 0, errors: 0 };
2086
+ if (!adapter.capabilities.shellHooks) {
2087
+ reportNotApplicable("Hook-event check", "shell hooks", adapter, silent);
2088
+ return { issues: 0, errors: 0 };
2089
+ }
2061
2090
  let found;
2062
2091
  try {
2063
- found = (0, scan_js_1.scanPlugin)(process.cwd()).hookEventIssues;
2092
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookEventIssues;
2064
2093
  }
2065
2094
  catch {
2066
2095
  return { issues: 0, errors: 0 };
@@ -2075,19 +2104,23 @@ function checkHookEvents(config, silent) {
2075
2104
  return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2076
2105
  }
2077
2106
  /**
2078
- * Apply the `agent-frontmatter` rule. Two kinds of agent-frontmatter defect, one
2107
+ * Apply the `subagent-frontmatter` rule. Two kinds of subagent-frontmatter defect, one
2079
2108
  * rule: (1) a subagent MISSING a required field (`name`/`description`) — it won't
2080
2109
  * register; (2) a subagent with an INVALID `model:`/`color:` value (a close typo
2081
2110
  * of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
2082
2111
  * `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
2083
2112
  */
2084
- function checkFrontmatterSchema(config, silent) {
2085
- const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["agent-frontmatter"]);
2113
+ function checkFrontmatterSchema(config, silent, adapter) {
2114
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
2086
2115
  if (!sev)
2087
2116
  return { issues: 0, errors: 0 };
2117
+ if (!adapter.capabilities.subagents) {
2118
+ reportNotApplicable("Subagent-frontmatter check", "subagents", adapter, silent);
2119
+ return { issues: 0, errors: 0 };
2120
+ }
2088
2121
  let found;
2089
2122
  try {
2090
- const r = (0, scan_js_1.scanPlugin)(process.cwd());
2123
+ const r = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect);
2091
2124
  found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
2092
2125
  }
2093
2126
  catch {
@@ -2110,13 +2143,13 @@ function checkFrontmatterSchema(config, silent) {
2110
2143
  * default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
2111
2144
  * `skillMetaIssues`.
2112
2145
  */
2113
- function checkSkillFrontmatter(config, silent) {
2146
+ function checkSkillFrontmatter(config, silent, adapter) {
2114
2147
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
2115
2148
  if (!sev)
2116
2149
  return { issues: 0, errors: 0 };
2117
2150
  let found;
2118
2151
  try {
2119
- found = (0, scan_js_1.scanPlugin)(process.cwd()).skillMetaIssues;
2152
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillMetaIssues;
2120
2153
  }
2121
2154
  catch {
2122
2155
  return { issues: 0, errors: 0 };
@@ -2135,13 +2168,13 @@ function checkSkillFrontmatter(config, silent) {
2135
2168
  * (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
2136
2169
  * Warning by default; "error" gates CI.
2137
2170
  */
2138
- function checkMcpConfig(config, silent) {
2171
+ function checkMcpConfig(config, silent, adapter) {
2139
2172
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
2140
2173
  if (!sev)
2141
2174
  return { issues: 0, errors: 0 };
2142
2175
  let found;
2143
2176
  try {
2144
- found = (0, scan_js_1.scanPlugin)(process.cwd()).mcpIssues;
2177
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpIssues;
2145
2178
  }
2146
2179
  catch {
2147
2180
  return { issues: 0, errors: 0 };
@@ -2162,13 +2195,17 @@ function checkMcpConfig(config, silent) {
2162
2195
  * `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
2163
2196
  * "error" gates CI.
2164
2197
  */
2165
- function checkDisallowedTools(config, silent) {
2198
+ function checkDisallowedTools(config, silent, adapter) {
2166
2199
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
2167
2200
  if (!sev)
2168
2201
  return { issues: 0, errors: 0 };
2202
+ if (!adapter.capabilities.subagents) {
2203
+ reportNotApplicable("Disallowed-tools check", "subagents", adapter, silent);
2204
+ return { issues: 0, errors: 0 };
2205
+ }
2169
2206
  let found;
2170
2207
  try {
2171
- found = (0, scan_js_1.scanPlugin)(process.cwd()).agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({ message: i.message, path: a.path })));
2208
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({ message: i.message, path: a.path })));
2172
2209
  }
2173
2210
  catch {
2174
2211
  return { issues: 0, errors: 0 };
@@ -2190,13 +2227,13 @@ function checkDisallowedTools(config, silent) {
2190
2227
  * colon / `<example>` is flagged though it may still load — hence WARN by default
2191
2228
  * (verify before setting "error").
2192
2229
  */
2193
- function checkFrontmatterValid(config, silent) {
2230
+ function checkFrontmatterValid(config, silent, adapter) {
2194
2231
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
2195
2232
  if (!sev)
2196
2233
  return { issues: 0, errors: 0 };
2197
2234
  let found;
2198
2235
  try {
2199
- found = (0, scan_js_1.scanPlugin)(process.cwd()).malformedFrontmatter;
2236
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).malformedFrontmatter;
2200
2237
  }
2201
2238
  catch {
2202
2239
  return { issues: 0, errors: 0 };
@@ -2217,13 +2254,13 @@ function checkFrontmatterValid(config, silent) {
2217
2254
  * `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
2218
2255
  * identical text). Warning by default; "error" gates CI.
2219
2256
  */
2220
- function checkDescriptionOverlap(config, silent) {
2257
+ function checkDescriptionOverlap(config, silent, adapter) {
2221
2258
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
2222
2259
  if (!sev)
2223
2260
  return { issues: 0, errors: 0 };
2224
2261
  let found;
2225
2262
  try {
2226
- found = (0, scan_js_1.scanPlugin)(process.cwd()).descriptionOverlaps;
2263
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).descriptionOverlaps;
2227
2264
  }
2228
2265
  catch {
2229
2266
  return { issues: 0, errors: 0 };
@@ -2244,13 +2281,17 @@ function checkDescriptionOverlap(config, silent) {
2244
2281
  * `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
2245
2282
  * Warning by default; "error" gates CI.
2246
2283
  */
2247
- function checkMcpHookTargets(config, silent) {
2284
+ function checkMcpHookTargets(config, silent, adapter) {
2248
2285
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
2249
2286
  if (!sev)
2250
2287
  return { issues: 0, errors: 0 };
2288
+ if (!adapter.capabilities.shellHooks) {
2289
+ reportNotApplicable("MCP hook-target check", "shell hooks", adapter, silent);
2290
+ return { issues: 0, errors: 0 };
2291
+ }
2251
2292
  let found;
2252
2293
  try {
2253
- found = (0, scan_js_1.scanPlugin)(process.cwd()).mcpHookIssues;
2294
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpHookIssues;
2254
2295
  }
2255
2296
  catch {
2256
2297
  return { issues: 0, errors: 0 };
@@ -2272,13 +2313,17 @@ function checkMcpHookTargets(config, silent) {
2272
2313
  * existence-guarded one-liners, inline commands). Matches Anthropic's own
2273
2314
  * `claude plugin validate`. Warning by default; "error" gates CI.
2274
2315
  */
2275
- function checkHookScriptExists(config, silent) {
2316
+ function checkHookScriptExists(config, silent, adapter) {
2276
2317
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
2277
2318
  if (!sev)
2278
2319
  return { issues: 0, errors: 0 };
2320
+ if (!adapter.capabilities.shellHooks) {
2321
+ reportNotApplicable("Hook-script existence check", "shell hooks", adapter, silent);
2322
+ return { issues: 0, errors: 0 };
2323
+ }
2279
2324
  let missing;
2280
2325
  try {
2281
- missing = (0, scan_js_1.scanPlugin)(process.cwd()).hooks.filter((h) => h.status === "missing");
2326
+ missing = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hooks.filter((h) => h.status === "missing");
2282
2327
  }
2283
2328
  catch {
2284
2329
  return { issues: 0, errors: 0 };
@@ -2303,13 +2348,17 @@ function checkHookScriptExists(config, silent) {
2303
2348
  * — high-precision (gated on a declared set, built-ins allowlisted, the
2304
2349
  * plugin-namespaced form skipped). Warning by default; "error" gates CI.
2305
2350
  */
2306
- function checkMcpToolResolves(config, silent) {
2351
+ function checkMcpToolResolves(config, silent, adapter) {
2307
2352
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
2308
2353
  if (!sev)
2309
2354
  return { issues: 0, errors: 0 };
2355
+ if (!adapter.capabilities.subagents) {
2356
+ reportNotApplicable("MCP tool-resolution check", "subagents", adapter, silent);
2357
+ return { issues: 0, errors: 0 };
2358
+ }
2310
2359
  let found;
2311
2360
  try {
2312
- found = (0, scan_js_1.scanPlugin)(process.cwd()).agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
2361
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
2313
2362
  }
2314
2363
  catch {
2315
2364
  return { issues: 0, errors: 0 };
@@ -2583,6 +2632,129 @@ function handleRunScripts(kind, args, restArgs) {
2583
2632
  process.exit(1);
2584
2633
  }
2585
2634
  }
2635
+ /** Parse the `--harness=<name>` override out of an argv list (the one definition). */
2636
+ function harnessFlagFrom(argv) {
2637
+ return argv
2638
+ .find((a) => a.startsWith("--harness="))
2639
+ ?.slice("--harness=".length);
2640
+ }
2641
+ /**
2642
+ * `vigiles explain <dir> [name]` — the deterministic WHY behind a low score (C4):
2643
+ * scan a plugin and surface the structural CAUSE of a behavioral symptom + the
2644
+ * one-line fix. No model — it reads the same `ScanReport` `scan` computes. An
2645
+ * optional surface name narrows to one underperforming skill/agent (the
2646
+ * optimizer's call). `--json` for the agent-consumable shape, `--harness=` to
2647
+ * override detection.
2648
+ */
2649
+ function handleExplain(restArgs, args) {
2650
+ const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
2651
+ const surface = restArgs[1];
2652
+ const json = args.includes("--json");
2653
+ const harnessFlag = harnessFlagFrom(args);
2654
+ const adapter = harnessFlag
2655
+ ? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
2656
+ : (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
2657
+ const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
2658
+ const exps = surface ? (0, score_explainer_js_1.explainSurface)(report, surface) : (0, score_explainer_js_1.explainScore)(report);
2659
+ if (json) {
2660
+ console.log(JSON.stringify(exps, null, 2));
2661
+ return;
2662
+ }
2663
+ if (surface)
2664
+ console.log(`Explaining "${surface}":\n`);
2665
+ console.log((0, score_explainer_js_1.formatExplanations)(exps));
2666
+ }
2667
+ /**
2668
+ * The plugin's declared name for the namespaced skill id, read from the layout's
2669
+ * manifest (adapter-aware path, not a hardcoded `.claude-plugin/`), falling back to
2670
+ * the dir basename. JSON manifests only for now (a TOML/Codex manifest → basename).
2671
+ */
2672
+ function pluginNameFor(dir, manifestPath) {
2673
+ try {
2674
+ const manifest = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, manifestPath), "utf-8"));
2675
+ if (typeof manifest.name === "string" && manifest.name)
2676
+ return manifest.name;
2677
+ }
2678
+ catch {
2679
+ /* missing / non-JSON manifest → fall back */
2680
+ }
2681
+ return (0, node_path_1.basename)(dir);
2682
+ }
2683
+ /** Enrich an untested Surface with the metadata the right template needs. */
2684
+ function scaffoldInputFor(s, report, pluginName) {
2685
+ const base = { kind: s.kind, name: s.name, path: s.path };
2686
+ switch (s.kind) {
2687
+ case "skill": {
2688
+ const sk = report.skills.find((x) => x.name === s.name);
2689
+ return { ...base, pluginName, userInvoked: sk?.userInvoked };
2690
+ }
2691
+ case "agent": {
2692
+ const ag = report.agents.find((x) => x.name === s.name);
2693
+ return { ...base, tools: ag?.tools ?? null };
2694
+ }
2695
+ case "hook":
2696
+ return { ...base, hookCommand: `bash ${s.path}` };
2697
+ }
2698
+ }
2699
+ /**
2700
+ * `vigiles scaffold-test [dir]` — generate a runnable STARTER test for each
2701
+ * untested skill/agent/hook (B1, test-gen from free-form). Reuses the
2702
+ * untested-surface detector for the list + `scan` for the metadata, then emits the
2703
+ * cheapest meaningful tier per kind (hook → `runHook`, skill → `measureTriggerRate`,
2704
+ * subagent → `runHarnessTest`) at the surface's suggested test path. Dry-run by
2705
+ * default (prints the scaffolds); `--write` creates the files (never clobbering an
2706
+ * existing one); `--json` for the agent-consumable `{ path, content }[]`.
2707
+ */
2708
+ function handleScaffoldTest(restArgs, args) {
2709
+ const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
2710
+ const write = args.includes("--write");
2711
+ const json = args.includes("--json");
2712
+ const harnessFlag = harnessFlagFrom(args);
2713
+ const adapter = harnessFlag
2714
+ ? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
2715
+ : (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
2716
+ const { untested } = (0, test_coverage_js_1.findUntestedSurfaces)({
2717
+ basePath: dir,
2718
+ layout: adapter.layout,
2719
+ });
2720
+ const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
2721
+ const pluginName = pluginNameFor(dir, adapter.layout.manifestPath);
2722
+ const scaffolds = untested.map((s) => (0, scaffold_test_js_1.scaffoldTest)(scaffoldInputFor(s, report, pluginName)));
2723
+ if (json) {
2724
+ console.log(JSON.stringify(scaffolds, null, 2));
2725
+ return;
2726
+ }
2727
+ if (!write) {
2728
+ console.log((0, scaffold_test_js_1.formatScaffolds)(scaffolds));
2729
+ for (const s of scaffolds) {
2730
+ console.log(`\n# ${s.path}\n`);
2731
+ console.log(s.content);
2732
+ }
2733
+ if (scaffolds.length > 0) {
2734
+ console.log("Re-run with --write to create these files.");
2735
+ }
2736
+ return;
2737
+ }
2738
+ const written = [];
2739
+ const skipped = [];
2740
+ for (const s of scaffolds) {
2741
+ const target = (0, node_path_1.resolve)(dir, s.path);
2742
+ if ((0, node_fs_1.existsSync)(target)) {
2743
+ skipped.push(s.path);
2744
+ continue;
2745
+ }
2746
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
2747
+ (0, node_fs_1.writeFileSync)(target, s.content);
2748
+ written.push(s.path);
2749
+ }
2750
+ for (const p of written)
2751
+ console.log(`✓ wrote ${p}`);
2752
+ for (const p of skipped)
2753
+ console.log(`⊘ skipped ${p} (already exists)`);
2754
+ if (written.length === 0 && skipped.length === 0) {
2755
+ console.log("Nothing to scaffold — every surface already has a test.");
2756
+ }
2757
+ }
2586
2758
  function printUsage(command) {
2587
2759
  console.log("vigiles — compile typed specs to instruction files");
2588
2760
  console.log("");
@@ -2592,6 +2764,8 @@ function printUsage(command) {
2592
2764
  console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
2593
2765
  console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
2594
2766
  console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
2767
+ console.log(" vigiles explain <dir> [name] Deterministic WHY a skill/agent underperforms + the fix (--json, --harness=)");
2768
+ console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
2595
2769
  console.log("");
2596
2770
  console.log("Examples:");
2597
2771
  console.log(" vigiles init Auto-detect project, create specs, wire CI");
@@ -2701,6 +2875,42 @@ function skillStartCommand(target) {
2701
2875
  (0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
2702
2876
  console.log(`Active skill: ${target}`);
2703
2877
  }
2878
+ /**
2879
+ * PreToolUse-hook entrypoint: enforce the active skill's declared purity floor.
2880
+ * Reads the tool event on stdin, parses the `vigiles:purity:` marker from the
2881
+ * active skill's compiled SKILL.md, and blocks (exit 2 + reason on stderr) any
2882
+ * tool call that violates the declared floor — refining `Bash` by the live
2883
+ * command via `isReadOnlyBash`. Skills have no tools-allowlist rail; this gate
2884
+ * is purity-only. Mirrors `agentHookCommand` for skills.
2885
+ */
2886
+ function skillToolHookCommand() {
2887
+ let raw = "";
2888
+ try {
2889
+ raw = (0, node_fs_1.readFileSync)(0, "utf-8");
2890
+ }
2891
+ catch {
2892
+ /* no stdin */
2893
+ }
2894
+ let tool = "";
2895
+ let command;
2896
+ try {
2897
+ const parsed = JSON.parse(raw);
2898
+ tool = parsed.tool_name ?? "";
2899
+ if (typeof parsed.tool_input?.command === "string") {
2900
+ command = parsed.tool_input.command;
2901
+ }
2902
+ }
2903
+ catch {
2904
+ /* malformed input → no tool, allow */
2905
+ }
2906
+ if (!tool)
2907
+ return;
2908
+ const decision = (0, skill_runtime_js_1.evaluateSkillPreToolUse)(process.cwd(), tool, command);
2909
+ if (!decision.allow) {
2910
+ console.error(decision.message);
2911
+ process.exit(2);
2912
+ }
2913
+ }
2704
2914
  /**
2705
2915
  * PreToolUse-hook entrypoint: enforce the active subagent's allowed-tools
2706
2916
  * contract. Reads the tool event on stdin, parses the active agent's compiled
@@ -2716,15 +2926,20 @@ function agentHookCommand() {
2716
2926
  /* no stdin */
2717
2927
  }
2718
2928
  let tool = "";
2929
+ let command;
2719
2930
  try {
2720
- tool = JSON.parse(raw).tool_name ?? "";
2931
+ const parsed = JSON.parse(raw);
2932
+ tool = parsed.tool_name ?? "";
2933
+ if (typeof parsed.tool_input?.command === "string") {
2934
+ command = parsed.tool_input.command;
2935
+ }
2721
2936
  }
2722
2937
  catch {
2723
2938
  /* malformed input → no tool, allow */
2724
2939
  }
2725
2940
  if (!tool)
2726
2941
  return;
2727
- const decision = (0, agent_runtime_js_2.evaluatePreToolUse)(process.cwd(), tool);
2942
+ const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(process.cwd(), tool, command);
2728
2943
  if (!decision.allow) {
2729
2944
  console.error(decision.message);
2730
2945
  process.exit(2);
@@ -2760,7 +2975,7 @@ function agentStartCommand(target) {
2760
2975
  console.error("Usage: vigiles agent-start <agents/<name>.md>");
2761
2976
  process.exit(2);
2762
2977
  }
2763
- (0, agent_runtime_js_2.setActiveAgent)(process.cwd(), target);
2978
+ (0, agent_runtime_js_1.setActiveAgent)(process.cwd(), target);
2764
2979
  console.log(`Active agent: ${target}`);
2765
2980
  }
2766
2981
  /** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
@@ -2778,11 +2993,14 @@ function handleSkillCommand(command, restArgs) {
2778
2993
  case "skill-hook":
2779
2994
  skillHookCommand();
2780
2995
  return true;
2996
+ case "skill-tool-hook":
2997
+ skillToolHookCommand();
2998
+ return true;
2781
2999
  case "agent-start":
2782
3000
  agentStartCommand(restArgs[0]);
2783
3001
  return true;
2784
3002
  case "agent-done":
2785
- (0, agent_runtime_js_2.clearActiveAgent)(process.cwd());
3003
+ (0, agent_runtime_js_1.clearActiveAgent)(process.cwd());
2786
3004
  return true;
2787
3005
  case "agent-hook":
2788
3006
  agentHookCommand();
@@ -2799,6 +3017,13 @@ function handleSkillCommand(command, restArgs) {
2799
3017
  case "refs-hook":
2800
3018
  refsHookCommand();
2801
3019
  return true;
3020
+ case "effect-enter":
3021
+ (0, effect_region_js_1.setEffectActive)(process.cwd());
3022
+ console.log("Effect boundary entered.");
3023
+ return true;
3024
+ case "effect-exit":
3025
+ (0, effect_region_js_1.clearEffectActive)(process.cwd());
3026
+ return true;
2802
3027
  default:
2803
3028
  return false;
2804
3029
  }
@@ -2969,9 +3194,7 @@ async function main() {
2969
3194
  console.log("Run `vigiles init` to create one.");
2970
3195
  process.exit(0);
2971
3196
  }
2972
- const harnessFlag = args
2973
- .find((a) => a.startsWith("--harness="))
2974
- ?.slice("--harness=".length);
3197
+ const harnessFlag = harnessFlagFrom(args);
2975
3198
  const valid = await compile(specs, config, { harnessFlag });
2976
3199
  console.log("");
2977
3200
  if (valid) {
@@ -3033,9 +3256,7 @@ async function main() {
3033
3256
  }
3034
3257
  else {
3035
3258
  const root = (0, node_path_1.resolve)(targets[0]);
3036
- const harnessFlag = args
3037
- .find((a) => a.startsWith("--harness="))
3038
- ?.slice("--harness=".length);
3259
+ const harnessFlag = harnessFlagFrom(args);
3039
3260
  const det = (0, adapter_registry_js_1.detectAdapterResult)(root);
3040
3261
  const adapter = harnessFlag
3041
3262
  ? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
@@ -3048,7 +3269,15 @@ async function main() {
3048
3269
  }
3049
3270
  console.log("");
3050
3271
  }
3051
- console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
3272
+ if (args.includes("--fix-plan")) {
3273
+ // The deterministic optimization lens on the SAME report: health score
3274
+ // + the ranked free fixes to clear before measuring (the A2 spine).
3275
+ const plan = (0, optimize_js_1.optimize)(report);
3276
+ console.log(json ? JSON.stringify(plan, null, 2) : (0, optimize_js_1.formatOptimize)(plan));
3277
+ }
3278
+ else {
3279
+ console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
3280
+ }
3052
3281
  if (wantTrigger) {
3053
3282
  const harness = adapter.name === "codex" ? "codex" : "claude-code";
3054
3283
  await handleScanTrigger(root, args, json, harness);
@@ -3056,6 +3285,12 @@ async function main() {
3056
3285
  }
3057
3286
  break;
3058
3287
  }
3288
+ case "explain":
3289
+ handleExplain(restArgs, args);
3290
+ break;
3291
+ case "scaffold-test":
3292
+ handleScaffoldTest(restArgs, args);
3293
+ break;
3059
3294
  // --- Plumbing ---
3060
3295
  case "generate-types":
3061
3296
  handleGenerateTypes(args, restArgs);