vigiles 5.0.1 → 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 (72) hide show
  1. package/README.md +15 -9
  2. package/dist/adapters/claude-code/adapter.js +1 -0
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +30 -6
  4. package/dist/adapters/claude-code/agent-runtime.js +66 -37
  5. package/dist/adapters/claude-code/dialect.js +37 -2
  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/eval.d.ts +94 -0
  13. package/dist/adapters/codex/eval.js +227 -0
  14. package/dist/adapters/codex/layout.js +3 -0
  15. package/dist/adapters/opencode/adapter.js +1 -0
  16. package/dist/adapters/opencode/layout.js +3 -0
  17. package/dist/check.d.ts +8 -0
  18. package/dist/check.js +27 -3
  19. package/dist/cli.js +712 -21
  20. package/dist/codex.d.ts +1 -0
  21. package/dist/codex.js +3 -0
  22. package/dist/core/adapter.d.ts +10 -0
  23. package/dist/core/bash-effects.d.ts +41 -0
  24. package/dist/core/bash-effects.js +405 -0
  25. package/dist/core/compile.d.ts +3 -1
  26. package/dist/core/compile.js +169 -74
  27. package/dist/core/description-overlap.d.ts +27 -0
  28. package/dist/core/description-overlap.js +53 -0
  29. package/dist/core/dialect.d.ts +18 -0
  30. package/dist/core/effects.d.ts +172 -0
  31. package/dist/core/effects.js +245 -0
  32. package/dist/core/frontmatter-read.d.ts +25 -0
  33. package/dist/core/frontmatter-read.js +138 -0
  34. package/dist/core/hook-events.d.ts +34 -0
  35. package/dist/core/hook-events.js +48 -0
  36. package/dist/core/layout.d.ts +6 -0
  37. package/dist/core/mcp-config.d.ts +20 -0
  38. package/dist/core/mcp-config.js +40 -0
  39. package/dist/core/mcp-hook.d.ts +35 -0
  40. package/dist/core/mcp-hook.js +70 -0
  41. package/dist/core/mcp-tool.d.ts +50 -0
  42. package/dist/core/mcp-tool.js +61 -0
  43. package/dist/core/orphans.js +21 -0
  44. package/dist/core/spec.d.ts +142 -3
  45. package/dist/core/spec.js +48 -0
  46. package/dist/core/tool-contract.d.ts +68 -0
  47. package/dist/core/tool-contract.js +113 -0
  48. package/dist/core/types.d.ts +91 -2
  49. package/dist/core/validate.js +23 -1
  50. package/dist/eval.d.ts +69 -13
  51. package/dist/eval.js +106 -51
  52. package/dist/harness-test.d.ts +7 -0
  53. package/dist/harness-test.js +19 -7
  54. package/dist/leaderboard.d.ts +2 -0
  55. package/dist/leaderboard.js +63 -3
  56. package/dist/optimize.d.ts +74 -0
  57. package/dist/optimize.js +94 -0
  58. package/dist/plugin-loader.d.ts +1 -0
  59. package/dist/plugin-loader.js +71 -18
  60. package/dist/scaffold-test.d.ts +30 -0
  61. package/dist/scaffold-test.js +158 -0
  62. package/dist/scan-behavioral.d.ts +73 -0
  63. package/dist/scan-behavioral.js +150 -0
  64. package/dist/scan.d.ts +166 -1
  65. package/dist/scan.js +622 -55
  66. package/dist/score-explainer.d.ts +69 -0
  67. package/dist/score-explainer.js +169 -0
  68. package/dist/test-coverage.d.ts +7 -0
  69. package/dist/test-coverage.js +39 -24
  70. package/package.json +2 -1
  71. package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -6
  72. package/skills/edit-spec/SKILL.md +1 -1
package/dist/cli.js CHANGED
@@ -19,10 +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");
24
+ const score_explainer_js_1 = require("./score-explainer.js");
25
+ const scan_behavioral_js_1 = require("./scan-behavioral.js");
23
26
  const adapter_registry_js_1 = require("./adapter-registry.js");
24
27
  const skill_harness_js_1 = require("./skill-harness.js");
25
28
  const leaderboard_js_1 = require("./leaderboard.js");
29
+ const optimize_js_1 = require("./optimize.js");
26
30
  const compile_js_1 = require("./core/compile.js");
27
31
  const proofs_js_1 = require("./core/proofs.js");
28
32
  const inline_js_1 = require("./core/inline.js");
@@ -32,6 +36,7 @@ const compose_js_1 = require("./core/compose.js");
32
36
  const compile_generator_js_1 = require("./core/compile-generator.js");
33
37
  const action_gate_js_1 = require("./action-gate.js");
34
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");
35
40
  const tool_intercept_js_1 = require("./tool-intercept.js");
36
41
  const refs_js_1 = require("./core/refs.js");
37
42
  const mcp_js_1 = require("./core/mcp.js");
@@ -633,6 +638,17 @@ function lintExitCode(report) {
633
638
  report.integrityErrors > 0 ||
634
639
  report.coverageErrors > 0 ||
635
640
  report.untestedErrors > 0 ||
641
+ report.toolContractErrors > 0 ||
642
+ report.hookEventErrors > 0 ||
643
+ report.frontmatterSchemaErrors > 0 ||
644
+ report.mcpConfigErrors > 0 ||
645
+ report.skillFrontmatterErrors > 0 ||
646
+ report.mcpToolErrors > 0 ||
647
+ report.hookScriptErrors > 0 ||
648
+ report.disallowedToolErrors > 0 ||
649
+ report.descriptionOverlapErrors > 0 ||
650
+ report.frontmatterValidErrors > 0 ||
651
+ report.mcpHookErrors > 0 ||
636
652
  report.symbolRefErrors > 0 ||
637
653
  report.mcpRefErrors > 0)
638
654
  return 2;
@@ -854,6 +870,17 @@ async function runLint(restArgs, flags, config) {
854
870
  const json = flags.includes("--json");
855
871
  const silent = summary || json;
856
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;
857
884
  // 1. Verify hashes and structure
858
885
  if (!silent) {
859
886
  if (files.length > 0) {
@@ -912,7 +939,41 @@ async function runLint(restArgs, flags, config) {
912
939
  // 7b. Untested-surface check — skills/agents/hooks shipping without a test or
913
940
  // eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
914
941
  // hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
915
- const untested = checkUntestedSurfaces(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);
948
+ // 7d. Hook-event check — a hook registered under an event the harness doesn't
949
+ // define never fires. High-precision (close typos only). Off unless configured.
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);
954
+ // 7f. MCP-config check — a declared MCP server with no command/url can't start.
955
+ const mcpConfig = checkMcpConfig(config, silent, adapter);
956
+ // 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
957
+ // reliable trigger surface). Best-practice nudge; skills load without it.
958
+ const skillFm = checkSkillFrontmatter(config, silent, adapter);
959
+ // 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
960
+ // the plugin doesn't declare can't resolve (the MCP half of the tool moat).
961
+ const mcpToolResolves = checkMcpToolResolves(config, silent, adapter);
962
+ // 7i. Hook-script existence — a hook command referencing a missing script file
963
+ // never runs (matches Anthropic's own `claude plugin validate`).
964
+ const hookScripts = checkHookScriptExists(config, silent, adapter);
965
+ // 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
966
+ // (the deny-side mirror of subagent-tool-contract; close-typo only).
967
+ const disallowedTools = checkDisallowedTools(config, silent, adapter);
968
+ // 7k. Description-overlap — two model-invocable skills with near-identical
969
+ // descriptions collide in the selector (deterministic NCD precision proxy).
970
+ const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter);
971
+ // 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
972
+ // stricter than some loaders, so verify before enforcing).
973
+ const frontmatterValid = checkFrontmatterValid(config, silent, adapter);
974
+ // 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
975
+ // targets an undeclared server (the moat applied to the hook surface).
976
+ const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
916
977
  // 8. Validate vigiles builder calls inside markdown code blocks. Default
917
978
  // is to validate every ref; illustrative blocks opt out via
918
979
  // `<!-- vigiles:ignore -->` (single block) or
@@ -956,6 +1017,28 @@ async function runLint(restArgs, flags, config) {
956
1017
  orphanCount: orphanReport.orphans.length,
957
1018
  untestedSurfaces: untested.untested,
958
1019
  untestedErrors: untested.errors,
1020
+ toolContractIssues: toolContract.issues,
1021
+ toolContractErrors: toolContract.errors,
1022
+ hookEventIssues: hookEvents.issues,
1023
+ hookEventErrors: hookEvents.errors,
1024
+ frontmatterSchemaIssues: frontmatter.issues,
1025
+ frontmatterSchemaErrors: frontmatter.errors,
1026
+ mcpConfigIssues: mcpConfig.issues,
1027
+ mcpConfigErrors: mcpConfig.errors,
1028
+ skillFrontmatterIssues: skillFm.issues,
1029
+ skillFrontmatterErrors: skillFm.errors,
1030
+ mcpToolIssues: mcpToolResolves.issues,
1031
+ mcpToolErrors: mcpToolResolves.errors,
1032
+ hookScriptIssues: hookScripts.issues,
1033
+ hookScriptErrors: hookScripts.errors,
1034
+ disallowedToolIssues: disallowedTools.issues,
1035
+ disallowedToolErrors: disallowedTools.errors,
1036
+ descriptionOverlapIssues: descriptionOverlap.issues,
1037
+ descriptionOverlapErrors: descriptionOverlap.errors,
1038
+ frontmatterValidIssues: frontmatterValid.issues,
1039
+ frontmatterValidErrors: frontmatterValid.errors,
1040
+ mcpHookIssues: mcpHookTargets.issues,
1041
+ mcpHookErrors: mcpHookTargets.errors,
959
1042
  docRefErrors: docRefReport.errors.length,
960
1043
  symbolRefErrors,
961
1044
  mcpRefErrors,
@@ -1524,7 +1607,7 @@ function redirectSyncToolTargets(cwd, targets) {
1524
1607
  }
1525
1608
  /** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
1526
1609
  * instruction file (so `--lint` always delivers a spec), but never compiles
1527
- * 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. */
1528
1611
  async function setupPillar1(detected, targetValue, harnesses) {
1529
1612
  const cwd = process.cwd();
1530
1613
  const written = [];
@@ -1549,7 +1632,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
1549
1632
  }
1550
1633
  if (targetExists && !targetHasHash((0, node_path_1.resolve)(cwd, target))) {
1551
1634
  needsMigration.push(target);
1552
- 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\`.`);
1553
1636
  }
1554
1637
  }
1555
1638
  // Generate types + schema.
@@ -1741,7 +1824,7 @@ function printSetupSummary(opts) {
1741
1824
  console.log("\n---\nSetup complete.\n");
1742
1825
  const nextSteps = [];
1743
1826
  if (needsMigration.length > 0) {
1744
- 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\``);
1745
1828
  }
1746
1829
  else if (specPathsList.length > 0) {
1747
1830
  nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
@@ -1888,16 +1971,16 @@ function checkIntegrityForFiles(files, severity, silent) {
1888
1971
  return severity === "error" ? errorCount : 0;
1889
1972
  }
1890
1973
  /**
1891
- * Apply the per-kind `untested-skill` / `untested-agent` / `untested-hook` rules:
1974
+ * Apply the per-kind `untested-skill` / `untested-subagent` / `untested-hook` rules:
1892
1975
  * find skills/agents/hooks with no test or eval (see src/test-coverage.ts). Each
1893
1976
  * kind is gated by its OWN rule severity — a kind set to `false` is not scanned;
1894
1977
  * "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
1895
1978
  * untested count plus the severity-gated error count.
1896
1979
  */
1897
- function checkUntestedSurfaces(config, silent) {
1980
+ function checkUntestedSurfaces(config, silent, adapter) {
1898
1981
  const rules = config?.rules;
1899
1982
  const skillSev = (0, types_js_1.ruleSeverity)(rules?.["untested-skill"]);
1900
- const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-agent"]);
1983
+ const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-subagent"]);
1901
1984
  const hookSev = (0, types_js_1.ruleSeverity)(rules?.["untested-hook"]);
1902
1985
  if (!skillSev && !agentSev && !hookSev)
1903
1986
  return { untested: 0, errors: 0 };
@@ -1906,11 +1989,12 @@ function checkUntestedSurfaces(config, silent) {
1906
1989
  // whichever of the three rules carries them.
1907
1990
  const opts = {
1908
1991
  ...(0, types_js_1.ruleOptions)(rules?.["untested-skill"]),
1909
- ...(0, types_js_1.ruleOptions)(rules?.["untested-agent"]),
1992
+ ...(0, types_js_1.ruleOptions)(rules?.["untested-subagent"]),
1910
1993
  ...(0, types_js_1.ruleOptions)(rules?.["untested-hook"]),
1911
1994
  };
1912
1995
  const report = (0, test_coverage_js_1.findUntestedSurfaces)({
1913
1996
  basePath: process.cwd(),
1997
+ layout: adapter.layout,
1914
1998
  skills: skillSev !== false,
1915
1999
  agents: agentSev !== false,
1916
2000
  hooks: hookSev !== false,
@@ -1931,6 +2015,363 @@ function checkUntestedSurfaces(config, silent) {
1931
2015
  errors: report.untested.filter((s) => sevFor(s.kind) === "error").length,
1932
2016
  };
1933
2017
  }
2018
+ /**
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:`
2032
+ * rail against the harness tool catalog (the moat — "valid is not true"). Flags
2033
+ * only the HIGH-CONFIDENCE issues (a never-available tool, or a close typo) via
2034
+ * the shared `confidentToolIssues` detector — the same code `scan` and
2035
+ * `compileAgent` use (one-detector-no-drift), so a bare unrecognized tool
2036
+ * (plugin/MCP-provided) is never a false alarm. Warning by default; set
2037
+ * `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
2038
+ */
2039
+ function checkSubagentToolContracts(config, silent, adapter) {
2040
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
2041
+ if (!sev)
2042
+ return { issues: 0, errors: 0 };
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
+ }
2057
+ let issues = 0;
2058
+ let printedHeader = false;
2059
+ for (const agent of agents) {
2060
+ if (agent.toolIssues.length === 0)
2061
+ continue;
2062
+ issues += agent.toolIssues.length;
2063
+ if (!silent) {
2064
+ if (!printedHeader) {
2065
+ console.log("\nSubagent tool-contract check:\n");
2066
+ printedHeader = true;
2067
+ }
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);
2071
+ }
2072
+ }
2073
+ }
2074
+ return { issues, errors: sev === "error" ? issues : 0 };
2075
+ }
2076
+ /**
2077
+ * Apply the `hook-events` rule: flag a hook registered under an event name the
2078
+ * harness doesn't define (a typo → the hook never fires). Reuses `scanPlugin`'s
2079
+ * `hookEventIssues` (the shared detector, high-precision: close typos only, never
2080
+ * a framework/custom event). Warning by default; "error" gates CI.
2081
+ */
2082
+ function checkHookEvents(config, silent, adapter) {
2083
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
2084
+ if (!sev)
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
+ }
2090
+ let found;
2091
+ try {
2092
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookEventIssues;
2093
+ }
2094
+ catch {
2095
+ return { issues: 0, errors: 0 };
2096
+ }
2097
+ if (found.length > 0 && !silent) {
2098
+ console.log("\nHook-event check:\n");
2099
+ for (const issue of found) {
2100
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
2101
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message);
2102
+ }
2103
+ }
2104
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2105
+ }
2106
+ /**
2107
+ * Apply the `subagent-frontmatter` rule. Two kinds of subagent-frontmatter defect, one
2108
+ * rule: (1) a subagent MISSING a required field (`name`/`description`) — it won't
2109
+ * register; (2) a subagent with an INVALID `model:`/`color:` value (a close typo
2110
+ * of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
2111
+ * `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
2112
+ */
2113
+ function checkFrontmatterSchema(config, silent, adapter) {
2114
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
2115
+ if (!sev)
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
+ }
2121
+ let found;
2122
+ try {
2123
+ const r = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect);
2124
+ found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
2125
+ }
2126
+ catch {
2127
+ return { issues: 0, errors: 0 };
2128
+ }
2129
+ if (found.length > 0 && !silent) {
2130
+ console.log("\nFrontmatter-schema check:\n");
2131
+ for (const issue of found) {
2132
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
2133
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
2134
+ }
2135
+ }
2136
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2137
+ }
2138
+ /**
2139
+ * Apply the `skill-frontmatter` rule: RECOMMEND (not require) that a SKILL.md
2140
+ * declares an explicit `name` + `description` rather than relying on the
2141
+ * dir-name / first-paragraph fallbacks — a more reliable trigger surface. The
2142
+ * skill still LOADS without them, so this is a best-practice nudge: warn by
2143
+ * default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
2144
+ * `skillMetaIssues`.
2145
+ */
2146
+ function checkSkillFrontmatter(config, silent, adapter) {
2147
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
2148
+ if (!sev)
2149
+ return { issues: 0, errors: 0 };
2150
+ let found;
2151
+ try {
2152
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillMetaIssues;
2153
+ }
2154
+ catch {
2155
+ return { issues: 0, errors: 0 };
2156
+ }
2157
+ if (found.length > 0 && !silent) {
2158
+ console.log("\nSkill-frontmatter check:\n");
2159
+ for (const issue of found) {
2160
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
2161
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
2162
+ }
2163
+ }
2164
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2165
+ }
2166
+ /**
2167
+ * Apply the `mcp-config` rule: a declared MCP server with neither a `command`
2168
+ * (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
2169
+ * Warning by default; "error" gates CI.
2170
+ */
2171
+ function checkMcpConfig(config, silent, adapter) {
2172
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
2173
+ if (!sev)
2174
+ return { issues: 0, errors: 0 };
2175
+ let found;
2176
+ try {
2177
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpIssues;
2178
+ }
2179
+ catch {
2180
+ return { issues: 0, errors: 0 };
2181
+ }
2182
+ if (found.length > 0 && !silent) {
2183
+ console.log("\nMCP-config check:\n");
2184
+ for (const issue of found) {
2185
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
2186
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message);
2187
+ }
2188
+ }
2189
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2190
+ }
2191
+ /**
2192
+ * Apply the `disallowed-tools-contract` rule: a subagent's `disallowedTools:`
2193
+ * block-list entry that's a close typo of a real tool blocks NOTHING — the tool
2194
+ * it was meant to deny stays available, silently. Reuses `scanPlugin`'s per-agent
2195
+ * `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
2196
+ * "error" gates CI.
2197
+ */
2198
+ function checkDisallowedTools(config, silent, adapter) {
2199
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
2200
+ if (!sev)
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
+ }
2206
+ let found;
2207
+ try {
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 })));
2209
+ }
2210
+ catch {
2211
+ return { issues: 0, errors: 0 };
2212
+ }
2213
+ if (found.length > 0 && !silent) {
2214
+ console.log("\nDisallowed-tools check:\n");
2215
+ for (const issue of found) {
2216
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.path}: ${issue.message}`);
2217
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
2218
+ }
2219
+ }
2220
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2221
+ }
2222
+ /**
2223
+ * Apply the `frontmatter-valid` rule: a skill/agent `---` block that EXISTS but
2224
+ * isn't valid YAML — fields may not parse as intended. Reuses `scanPlugin`'s
2225
+ * `malformedFrontmatter`. HONEST caveat (see docs/rules/frontmatter-valid.md):
2226
+ * js-yaml is stricter than some loaders, so a one-line `description:` with a
2227
+ * colon / `<example>` is flagged though it may still load — hence WARN by default
2228
+ * (verify before setting "error").
2229
+ */
2230
+ function checkFrontmatterValid(config, silent, adapter) {
2231
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
2232
+ if (!sev)
2233
+ return { issues: 0, errors: 0 };
2234
+ let found;
2235
+ try {
2236
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).malformedFrontmatter;
2237
+ }
2238
+ catch {
2239
+ return { issues: 0, errors: 0 };
2240
+ }
2241
+ if (found.length > 0 && !silent) {
2242
+ console.log("\nFrontmatter-validity check:\n");
2243
+ for (const issue of found) {
2244
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
2245
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
2246
+ }
2247
+ }
2248
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2249
+ }
2250
+ /**
2251
+ * Apply the `description-overlap` rule: two model-invocable skills with
2252
+ * near-identical descriptions collide in the selector — the wrong one fires. A
2253
+ * deterministic NCD proxy for a `--trigger`-class precision bug. Reuses
2254
+ * `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
2255
+ * identical text). Warning by default; "error" gates CI.
2256
+ */
2257
+ function checkDescriptionOverlap(config, silent, adapter) {
2258
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
2259
+ if (!sev)
2260
+ return { issues: 0, errors: 0 };
2261
+ let found;
2262
+ try {
2263
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).descriptionOverlaps;
2264
+ }
2265
+ catch {
2266
+ return { issues: 0, errors: 0 };
2267
+ }
2268
+ if (found.length > 0 && !silent) {
2269
+ console.log("\nDescription-overlap check:\n");
2270
+ for (const issue of found) {
2271
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
2272
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message);
2273
+ }
2274
+ }
2275
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2276
+ }
2277
+ /**
2278
+ * Apply the `mcp-hook-target-resolves` rule: a `type: "mcp_tool"` hook action
2279
+ * that's incomplete (no `server`/`tool`) or targets a server the plugin doesn't
2280
+ * declare — the hook silently never dispatches. Reuses `scanPlugin`'s
2281
+ * `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
2282
+ * Warning by default; "error" gates CI.
2283
+ */
2284
+ function checkMcpHookTargets(config, silent, adapter) {
2285
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
2286
+ if (!sev)
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
+ }
2292
+ let found;
2293
+ try {
2294
+ found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpHookIssues;
2295
+ }
2296
+ catch {
2297
+ return { issues: 0, errors: 0 };
2298
+ }
2299
+ if (found.length > 0 && !silent) {
2300
+ console.log("\nMCP hook-target check:\n");
2301
+ for (const issue of found) {
2302
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
2303
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message);
2304
+ }
2305
+ }
2306
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2307
+ }
2308
+ /**
2309
+ * Apply the `hook-script-exists` rule: a hook command references a script file
2310
+ * that doesn't exist on disk (with `${CLAUDE_PLUGIN_ROOT}` resolved) → the hook
2311
+ * silently never runs. Reuses `scanPlugin`'s `hooks` (status "missing"); the
2312
+ * shared resolver already excludes the FP-prone cases (unresolved vars,
2313
+ * existence-guarded one-liners, inline commands). Matches Anthropic's own
2314
+ * `claude plugin validate`. Warning by default; "error" gates CI.
2315
+ */
2316
+ function checkHookScriptExists(config, silent, adapter) {
2317
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
2318
+ if (!sev)
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
+ }
2324
+ let missing;
2325
+ try {
2326
+ missing = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hooks.filter((h) => h.status === "missing");
2327
+ }
2328
+ catch {
2329
+ return { issues: 0, errors: 0 };
2330
+ }
2331
+ if (missing.length > 0 && !silent) {
2332
+ console.log("\nHook-script existence check:\n");
2333
+ for (const h of missing) {
2334
+ const msg = `hook script "${h.script}" is referenced but missing — the hook never runs.`;
2335
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${msg}`);
2336
+ ghAnnotate(sev === "error" ? "error" : "warning", msg);
2337
+ }
2338
+ }
2339
+ return {
2340
+ issues: missing.length,
2341
+ errors: sev === "error" ? missing.length : 0,
2342
+ };
2343
+ }
2344
+ /**
2345
+ * Apply the `mcp-tool-resolves` rule: an `mcp__server__tool` in a subagent's
2346
+ * contract whose server isn't in the plugin's declared `mcpServers` can't resolve
2347
+ * (the MCP half of the tool moat). Reuses `scanPlugin`'s per-agent `mcpToolIssues`
2348
+ * — high-precision (gated on a declared set, built-ins allowlisted, the
2349
+ * plugin-namespaced form skipped). Warning by default; "error" gates CI.
2350
+ */
2351
+ function checkMcpToolResolves(config, silent, adapter) {
2352
+ const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
2353
+ if (!sev)
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
+ }
2359
+ let found;
2360
+ try {
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 })));
2362
+ }
2363
+ catch {
2364
+ return { issues: 0, errors: 0 };
2365
+ }
2366
+ if (found.length > 0 && !silent) {
2367
+ console.log("\nMCP tool-resolution check:\n");
2368
+ for (const issue of found) {
2369
+ console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.path}: ${issue.message}`);
2370
+ ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
2371
+ }
2372
+ }
2373
+ return { issues: found.length, errors: sev === "error" ? found.length : 0 };
2374
+ }
1934
2375
  /**
1935
2376
  * Apply the configured coverage thresholds. Returns the number of failing
1936
2377
  * thresholds (so the lint can fail CI when severity is "error").
@@ -2008,6 +2449,43 @@ function findInstructionFiles(restArgs) {
2008
2449
  }
2009
2450
  return files;
2010
2451
  }
2452
+ /** Value of a `--flag=value` arg (the `=` form, so it never collides with a positional). */
2453
+ function flagValue(args, name) {
2454
+ return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
2455
+ }
2456
+ /**
2457
+ * The `scan --trigger` behavioral column: load the author-supplied per-skill
2458
+ * prompt sets, probe the plugin's model-invocable skills, print the column.
2459
+ * Model-gated and opt-in — the structural scan above stays deterministic.
2460
+ */
2461
+ async function handleScanTrigger(root, args, json, harness) {
2462
+ const promptsPath = flagValue(args, "--prompts");
2463
+ if (!promptsPath) {
2464
+ console.error("scan --trigger needs --prompts=<file.json> (a map of skill name → { prompts, irrelevant }).");
2465
+ process.exitCode = 2;
2466
+ return;
2467
+ }
2468
+ let promptSet;
2469
+ try {
2470
+ promptSet = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(promptsPath), "utf-8"));
2471
+ }
2472
+ catch (e) {
2473
+ console.error(`scan --trigger: could not read --prompts file "${promptsPath}": ${e instanceof Error ? e.message : String(e)}`);
2474
+ process.exitCode = 2;
2475
+ return;
2476
+ }
2477
+ const concurrencyRaw = flagValue(args, "--concurrency");
2478
+ const minPromptsRaw = flagValue(args, "--min-prompts");
2479
+ const report = await (0, scan_behavioral_js_1.probePluginTriggers)(root, promptSet, {
2480
+ concurrency: concurrencyRaw ? Number(concurrencyRaw) : undefined,
2481
+ minPrompts: minPromptsRaw ? Number(minPromptsRaw) : undefined,
2482
+ model: flagValue(args, "--model"),
2483
+ harness,
2484
+ });
2485
+ console.log(json
2486
+ ? JSON.stringify(report, null, 2)
2487
+ : `\n${(0, scan_behavioral_js_1.formatBehavioralReport)(report)}`);
2488
+ }
2011
2489
  function handleGenerateTypes(args, restArgs) {
2012
2490
  const checkOnly = args.includes("--check");
2013
2491
  const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
@@ -2154,6 +2632,129 @@ function handleRunScripts(kind, args, restArgs) {
2154
2632
  process.exit(1);
2155
2633
  }
2156
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
+ }
2157
2758
  function printUsage(command) {
2158
2759
  console.log("vigiles — compile typed specs to instruction files");
2159
2760
  console.log("");
@@ -2163,6 +2764,8 @@ function printUsage(command) {
2163
2764
  console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
2164
2765
  console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
2165
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)");
2166
2769
  console.log("");
2167
2770
  console.log("Examples:");
2168
2771
  console.log(" vigiles init Auto-detect project, create specs, wire CI");
@@ -2272,6 +2875,42 @@ function skillStartCommand(target) {
2272
2875
  (0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
2273
2876
  console.log(`Active skill: ${target}`);
2274
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
+ }
2275
2914
  /**
2276
2915
  * PreToolUse-hook entrypoint: enforce the active subagent's allowed-tools
2277
2916
  * contract. Reads the tool event on stdin, parses the active agent's compiled
@@ -2287,15 +2926,20 @@ function agentHookCommand() {
2287
2926
  /* no stdin */
2288
2927
  }
2289
2928
  let tool = "";
2929
+ let command;
2290
2930
  try {
2291
- 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
+ }
2292
2936
  }
2293
2937
  catch {
2294
2938
  /* malformed input → no tool, allow */
2295
2939
  }
2296
2940
  if (!tool)
2297
2941
  return;
2298
- const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(process.cwd(), tool);
2942
+ const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(process.cwd(), tool, command);
2299
2943
  if (!decision.allow) {
2300
2944
  console.error(decision.message);
2301
2945
  process.exit(2);
@@ -2349,6 +2993,9 @@ function handleSkillCommand(command, restArgs) {
2349
2993
  case "skill-hook":
2350
2994
  skillHookCommand();
2351
2995
  return true;
2996
+ case "skill-tool-hook":
2997
+ skillToolHookCommand();
2998
+ return true;
2352
2999
  case "agent-start":
2353
3000
  agentStartCommand(restArgs[0]);
2354
3001
  return true;
@@ -2370,6 +3017,13 @@ function handleSkillCommand(command, restArgs) {
2370
3017
  case "refs-hook":
2371
3018
  refsHookCommand();
2372
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;
2373
3027
  default:
2374
3028
  return false;
2375
3029
  }
@@ -2540,9 +3194,7 @@ async function main() {
2540
3194
  console.log("Run `vigiles init` to create one.");
2541
3195
  process.exit(0);
2542
3196
  }
2543
- const harnessFlag = args
2544
- .find((a) => a.startsWith("--harness="))
2545
- ?.slice("--harness=".length);
3197
+ const harnessFlag = harnessFlagFrom(args);
2546
3198
  const valid = await compile(specs, config, { harnessFlag });
2547
3199
  console.log("");
2548
3200
  if (valid) {
@@ -2574,21 +3226,42 @@ async function main() {
2574
3226
  case "scan": {
2575
3227
  const dirs = restArgs.length > 0 ? restArgs : ["."];
2576
3228
  const json = args.includes("--json");
2577
- if (dirs.length > 1) {
3229
+ // A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
3230
+ // under one marketplace.json) expands into its members and ranks them.
3231
+ const market = dirs.length === 1 ? (0, scan_js_1.inspectMarketplace)((0, node_path_1.resolve)(dirs[0])) : null;
3232
+ const targets = market && market.onDisk.length > 0 ? [...market.onDisk] : dirs;
3233
+ const wantTrigger = args.includes("--trigger");
3234
+ if (market && market.onDisk.length === 0 && market.total > 0) {
3235
+ // A CURATED marketplace — every member is an external git/url plugin, so
3236
+ // there's nothing on disk to scan. Say so honestly instead of falling
3237
+ // through to a misleading "empty machine / no structural issues" report
3238
+ // (obra/superpowers-marketplace, anthropics/claude-plugins-community).
3239
+ if (json) {
3240
+ console.log(JSON.stringify(market, null, 2));
3241
+ }
3242
+ else {
3243
+ console.log(`Marketplace "${market.name}": ${String(market.total)} plugin(s), all external ` +
3244
+ `(url/git sources, not on disk).\n` +
3245
+ `Nothing to scan here — clone a member plugin and scan that, or scan a ` +
3246
+ `marketplace that vendors its plugins in-tree.`);
3247
+ }
3248
+ }
3249
+ else if (targets.length > 1) {
2578
3250
  // Multiple targets → rank them (the leaderboard engine).
2579
- const scores = (0, leaderboard_js_1.rankPlugins)(dirs);
3251
+ const scores = (0, leaderboard_js_1.rankPlugins)(targets);
2580
3252
  console.log(json ? JSON.stringify(scores, null, 2) : (0, leaderboard_js_1.formatLeaderboard)(scores));
3253
+ if (wantTrigger) {
3254
+ console.log("\n⚠ --trigger (behavioral column) runs per single plugin; not yet wired into the leaderboard. Scan one plugin dir to probe it.");
3255
+ }
2581
3256
  }
2582
3257
  else {
2583
- const root = (0, node_path_1.resolve)(dirs[0]);
2584
- const harnessFlag = args
2585
- .find((a) => a.startsWith("--harness="))
2586
- ?.slice("--harness=".length);
3258
+ const root = (0, node_path_1.resolve)(targets[0]);
3259
+ const harnessFlag = harnessFlagFrom(args);
2587
3260
  const det = (0, adapter_registry_js_1.detectAdapterResult)(root);
2588
3261
  const adapter = harnessFlag
2589
3262
  ? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
2590
3263
  : det.adapter;
2591
- const report = (0, scan_js_1.scanPlugin)(dirs[0], adapter.layout);
3264
+ const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect);
2592
3265
  if (!json) {
2593
3266
  console.log(`Detected harness: ${adapter.name}`);
2594
3267
  if (!harnessFlag && det.ambiguousWith.length > 0) {
@@ -2596,10 +3269,28 @@ async function main() {
2596
3269
  }
2597
3270
  console.log("");
2598
3271
  }
2599
- 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
+ }
3281
+ if (wantTrigger) {
3282
+ const harness = adapter.name === "codex" ? "codex" : "claude-code";
3283
+ await handleScanTrigger(root, args, json, harness);
3284
+ }
2600
3285
  }
2601
3286
  break;
2602
3287
  }
3288
+ case "explain":
3289
+ handleExplain(restArgs, args);
3290
+ break;
3291
+ case "scaffold-test":
3292
+ handleScaffoldTest(restArgs, args);
3293
+ break;
2603
3294
  // --- Plumbing ---
2604
3295
  case "generate-types":
2605
3296
  handleGenerateTypes(args, restArgs);