vigiles 5.1.0 → 6.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.
Files changed (57) hide show
  1. package/README.md +59 -18
  2. package/dist/adapters/claude-code/adapter.js +1 -0
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +45 -6
  4. package/dist/adapters/claude-code/agent-runtime.js +94 -8
  5. package/dist/adapters/claude-code/dialect.d.ts +34 -0
  6. package/dist/adapters/claude-code/dialect.js +51 -19
  7. package/dist/adapters/claude-code/effect-region.d.ts +9 -0
  8. package/dist/adapters/claude-code/effect-region.js +45 -0
  9. package/dist/adapters/claude-code/layout.js +3 -0
  10. package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
  11. package/dist/adapters/claude-code/skill-runtime.js +40 -0
  12. package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
  13. package/dist/adapters/claude-code/typed-spec.js +55 -0
  14. package/dist/adapters/codex/adapter.js +3 -0
  15. package/dist/adapters/codex/layout.js +3 -0
  16. package/dist/adapters/opencode/adapter.js +1 -0
  17. package/dist/adapters/opencode/layout.js +3 -0
  18. package/dist/check.d.ts +8 -0
  19. package/dist/check.js +27 -3
  20. package/dist/claude-code.d.ts +1 -0
  21. package/dist/claude-code.js +8 -1
  22. package/dist/cli.js +469 -88
  23. package/dist/core/adapter.d.ts +10 -0
  24. package/dist/core/bash-effects.d.ts +41 -0
  25. package/dist/core/bash-effects.js +405 -0
  26. package/dist/core/compile.d.ts +3 -1
  27. package/dist/core/compile.js +176 -39
  28. package/dist/core/dialect.d.ts +10 -0
  29. package/dist/core/effects.d.ts +172 -0
  30. package/dist/core/effects.js +245 -0
  31. package/dist/core/generate-harness.d.ts +187 -0
  32. package/dist/core/generate-harness.js +337 -0
  33. package/dist/core/layout.d.ts +6 -0
  34. package/dist/core/mcp-tool.d.ts +1 -1
  35. package/dist/core/orphans.js +21 -0
  36. package/dist/core/spec.d.ts +432 -11
  37. package/dist/core/spec.js +166 -3
  38. package/dist/core/tool-contract.d.ts +1 -1
  39. package/dist/core/types.d.ts +6 -6
  40. package/dist/core/validate.js +4 -4
  41. package/dist/harness-test.d.ts +7 -0
  42. package/dist/harness-test.js +19 -7
  43. package/dist/leaderboard.d.ts +2 -0
  44. package/dist/leaderboard.js +2 -0
  45. package/dist/optimize.d.ts +74 -0
  46. package/dist/optimize.js +94 -0
  47. package/dist/scaffold-test.d.ts +58 -0
  48. package/dist/scaffold-test.js +263 -0
  49. package/dist/scan.d.ts +40 -0
  50. package/dist/scan.js +91 -43
  51. package/dist/score-explainer.d.ts +69 -0
  52. package/dist/score-explainer.js +169 -0
  53. package/dist/test-coverage.d.ts +7 -0
  54. package/dist/test-coverage.js +39 -24
  55. package/package.json +2 -1
  56. package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -4
  57. package/skills/edit-spec/SKILL.md +1 -1
package/dist/cli.js CHANGED
@@ -14,19 +14,21 @@ const node_fs_1 = require("node:fs");
14
14
  const node_path_1 = require("node:path");
15
15
  const glob_1 = require("glob");
16
16
  const generate_types_js_1 = require("./core/generate-types.js");
17
+ const generate_harness_js_1 = require("./core/generate-harness.js");
17
18
  const validate_js_1 = require("./core/validate.js");
18
19
  const cli_flags_js_1 = require("./cli-flags.js");
19
20
  const setup_plan_js_1 = require("./setup-plan.js");
20
21
  const types_js_1 = require("./core/types.js");
21
22
  const test_coverage_js_1 = require("./test-coverage.js");
23
+ const scaffold_test_js_1 = require("./scaffold-test.js");
24
+ const effects_js_1 = require("./core/effects.js");
22
25
  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");
26
+ const score_explainer_js_1 = require("./score-explainer.js");
26
27
  const scan_behavioral_js_1 = require("./scan-behavioral.js");
27
28
  const adapter_registry_js_1 = require("./adapter-registry.js");
28
29
  const skill_harness_js_1 = require("./skill-harness.js");
29
30
  const leaderboard_js_1 = require("./leaderboard.js");
31
+ const optimize_js_1 = require("./optimize.js");
30
32
  const compile_js_1 = require("./core/compile.js");
31
33
  const proofs_js_1 = require("./core/proofs.js");
32
34
  const inline_js_1 = require("./core/inline.js");
@@ -35,7 +37,8 @@ const generate_schema_js_1 = require("./core/generate-schema.js");
35
37
  const compose_js_1 = require("./core/compose.js");
36
38
  const compile_generator_js_1 = require("./core/compile-generator.js");
37
39
  const action_gate_js_1 = require("./action-gate.js");
38
- const agent_runtime_js_2 = require("./adapters/claude-code/agent-runtime.js");
40
+ const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
41
+ const effect_region_js_1 = require("./adapters/claude-code/effect-region.js");
39
42
  const tool_intercept_js_1 = require("./tool-intercept.js");
40
43
  const refs_js_1 = require("./core/refs.js");
41
44
  const mcp_js_1 = require("./core/mcp.js");
@@ -869,6 +872,17 @@ async function runLint(restArgs, flags, config) {
869
872
  const json = flags.includes("--json");
870
873
  const silent = summary || json;
871
874
  const files = findInstructionFiles(restArgs);
875
+ // Resolve the active harness ONCE so the harness-specific checks below run
876
+ // against the right adapter's dialect (tool/event catalogs) and surfaces —
877
+ // not a hard-coded Claude Code default. A subagent-surface rule reports n/a
878
+ // on a harness without subagents (Codex) rather than scanning nothing.
879
+ const harnessFlag = harnessFlagFrom(flags);
880
+ const lintSelection = (0, adapter_registry_js_1.resolveHarnessSelection)({
881
+ root: process.cwd(),
882
+ flag: harnessFlag,
883
+ configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
884
+ });
885
+ const adapter = lintSelection.adapter;
872
886
  // 1. Verify hashes and structure
873
887
  if (!silent) {
874
888
  if (files.length > 0) {
@@ -927,40 +941,41 @@ async function runLint(restArgs, flags, config) {
927
941
  // 7b. Untested-surface check — skills/agents/hooks shipping without a test or
928
942
  // eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
929
943
  // 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);
944
+ const untested = checkUntestedSurfaces(config, silent, adapter);
945
+ // 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
946
+ // rail against the harness catalog (the moat). n/a on a harness with no
947
+ // subagents. Off by default unless a severity is configured; warning surfaces
948
+ // a typo/never-available tool, error gates CI.
949
+ const toolContract = checkSubagentToolContracts(config, silent, adapter);
935
950
  // 7d. Hook-event check — a hook registered under an event the harness doesn't
936
951
  // 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);
952
+ const hookEvents = checkHookEvents(config, silent, adapter);
953
+ // 7e. Subagent-frontmatter check — a subagent missing required frontmatter
954
+ // (name + description) won't register. n/a on a harness with no subagents.
955
+ const frontmatter = checkFrontmatterSchema(config, silent, adapter);
941
956
  // 7f. MCP-config check — a declared MCP server with no command/url can't start.
942
- const mcpConfig = checkMcpConfig(config, silent);
957
+ const mcpConfig = checkMcpConfig(config, silent, adapter);
943
958
  // 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
944
959
  // reliable trigger surface). Best-practice nudge; skills load without it.
945
- const skillFm = checkSkillFrontmatter(config, silent);
960
+ const skillFm = checkSkillFrontmatter(config, silent, adapter);
946
961
  // 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
947
962
  // the plugin doesn't declare can't resolve (the MCP half of the tool moat).
948
- const mcpToolResolves = checkMcpToolResolves(config, silent);
963
+ const mcpToolResolves = checkMcpToolResolves(config, silent, adapter);
949
964
  // 7i. Hook-script existence — a hook command referencing a missing script file
950
965
  // never runs (matches Anthropic's own `claude plugin validate`).
951
- const hookScripts = checkHookScriptExists(config, silent);
966
+ const hookScripts = checkHookScriptExists(config, silent, adapter);
952
967
  // 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);
968
+ // (the deny-side mirror of subagent-tool-contract; close-typo only).
969
+ const disallowedTools = checkDisallowedTools(config, silent, adapter);
955
970
  // 7k. Description-overlap — two model-invocable skills with near-identical
956
971
  // descriptions collide in the selector (deterministic NCD precision proxy).
957
- const descriptionOverlap = checkDescriptionOverlap(config, silent);
972
+ const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter);
958
973
  // 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
959
974
  // stricter than some loaders, so verify before enforcing).
960
- const frontmatterValid = checkFrontmatterValid(config, silent);
975
+ const frontmatterValid = checkFrontmatterValid(config, silent, adapter);
961
976
  // 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
962
977
  // targets an undeclared server (the moat applied to the hook surface).
963
- const mcpHookTargets = checkMcpHookTargets(config, silent);
978
+ const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
964
979
  // 8. Validate vigiles builder calls inside markdown code blocks. Default
965
980
  // is to validate every ref; illustrative blocks opt out via
966
981
  // `<!-- vigiles:ignore -->` (single block) or
@@ -1594,7 +1609,7 @@ function redirectSyncToolTargets(cwd, targets) {
1594
1609
  }
1595
1610
  /** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
1596
1611
  * 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. */
1612
+ * OVER a hand-written file — that is left to the adopt-spec skill. */
1598
1613
  async function setupPillar1(detected, targetValue, harnesses) {
1599
1614
  const cwd = process.cwd();
1600
1615
  const written = [];
@@ -1619,7 +1634,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
1619
1634
  }
1620
1635
  if (targetExists && !targetHasHash((0, node_path_1.resolve)(cwd, target))) {
1621
1636
  needsMigration.push(target);
1622
- console.log(` ${target} already has content — port it into the spec with the migrate-to-spec skill, then \`vigiles compile\`.`);
1637
+ console.log(` ${target} already has content — adopt it into a spec with the adopt-spec skill, then \`vigiles compile\`.`);
1623
1638
  }
1624
1639
  }
1625
1640
  // Generate types + schema.
@@ -1811,7 +1826,7 @@ function printSetupSummary(opts) {
1811
1826
  console.log("\n---\nSetup complete.\n");
1812
1827
  const nextSteps = [];
1813
1828
  if (needsMigration.length > 0) {
1814
- nextSteps.push(`Port ${needsMigration.join(", ")} into its spec with the migrate-to-spec skill, then \`npx vigiles compile\``);
1829
+ nextSteps.push(`Adopt ${needsMigration.join(", ")} into a spec with the adopt-spec skill, then \`npx vigiles compile\``);
1815
1830
  }
1816
1831
  else if (specPathsList.length > 0) {
1817
1832
  nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
@@ -1958,16 +1973,16 @@ function checkIntegrityForFiles(files, severity, silent) {
1958
1973
  return severity === "error" ? errorCount : 0;
1959
1974
  }
1960
1975
  /**
1961
- * Apply the per-kind `untested-skill` / `untested-agent` / `untested-hook` rules:
1976
+ * Apply the per-kind `untested-skill` / `untested-subagent` / `untested-hook` rules:
1962
1977
  * find skills/agents/hooks with no test or eval (see src/test-coverage.ts). Each
1963
1978
  * kind is gated by its OWN rule severity — a kind set to `false` is not scanned;
1964
1979
  * "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
1965
1980
  * untested count plus the severity-gated error count.
1966
1981
  */
1967
- function checkUntestedSurfaces(config, silent) {
1982
+ function checkUntestedSurfaces(config, silent, adapter) {
1968
1983
  const rules = config?.rules;
1969
1984
  const skillSev = (0, types_js_1.ruleSeverity)(rules?.["untested-skill"]);
1970
- const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-agent"]);
1985
+ const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-subagent"]);
1971
1986
  const hookSev = (0, types_js_1.ruleSeverity)(rules?.["untested-hook"]);
1972
1987
  if (!skillSev && !agentSev && !hookSev)
1973
1988
  return { untested: 0, errors: 0 };
@@ -1976,11 +1991,12 @@ function checkUntestedSurfaces(config, silent) {
1976
1991
  // whichever of the three rules carries them.
1977
1992
  const opts = {
1978
1993
  ...(0, types_js_1.ruleOptions)(rules?.["untested-skill"]),
1979
- ...(0, types_js_1.ruleOptions)(rules?.["untested-agent"]),
1994
+ ...(0, types_js_1.ruleOptions)(rules?.["untested-subagent"]),
1980
1995
  ...(0, types_js_1.ruleOptions)(rules?.["untested-hook"]),
1981
1996
  };
1982
1997
  const report = (0, test_coverage_js_1.findUntestedSurfaces)({
1983
1998
  basePath: process.cwd(),
1999
+ layout: adapter.layout,
1984
2000
  skills: skillSev !== false,
1985
2001
  agents: agentSev !== false,
1986
2002
  hooks: hookSev !== false,
@@ -2002,47 +2018,58 @@ function checkUntestedSurfaces(config, silent) {
2002
2018
  };
2003
2019
  }
2004
2020
  /**
2005
- * Apply the `agent-tool-contract` rule: cross-reference every subagent's `tools:`
2021
+ * A surface-scoped rule (subagent / shell-hook) is configured, but the active
2022
+ * harness doesn't have that surface. Report it as **n/a** — loud, not silent (the
2023
+ * no-silent-skips ethos): the rule isn't failing and isn't passing, it simply
2024
+ * doesn't apply to this harness. Never counts toward issues/errors.
2025
+ */
2026
+ function reportNotApplicable(check, surface, adapter, silent) {
2027
+ if (silent)
2028
+ return;
2029
+ console.log(`\n${check}:\n`);
2030
+ console.log(` – n/a — ${adapter.name} has no ${surface}`);
2031
+ }
2032
+ /**
2033
+ * Apply the `subagent-tool-contract` rule: cross-reference every subagent's `tools:`
2006
2034
  * rail against the harness tool catalog (the moat — "valid is not true"). Flags
2007
2035
  * only the HIGH-CONFIDENCE issues (a never-available tool, or a close typo) via
2008
2036
  * the shared `confidentToolIssues` detector — the same code `scan` and
2009
2037
  * `compileAgent` use (one-detector-no-drift), so a bare unrecognized tool
2010
2038
  * (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.
2039
+ * `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
2012
2040
  */
2013
- function checkAgentToolContracts(config, silent) {
2014
- const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["agent-tool-contract"]);
2041
+ function checkSubagentToolContracts(config, silent, adapter) {
2042
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
2015
2043
  if (!sev)
2016
2044
  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
- });
2045
+ if (!adapter.capabilities.subagents) {
2046
+ reportNotApplicable("Subagent tool-contract check", "subagents", adapter, silent);
2047
+ return { issues: 0, errors: 0 };
2048
+ }
2049
+ // Reuse the loader's already-resolved, layout+dialect-driven agents (the same
2050
+ // `scan` detector — one-detector-no-drift) instead of re-globbing a hard-coded
2051
+ // `agents/` path, so a harness with a different subagent dir Just Works.
2052
+ let agents;
2053
+ try {
2054
+ agents = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents;
2055
+ }
2056
+ catch {
2057
+ return { issues: 0, errors: 0 };
2058
+ }
2021
2059
  let issues = 0;
2022
2060
  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 {
2061
+ for (const agent of agents) {
2062
+ if (agent.toolIssues.length === 0)
2029
2063
  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)
2036
- continue;
2037
- issues += found.length;
2064
+ issues += agent.toolIssues.length;
2038
2065
  if (!silent) {
2039
2066
  if (!printedHeader) {
2040
- console.log("\nAgent tool-contract check:\n");
2067
+ console.log("\nSubagent tool-contract check:\n");
2041
2068
  printedHeader = true;
2042
2069
  }
2043
- for (const issue of found) {
2044
- console.log(` ${sev === "error" ? "✗" : "⚠"} ${rel}: ${issue.message}`);
2045
- ghAnnotate(sev === "error" ? "error" : "warning", issue.message, rel);
2070
+ for (const issue of agent.toolIssues) {
2071
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${agent.path}: ${issue.message}`);
2072
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message, agent.path);
2046
2073
  }
2047
2074
  }
2048
2075
  }
@@ -2054,13 +2081,17 @@ function checkAgentToolContracts(config, silent) {
2054
2081
  * `hookEventIssues` (the shared detector, high-precision: close typos only, never
2055
2082
  * a framework/custom event). Warning by default; "error" gates CI.
2056
2083
  */
2057
- function checkHookEvents(config, silent) {
2084
+ function checkHookEvents(config, silent, adapter) {
2058
2085
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
2059
2086
  if (!sev)
2060
2087
  return { issues: 0, errors: 0 };
2088
+ if (!adapter.capabilities.shellHooks) {
2089
+ reportNotApplicable("Hook-event check", "shell hooks", adapter, silent);
2090
+ return { issues: 0, errors: 0 };
2091
+ }
2061
2092
  let found;
2062
2093
  try {
2063
- found = (0, scan_js_1.scanPlugin)(process.cwd()).hookEventIssues;
2094
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookEventIssues;
2064
2095
  }
2065
2096
  catch {
2066
2097
  return { issues: 0, errors: 0 };
@@ -2075,19 +2106,23 @@ function checkHookEvents(config, silent) {
2075
2106
  return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2076
2107
  }
2077
2108
  /**
2078
- * Apply the `agent-frontmatter` rule. Two kinds of agent-frontmatter defect, one
2109
+ * Apply the `subagent-frontmatter` rule. Two kinds of subagent-frontmatter defect, one
2079
2110
  * rule: (1) a subagent MISSING a required field (`name`/`description`) — it won't
2080
2111
  * register; (2) a subagent with an INVALID `model:`/`color:` value (a close typo
2081
2112
  * of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
2082
2113
  * `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
2083
2114
  */
2084
- function checkFrontmatterSchema(config, silent) {
2085
- const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["agent-frontmatter"]);
2115
+ function checkFrontmatterSchema(config, silent, adapter) {
2116
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
2086
2117
  if (!sev)
2087
2118
  return { issues: 0, errors: 0 };
2119
+ if (!adapter.capabilities.subagents) {
2120
+ reportNotApplicable("Subagent-frontmatter check", "subagents", adapter, silent);
2121
+ return { issues: 0, errors: 0 };
2122
+ }
2088
2123
  let found;
2089
2124
  try {
2090
- const r = (0, scan_js_1.scanPlugin)(process.cwd());
2125
+ const r = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect);
2091
2126
  found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
2092
2127
  }
2093
2128
  catch {
@@ -2110,13 +2145,13 @@ function checkFrontmatterSchema(config, silent) {
2110
2145
  * default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
2111
2146
  * `skillMetaIssues`.
2112
2147
  */
2113
- function checkSkillFrontmatter(config, silent) {
2148
+ function checkSkillFrontmatter(config, silent, adapter) {
2114
2149
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
2115
2150
  if (!sev)
2116
2151
  return { issues: 0, errors: 0 };
2117
2152
  let found;
2118
2153
  try {
2119
- found = (0, scan_js_1.scanPlugin)(process.cwd()).skillMetaIssues;
2154
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillMetaIssues;
2120
2155
  }
2121
2156
  catch {
2122
2157
  return { issues: 0, errors: 0 };
@@ -2135,13 +2170,13 @@ function checkSkillFrontmatter(config, silent) {
2135
2170
  * (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
2136
2171
  * Warning by default; "error" gates CI.
2137
2172
  */
2138
- function checkMcpConfig(config, silent) {
2173
+ function checkMcpConfig(config, silent, adapter) {
2139
2174
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
2140
2175
  if (!sev)
2141
2176
  return { issues: 0, errors: 0 };
2142
2177
  let found;
2143
2178
  try {
2144
- found = (0, scan_js_1.scanPlugin)(process.cwd()).mcpIssues;
2179
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpIssues;
2145
2180
  }
2146
2181
  catch {
2147
2182
  return { issues: 0, errors: 0 };
@@ -2162,13 +2197,17 @@ function checkMcpConfig(config, silent) {
2162
2197
  * `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
2163
2198
  * "error" gates CI.
2164
2199
  */
2165
- function checkDisallowedTools(config, silent) {
2200
+ function checkDisallowedTools(config, silent, adapter) {
2166
2201
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
2167
2202
  if (!sev)
2168
2203
  return { issues: 0, errors: 0 };
2204
+ if (!adapter.capabilities.subagents) {
2205
+ reportNotApplicable("Disallowed-tools check", "subagents", adapter, silent);
2206
+ return { issues: 0, errors: 0 };
2207
+ }
2169
2208
  let found;
2170
2209
  try {
2171
- found = (0, scan_js_1.scanPlugin)(process.cwd()).agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({ message: i.message, path: a.path })));
2210
+ 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
2211
  }
2173
2212
  catch {
2174
2213
  return { issues: 0, errors: 0 };
@@ -2190,13 +2229,13 @@ function checkDisallowedTools(config, silent) {
2190
2229
  * colon / `<example>` is flagged though it may still load — hence WARN by default
2191
2230
  * (verify before setting "error").
2192
2231
  */
2193
- function checkFrontmatterValid(config, silent) {
2232
+ function checkFrontmatterValid(config, silent, adapter) {
2194
2233
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
2195
2234
  if (!sev)
2196
2235
  return { issues: 0, errors: 0 };
2197
2236
  let found;
2198
2237
  try {
2199
- found = (0, scan_js_1.scanPlugin)(process.cwd()).malformedFrontmatter;
2238
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).malformedFrontmatter;
2200
2239
  }
2201
2240
  catch {
2202
2241
  return { issues: 0, errors: 0 };
@@ -2217,13 +2256,13 @@ function checkFrontmatterValid(config, silent) {
2217
2256
  * `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
2218
2257
  * identical text). Warning by default; "error" gates CI.
2219
2258
  */
2220
- function checkDescriptionOverlap(config, silent) {
2259
+ function checkDescriptionOverlap(config, silent, adapter) {
2221
2260
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
2222
2261
  if (!sev)
2223
2262
  return { issues: 0, errors: 0 };
2224
2263
  let found;
2225
2264
  try {
2226
- found = (0, scan_js_1.scanPlugin)(process.cwd()).descriptionOverlaps;
2265
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).descriptionOverlaps;
2227
2266
  }
2228
2267
  catch {
2229
2268
  return { issues: 0, errors: 0 };
@@ -2244,13 +2283,17 @@ function checkDescriptionOverlap(config, silent) {
2244
2283
  * `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
2245
2284
  * Warning by default; "error" gates CI.
2246
2285
  */
2247
- function checkMcpHookTargets(config, silent) {
2286
+ function checkMcpHookTargets(config, silent, adapter) {
2248
2287
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
2249
2288
  if (!sev)
2250
2289
  return { issues: 0, errors: 0 };
2290
+ if (!adapter.capabilities.shellHooks) {
2291
+ reportNotApplicable("MCP hook-target check", "shell hooks", adapter, silent);
2292
+ return { issues: 0, errors: 0 };
2293
+ }
2251
2294
  let found;
2252
2295
  try {
2253
- found = (0, scan_js_1.scanPlugin)(process.cwd()).mcpHookIssues;
2296
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpHookIssues;
2254
2297
  }
2255
2298
  catch {
2256
2299
  return { issues: 0, errors: 0 };
@@ -2272,13 +2315,17 @@ function checkMcpHookTargets(config, silent) {
2272
2315
  * existence-guarded one-liners, inline commands). Matches Anthropic's own
2273
2316
  * `claude plugin validate`. Warning by default; "error" gates CI.
2274
2317
  */
2275
- function checkHookScriptExists(config, silent) {
2318
+ function checkHookScriptExists(config, silent, adapter) {
2276
2319
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
2277
2320
  if (!sev)
2278
2321
  return { issues: 0, errors: 0 };
2322
+ if (!adapter.capabilities.shellHooks) {
2323
+ reportNotApplicable("Hook-script existence check", "shell hooks", adapter, silent);
2324
+ return { issues: 0, errors: 0 };
2325
+ }
2279
2326
  let missing;
2280
2327
  try {
2281
- missing = (0, scan_js_1.scanPlugin)(process.cwd()).hooks.filter((h) => h.status === "missing");
2328
+ missing = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hooks.filter((h) => h.status === "missing");
2282
2329
  }
2283
2330
  catch {
2284
2331
  return { issues: 0, errors: 0 };
@@ -2303,13 +2350,17 @@ function checkHookScriptExists(config, silent) {
2303
2350
  * — high-precision (gated on a declared set, built-ins allowlisted, the
2304
2351
  * plugin-namespaced form skipped). Warning by default; "error" gates CI.
2305
2352
  */
2306
- function checkMcpToolResolves(config, silent) {
2353
+ function checkMcpToolResolves(config, silent, adapter) {
2307
2354
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
2308
2355
  if (!sev)
2309
2356
  return { issues: 0, errors: 0 };
2357
+ if (!adapter.capabilities.subagents) {
2358
+ reportNotApplicable("MCP tool-resolution check", "subagents", adapter, silent);
2359
+ return { issues: 0, errors: 0 };
2360
+ }
2310
2361
  let found;
2311
2362
  try {
2312
- found = (0, scan_js_1.scanPlugin)(process.cwd()).agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
2363
+ 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
2364
  }
2314
2365
  catch {
2315
2366
  return { issues: 0, errors: 0 };
@@ -2524,6 +2575,78 @@ function handleGenerateSchema(args, restArgs) {
2524
2575
  console.log(" Add to your markdown frontmatter:\n" +
2525
2576
  ` # yaml-language-server: $schema=./${outPath}`);
2526
2577
  }
2578
+ /**
2579
+ * `vigiles generate-harness [dir] [out]` — emit one typed registry over every
2580
+ * `*.spec.ts` under `dir`, so a single `tsc --noEmit` cross-checks the whole
2581
+ * harness (dangling delegates → a tsc error; duplicate names → this command
2582
+ * exits non-zero; the capability lattice → a computed export). The third
2583
+ * generated artifact beside `generate-types` / `generate-schema`. See
2584
+ * docs/cli.md and research/whole-harness-codegen.md.
2585
+ */
2586
+ async function handleGenerateHarness(args, restArgs) {
2587
+ const checkOnly = args.includes("--check");
2588
+ const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
2589
+ const outPath = restArgs[1] ?? (0, node_path_1.resolve)(dir, generate_harness_js_1.HARNESS_GEN_FILENAME);
2590
+ const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
2591
+ const specImport = args
2592
+ .filter((a) => a.startsWith("--spec-import="))
2593
+ .map((a) => a.split("=")[1])
2594
+ .filter(Boolean)[0] ?? undefined;
2595
+ // Resolve the harness ONCE (honour --harness / config / auto-detect) so the
2596
+ // capability lattice is computed against the right dialect — never defaulting
2597
+ // to Claude Code in core. The dialect is INJECTED into the generator.
2598
+ const harnessFlag = harnessFlagFrom(args);
2599
+ const adapter = harnessFlag
2600
+ ? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
2601
+ : (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
2602
+ console.log(`Scanning ${(0, generate_harness_js_1.labelFor)(process.cwd(), dir)} for *.spec.ts...\n`);
2603
+ const model = await (0, generate_harness_js_1.loadHarnessModel)(dir, (abs) => loadSpec(abs));
2604
+ const result = (0, generate_harness_js_1.generateHarness)(model, {
2605
+ dialect: adapter.dialect,
2606
+ outDir: (0, node_path_1.dirname)(fullOut),
2607
+ specImport,
2608
+ });
2609
+ console.log(` ${String(result.agentCount)} agent(s), ${String(result.edgeCount)} delegate edge(s)` +
2610
+ (result.handoffCount > 0
2611
+ ? `, ${String(result.handoffCount)} handoff check(s)`
2612
+ : ""));
2613
+ console.log(` capabilities: ${result.capabilities.purity} (` +
2614
+ `${String(result.capabilities.sideEffecting.length)} side-effecting, ` +
2615
+ `${String(result.capabilities.unknown.length)} unknown)`);
2616
+ // DUPLICATE NAME — the O(N) JS check (never a type). Exit non-zero, no write.
2617
+ if (result.duplicate) {
2618
+ console.log(`\n✗ ${result.duplicate.message}`);
2619
+ console.log(`::error::${result.duplicate.message}`);
2620
+ process.exit(2);
2621
+ }
2622
+ if (checkOnly) {
2623
+ if (!(0, node_fs_1.existsSync)(fullOut)) {
2624
+ console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate-harness\` to create it.`);
2625
+ process.exit(1);
2626
+ }
2627
+ const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
2628
+ const normalize = (s) => s
2629
+ .split("\n")
2630
+ .map((l) => l.trimEnd())
2631
+ .join("\n")
2632
+ .replace(/\n{3,}/g, "\n\n")
2633
+ .trim();
2634
+ if (normalize(existing) === normalize(result.gen)) {
2635
+ console.log(`\n✓ ${outPath} is up to date`);
2636
+ }
2637
+ else {
2638
+ console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate-harness\` to update.`);
2639
+ process.exit(1);
2640
+ }
2641
+ return;
2642
+ }
2643
+ const outDir = (0, node_path_1.dirname)(fullOut);
2644
+ if (!(0, node_fs_1.existsSync)(outDir))
2645
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
2646
+ (0, node_fs_1.writeFileSync)(fullOut, result.gen);
2647
+ console.log(`\n✓ Generated ${(0, generate_harness_js_1.labelFor)(process.cwd(), fullOut)}`);
2648
+ console.log(" `tsc --noEmit` over this file now checks every delegate target resolves.");
2649
+ }
2527
2650
  /**
2528
2651
  * `vigiles test` / `vigiles eval` — discover and run the two-tier harness
2529
2652
  * scripts (deterministic `*.harness.mjs` / real-model `*.eval.mjs`) as child
@@ -2583,6 +2706,167 @@ function handleRunScripts(kind, args, restArgs) {
2583
2706
  process.exit(1);
2584
2707
  }
2585
2708
  }
2709
+ /** Parse the `--harness=<name>` override out of an argv list (the one definition). */
2710
+ function harnessFlagFrom(argv) {
2711
+ return argv
2712
+ .find((a) => a.startsWith("--harness="))
2713
+ ?.slice("--harness=".length);
2714
+ }
2715
+ /**
2716
+ * `vigiles explain <dir> [name]` — the deterministic WHY behind a low score (C4):
2717
+ * scan a plugin and surface the structural CAUSE of a behavioral symptom + the
2718
+ * one-line fix. No model — it reads the same `ScanReport` `scan` computes. An
2719
+ * optional surface name narrows to one underperforming skill/agent (the
2720
+ * optimizer's call). `--json` for the agent-consumable shape, `--harness=` to
2721
+ * override detection.
2722
+ */
2723
+ function handleExplain(restArgs, args) {
2724
+ const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
2725
+ const surface = restArgs[1];
2726
+ const json = args.includes("--json");
2727
+ const harnessFlag = harnessFlagFrom(args);
2728
+ const adapter = harnessFlag
2729
+ ? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
2730
+ : (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
2731
+ const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
2732
+ const exps = surface ? (0, score_explainer_js_1.explainSurface)(report, surface) : (0, score_explainer_js_1.explainScore)(report);
2733
+ if (json) {
2734
+ console.log(JSON.stringify(exps, null, 2));
2735
+ return;
2736
+ }
2737
+ if (surface)
2738
+ console.log(`Explaining "${surface}":\n`);
2739
+ console.log((0, score_explainer_js_1.formatExplanations)(exps));
2740
+ }
2741
+ /**
2742
+ * The plugin's declared name for the namespaced skill id, read from the layout's
2743
+ * manifest (adapter-aware path, not a hardcoded `.claude-plugin/`), falling back to
2744
+ * the dir basename. JSON manifests only for now (a TOML/Codex manifest → basename).
2745
+ */
2746
+ function pluginNameFor(dir, manifestPath) {
2747
+ try {
2748
+ const manifest = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, manifestPath), "utf-8"));
2749
+ if (typeof manifest.name === "string" && manifest.name)
2750
+ return manifest.name;
2751
+ }
2752
+ catch {
2753
+ /* missing / non-JSON manifest → fall back */
2754
+ }
2755
+ return (0, node_path_1.basename)(dir);
2756
+ }
2757
+ /** Enrich an untested Surface with the metadata the right template needs. */
2758
+ /** Extract `"name": type` fields from one rendered `vigiles:ok`/`err` shape block. */
2759
+ function parseContractFields(block) {
2760
+ const fields = [];
2761
+ const re = /"([^"]+)"\s*:\s*(string\[\]|string|number|boolean)/g;
2762
+ let m;
2763
+ while ((m = re.exec(block)) !== null) {
2764
+ fields.push({ name: m[1], type: m[2] });
2765
+ }
2766
+ return fields;
2767
+ }
2768
+ /**
2769
+ * Parse a subagent's compiled `## Output contract` (the `vigiles:ok` / `vigiles:err`
2770
+ * blocks the compiler emits) back into a typed `ResultContract`, so the generator
2771
+ * can write an `assertAgentOk` test against the real fields. Returns null when the
2772
+ * agent has no result() contract.
2773
+ */
2774
+ function parseResultContract(md) {
2775
+ const ok = /```vigiles:ok\n([\s\S]*?)```/.exec(md);
2776
+ const err = /```vigiles:err\n([\s\S]*?)```/.exec(md);
2777
+ if (!ok && !err)
2778
+ return null;
2779
+ const okFields = ok ? parseContractFields(ok[1]) : [];
2780
+ const errFields = err ? parseContractFields(err[1]) : [];
2781
+ if (okFields.length === 0 && errFields.length === 0)
2782
+ return null;
2783
+ return { ok: okFields, err: errFields };
2784
+ }
2785
+ function scaffoldInputFor(s, report, pluginName, dir, dialect) {
2786
+ const base = { kind: s.kind, name: s.name, path: s.path };
2787
+ switch (s.kind) {
2788
+ case "skill": {
2789
+ const sk = report.skills.find((x) => x.name === s.name);
2790
+ return { ...base, pluginName, userInvoked: sk?.userInvoked };
2791
+ }
2792
+ case "agent": {
2793
+ const ag = report.agents.find((x) => x.name === s.name);
2794
+ const tools = ag?.tools ?? null;
2795
+ const sideEffectingTools = tools
2796
+ ? (0, effects_js_1.effectSurface)(tools, dialect).sideEffecting
2797
+ : undefined;
2798
+ let resultContract = null;
2799
+ try {
2800
+ resultContract = parseResultContract((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, s.path), "utf-8"));
2801
+ }
2802
+ catch {
2803
+ // agent .md unreadable → no contract to generate against
2804
+ }
2805
+ return { ...base, tools, sideEffectingTools, resultContract };
2806
+ }
2807
+ case "hook":
2808
+ return { ...base, hookCommand: `bash ${s.path}` };
2809
+ }
2810
+ }
2811
+ /**
2812
+ * `vigiles scaffold-test [dir]` — generate a runnable STARTER test for each
2813
+ * untested skill/agent/hook (B1, test-gen from free-form). Reuses the
2814
+ * untested-surface detector for the list + `scan` for the metadata, then emits the
2815
+ * cheapest meaningful tier per kind (hook → `runHook`, skill → `measureTriggerRate`,
2816
+ * subagent → `runHarnessTest`) at the surface's suggested test path. Dry-run by
2817
+ * default (prints the scaffolds); `--write` creates the files (never clobbering an
2818
+ * existing one); `--json` for the agent-consumable `{ path, content }[]`.
2819
+ */
2820
+ function handleScaffoldTest(restArgs, args) {
2821
+ const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
2822
+ const write = args.includes("--write");
2823
+ const json = args.includes("--json");
2824
+ const harnessFlag = harnessFlagFrom(args);
2825
+ const adapter = harnessFlag
2826
+ ? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
2827
+ : (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
2828
+ const { untested } = (0, test_coverage_js_1.findUntestedSurfaces)({
2829
+ basePath: dir,
2830
+ layout: adapter.layout,
2831
+ });
2832
+ const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
2833
+ const pluginName = pluginNameFor(dir, adapter.layout.manifestPath);
2834
+ const scaffolds = untested.map((s) => (0, scaffold_test_js_1.scaffoldTest)(scaffoldInputFor(s, report, pluginName, dir, adapter.dialect)));
2835
+ if (json) {
2836
+ console.log(JSON.stringify(scaffolds, null, 2));
2837
+ return;
2838
+ }
2839
+ if (!write) {
2840
+ console.log((0, scaffold_test_js_1.formatScaffolds)(scaffolds));
2841
+ for (const s of scaffolds) {
2842
+ console.log(`\n# ${s.path}\n`);
2843
+ console.log(s.content);
2844
+ }
2845
+ if (scaffolds.length > 0) {
2846
+ console.log("Re-run with --write to create these files.");
2847
+ }
2848
+ return;
2849
+ }
2850
+ const written = [];
2851
+ const skipped = [];
2852
+ for (const s of scaffolds) {
2853
+ const target = (0, node_path_1.resolve)(dir, s.path);
2854
+ if ((0, node_fs_1.existsSync)(target)) {
2855
+ skipped.push(s.path);
2856
+ continue;
2857
+ }
2858
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
2859
+ (0, node_fs_1.writeFileSync)(target, s.content);
2860
+ written.push(s.path);
2861
+ }
2862
+ for (const p of written)
2863
+ console.log(`✓ wrote ${p}`);
2864
+ for (const p of skipped)
2865
+ console.log(`⊘ skipped ${p} (already exists)`);
2866
+ if (written.length === 0 && skipped.length === 0) {
2867
+ console.log("Nothing to scaffold — every surface already has a test.");
2868
+ }
2869
+ }
2586
2870
  function printUsage(command) {
2587
2871
  console.log("vigiles — compile typed specs to instruction files");
2588
2872
  console.log("");
@@ -2592,6 +2876,8 @@ function printUsage(command) {
2592
2876
  console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
2593
2877
  console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
2594
2878
  console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
2879
+ console.log(" vigiles explain <dir> [name] Deterministic WHY a skill/agent underperforms + the fix (--json, --harness=)");
2880
+ console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
2595
2881
  console.log("");
2596
2882
  console.log("Examples:");
2597
2883
  console.log(" vigiles init Auto-detect project, create specs, wire CI");
@@ -2603,6 +2889,8 @@ function printUsage(command) {
2603
2889
  console.log(" vigiles generate-types --check Verify .d.ts is up to date");
2604
2890
  console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
2605
2891
  console.log(" vigiles generate-schema --check Verify schema.json is up to date");
2892
+ console.log(" vigiles generate-harness [dir] Emit harness.gen.ts — one typed registry");
2893
+ console.log(" vigiles generate-harness --check Verify harness.gen.ts is up to date");
2606
2894
  console.log(" vigiles --version Print the version number");
2607
2895
  if (command && command !== "--help") {
2608
2896
  console.log(`\nUnknown command: "${command}"`);
@@ -2701,6 +2989,42 @@ function skillStartCommand(target) {
2701
2989
  (0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
2702
2990
  console.log(`Active skill: ${target}`);
2703
2991
  }
2992
+ /**
2993
+ * PreToolUse-hook entrypoint: enforce the active skill's declared purity floor.
2994
+ * Reads the tool event on stdin, parses the `vigiles:purity:` marker from the
2995
+ * active skill's compiled SKILL.md, and blocks (exit 2 + reason on stderr) any
2996
+ * tool call that violates the declared floor — refining `Bash` by the live
2997
+ * command via `isReadOnlyBash`. Skills have no tools-allowlist rail; this gate
2998
+ * is purity-only. Mirrors `agentHookCommand` for skills.
2999
+ */
3000
+ function skillToolHookCommand() {
3001
+ let raw = "";
3002
+ try {
3003
+ raw = (0, node_fs_1.readFileSync)(0, "utf-8");
3004
+ }
3005
+ catch {
3006
+ /* no stdin */
3007
+ }
3008
+ let tool = "";
3009
+ let command;
3010
+ try {
3011
+ const parsed = JSON.parse(raw);
3012
+ tool = parsed.tool_name ?? "";
3013
+ if (typeof parsed.tool_input?.command === "string") {
3014
+ command = parsed.tool_input.command;
3015
+ }
3016
+ }
3017
+ catch {
3018
+ /* malformed input → no tool, allow */
3019
+ }
3020
+ if (!tool)
3021
+ return;
3022
+ const decision = (0, skill_runtime_js_1.evaluateSkillPreToolUse)(process.cwd(), tool, command);
3023
+ if (!decision.allow) {
3024
+ console.error(decision.message);
3025
+ process.exit(2);
3026
+ }
3027
+ }
2704
3028
  /**
2705
3029
  * PreToolUse-hook entrypoint: enforce the active subagent's allowed-tools
2706
3030
  * contract. Reads the tool event on stdin, parses the active agent's compiled
@@ -2716,15 +3040,49 @@ function agentHookCommand() {
2716
3040
  /* no stdin */
2717
3041
  }
2718
3042
  let tool = "";
3043
+ let command;
3044
+ let event = "";
3045
+ let toolInput;
2719
3046
  try {
2720
- tool = JSON.parse(raw).tool_name ?? "";
3047
+ const parsed = JSON.parse(raw);
3048
+ event = parsed.hook_event_name ?? "";
3049
+ tool = parsed.tool_name ?? "";
3050
+ toolInput = parsed.tool_input;
3051
+ if (typeof parsed.tool_input?.command === "string") {
3052
+ command = parsed.tool_input.command;
3053
+ }
2721
3054
  }
2722
3055
  catch {
2723
3056
  /* malformed input → no tool, allow */
2724
3057
  }
3058
+ const cwd = process.cwd();
3059
+ // EXPERIMENTAL (parked P3, flat-only — do NOT auto-wire): the Task/SubagentStop
3060
+ // bracketing below assumes one active subagent at a time and is NOT nesting-safe
3061
+ // (CC v2.1.172 depth-5 nesting needs a stack + spawn-tool-name check). See
3062
+ // research/effect-boundary-design.md.
3063
+ // SubagentStop → CLOSE the window deterministically (no model `agent-done`):
3064
+ // the subagent returned, so its contract/purity no longer apply.
3065
+ if (event === "SubagentStop") {
3066
+ (0, agent_runtime_js_1.clearActiveAgent)(cwd);
3067
+ (0, effect_region_js_1.clearEffectActive)(cwd);
3068
+ return;
3069
+ }
3070
+ // PreToolUse(Task) → OPEN the window deterministically (no model `agent-start`
3071
+ // / `effect-enter`): the parent is dispatching a subagent, so activate that
3072
+ // subagent's compiled contract for the tool calls it is about to make. The
3073
+ // Task dispatch itself is the PARENT's action — don't gate it against the
3074
+ // subagent's contract; just open the window and allow.
3075
+ if (tool === "Task") {
3076
+ const agentPath = (0, agent_runtime_js_1.decideTaskDispatch)(toolInput, cwd, process.env.CLAUDE_PLUGIN_ROOT);
3077
+ if (agentPath) {
3078
+ (0, agent_runtime_js_1.setActiveAgent)(cwd, agentPath);
3079
+ (0, effect_region_js_1.setEffectActive)(cwd);
3080
+ }
3081
+ return;
3082
+ }
2725
3083
  if (!tool)
2726
3084
  return;
2727
- const decision = (0, agent_runtime_js_2.evaluatePreToolUse)(process.cwd(), tool);
3085
+ const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(cwd, tool, command);
2728
3086
  if (!decision.allow) {
2729
3087
  console.error(decision.message);
2730
3088
  process.exit(2);
@@ -2760,7 +3118,7 @@ function agentStartCommand(target) {
2760
3118
  console.error("Usage: vigiles agent-start <agents/<name>.md>");
2761
3119
  process.exit(2);
2762
3120
  }
2763
- (0, agent_runtime_js_2.setActiveAgent)(process.cwd(), target);
3121
+ (0, agent_runtime_js_1.setActiveAgent)(process.cwd(), target);
2764
3122
  console.log(`Active agent: ${target}`);
2765
3123
  }
2766
3124
  /** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
@@ -2778,11 +3136,14 @@ function handleSkillCommand(command, restArgs) {
2778
3136
  case "skill-hook":
2779
3137
  skillHookCommand();
2780
3138
  return true;
3139
+ case "skill-tool-hook":
3140
+ skillToolHookCommand();
3141
+ return true;
2781
3142
  case "agent-start":
2782
3143
  agentStartCommand(restArgs[0]);
2783
3144
  return true;
2784
3145
  case "agent-done":
2785
- (0, agent_runtime_js_2.clearActiveAgent)(process.cwd());
3146
+ (0, agent_runtime_js_1.clearActiveAgent)(process.cwd());
2786
3147
  return true;
2787
3148
  case "agent-hook":
2788
3149
  agentHookCommand();
@@ -2799,6 +3160,13 @@ function handleSkillCommand(command, restArgs) {
2799
3160
  case "refs-hook":
2800
3161
  refsHookCommand();
2801
3162
  return true;
3163
+ case "effect-enter":
3164
+ (0, effect_region_js_1.setEffectActive)(process.cwd());
3165
+ console.log("Effect boundary entered.");
3166
+ return true;
3167
+ case "effect-exit":
3168
+ (0, effect_region_js_1.clearEffectActive)(process.cwd());
3169
+ return true;
2802
3170
  default:
2803
3171
  return false;
2804
3172
  }
@@ -2969,9 +3337,7 @@ async function main() {
2969
3337
  console.log("Run `vigiles init` to create one.");
2970
3338
  process.exit(0);
2971
3339
  }
2972
- const harnessFlag = args
2973
- .find((a) => a.startsWith("--harness="))
2974
- ?.slice("--harness=".length);
3340
+ const harnessFlag = harnessFlagFrom(args);
2975
3341
  const valid = await compile(specs, config, { harnessFlag });
2976
3342
  console.log("");
2977
3343
  if (valid) {
@@ -3033,9 +3399,7 @@ async function main() {
3033
3399
  }
3034
3400
  else {
3035
3401
  const root = (0, node_path_1.resolve)(targets[0]);
3036
- const harnessFlag = args
3037
- .find((a) => a.startsWith("--harness="))
3038
- ?.slice("--harness=".length);
3402
+ const harnessFlag = harnessFlagFrom(args);
3039
3403
  const det = (0, adapter_registry_js_1.detectAdapterResult)(root);
3040
3404
  const adapter = harnessFlag
3041
3405
  ? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
@@ -3048,7 +3412,15 @@ async function main() {
3048
3412
  }
3049
3413
  console.log("");
3050
3414
  }
3051
- console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
3415
+ if (args.includes("--fix-plan")) {
3416
+ // The deterministic optimization lens on the SAME report: health score
3417
+ // + the ranked free fixes to clear before measuring (the A2 spine).
3418
+ const plan = (0, optimize_js_1.optimize)(report);
3419
+ console.log(json ? JSON.stringify(plan, null, 2) : (0, optimize_js_1.formatOptimize)(plan));
3420
+ }
3421
+ else {
3422
+ console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
3423
+ }
3052
3424
  if (wantTrigger) {
3053
3425
  const harness = adapter.name === "codex" ? "codex" : "claude-code";
3054
3426
  await handleScanTrigger(root, args, json, harness);
@@ -3056,6 +3428,12 @@ async function main() {
3056
3428
  }
3057
3429
  break;
3058
3430
  }
3431
+ case "explain":
3432
+ handleExplain(restArgs, args);
3433
+ break;
3434
+ case "scaffold-test":
3435
+ handleScaffoldTest(restArgs, args);
3436
+ break;
3059
3437
  // --- Plumbing ---
3060
3438
  case "generate-types":
3061
3439
  handleGenerateTypes(args, restArgs);
@@ -3063,6 +3441,9 @@ async function main() {
3063
3441
  case "generate-schema":
3064
3442
  handleGenerateSchema(args, restArgs);
3065
3443
  break;
3444
+ case "generate-harness":
3445
+ await handleGenerateHarness(args, restArgs);
3446
+ break;
3066
3447
  default:
3067
3448
  if (!handleSkillCommand(command, restArgs))
3068
3449
  printUsage(command);