vigiles 2.5.0 → 3.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 (209) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/README.md +73 -434
  3. package/action.yml +143 -7
  4. package/dist/action-gate.d.ts +1 -1
  5. package/dist/action-gate.js +1 -1
  6. package/dist/adapter-conformance.d.ts +30 -0
  7. package/dist/adapter-conformance.js +153 -0
  8. package/dist/adapter-registry.d.ts +42 -0
  9. package/dist/adapter-registry.js +55 -0
  10. package/dist/adapter.d.ts +26 -0
  11. package/dist/adapter.js +16 -0
  12. package/dist/adapters/claude-code/adapter.d.ts +3 -0
  13. package/dist/adapters/claude-code/adapter.js +46 -0
  14. package/dist/{agent-result.d.ts → adapters/claude-code/agent-result.d.ts} +1 -1
  15. package/dist/adapters/claude-code/dialect.d.ts +13 -0
  16. package/dist/adapters/claude-code/dialect.js +51 -0
  17. package/dist/adapters/claude-code/egress-entry.d.ts +2 -0
  18. package/dist/adapters/claude-code/egress-entry.js +115 -0
  19. package/dist/adapters/claude-code/egress-proxy.d.ts +2 -0
  20. package/dist/adapters/claude-code/egress-proxy.js +60 -0
  21. package/dist/adapters/claude-code/egress.d.ts +114 -0
  22. package/dist/adapters/claude-code/egress.js +276 -0
  23. package/dist/adapters/claude-code/eval-baseline.d.ts +68 -0
  24. package/dist/adapters/claude-code/eval-baseline.js +173 -0
  25. package/dist/{eval-cache.d.ts → adapters/claude-code/eval-cache.d.ts} +1 -1
  26. package/dist/{eval-cache.js → adapters/claude-code/eval-cache.js} +1 -1
  27. package/dist/{eval.d.ts → adapters/claude-code/eval.d.ts} +28 -4
  28. package/dist/{eval.js → adapters/claude-code/eval.js} +61 -25
  29. package/dist/{harness-test.d.ts → adapters/claude-code/harness-test.d.ts} +33 -37
  30. package/dist/{harness-test.js → adapters/claude-code/harness-test.js} +124 -43
  31. package/dist/adapters/claude-code/hook-protocol.d.ts +10 -0
  32. package/dist/adapters/claude-code/hook-protocol.js +10 -0
  33. package/dist/adapters/claude-code/layout.d.ts +8 -0
  34. package/dist/adapters/claude-code/layout.js +18 -0
  35. package/dist/{mock-model.d.ts → adapters/claude-code/mock-model.d.ts} +2 -24
  36. package/dist/adapters/claude-code/model-mock.d.ts +11 -0
  37. package/dist/adapters/claude-code/model-mock.js +10 -0
  38. package/dist/adapters/claude-code/plugin-loader.d.ts +25 -0
  39. package/dist/adapters/claude-code/plugin-loader.js +19 -0
  40. package/dist/adapters/claude-code/run-hook.d.ts +204 -0
  41. package/dist/adapters/claude-code/run-hook.js +418 -0
  42. package/dist/adapters/claude-code/run-scripts.d.ts +52 -0
  43. package/dist/adapters/claude-code/run-scripts.js +150 -0
  44. package/dist/adapters/claude-code/runtime.d.ts +16 -0
  45. package/dist/adapters/claude-code/runtime.js +39 -0
  46. package/dist/{sandbox.d.ts → adapters/claude-code/sandbox.d.ts} +45 -4
  47. package/dist/{sandbox.js → adapters/claude-code/sandbox.js} +76 -8
  48. package/dist/{skill-driver.d.ts → adapters/claude-code/skill-driver.d.ts} +1 -1
  49. package/dist/adapters/codex/adapter.d.ts +3 -0
  50. package/dist/adapters/codex/adapter.js +49 -0
  51. package/dist/adapters/codex/dialect.d.ts +10 -0
  52. package/dist/adapters/codex/dialect.js +30 -0
  53. package/dist/adapters/codex/driver.d.ts +20 -0
  54. package/dist/adapters/codex/driver.js +89 -0
  55. package/dist/adapters/codex/hook-protocol.d.ts +10 -0
  56. package/dist/adapters/codex/hook-protocol.js +18 -0
  57. package/dist/adapters/codex/layout.d.ts +16 -0
  58. package/dist/adapters/codex/layout.js +18 -0
  59. package/dist/adapters/codex/mock-model.d.ts +52 -0
  60. package/dist/adapters/codex/mock-model.js +210 -0
  61. package/dist/adapters/codex/model-mock.d.ts +11 -0
  62. package/dist/adapters/codex/model-mock.js +10 -0
  63. package/dist/adapters/codex/runtime.d.ts +38 -0
  64. package/dist/adapters/codex/runtime.js +62 -0
  65. package/dist/adapters/opencode/adapter.d.ts +3 -0
  66. package/dist/adapters/opencode/adapter.js +48 -0
  67. package/dist/adapters/opencode/dialect.d.ts +10 -0
  68. package/dist/adapters/opencode/dialect.js +36 -0
  69. package/dist/adapters/opencode/layout.d.ts +9 -0
  70. package/dist/adapters/opencode/layout.js +25 -0
  71. package/dist/adapters/opencode/model-mock.d.ts +10 -0
  72. package/dist/adapters/opencode/model-mock.js +10 -0
  73. package/dist/adapters/opencode/runtime.d.ts +9 -0
  74. package/dist/adapters/opencode/runtime.js +21 -0
  75. package/dist/claude-code.d.ts +16 -0
  76. package/dist/claude-code.js +32 -0
  77. package/dist/cli-flags.d.ts +22 -0
  78. package/dist/cli-flags.js +38 -0
  79. package/dist/cli.js +406 -206
  80. package/dist/codex.d.ts +20 -0
  81. package/dist/codex.js +36 -0
  82. package/dist/community-skills.d.ts +1 -1
  83. package/dist/community-skills.js +2 -2
  84. package/dist/core/adapter.d.ts +88 -0
  85. package/dist/core/adapter.js +3 -0
  86. package/dist/{compile.d.ts → core/compile.d.ts} +11 -2
  87. package/dist/{compile.js → core/compile.js} +68 -53
  88. package/dist/core/compose.d.ts +79 -0
  89. package/dist/core/compose.js +145 -0
  90. package/dist/core/dialect.d.ts +51 -0
  91. package/dist/core/dialect.js +3 -0
  92. package/dist/core/harness-driver.d.ts +134 -0
  93. package/dist/core/harness-driver.js +3 -0
  94. package/dist/core/hook-protocol.d.ts +28 -0
  95. package/dist/core/hook-protocol.js +3 -0
  96. package/dist/core/layout.d.ts +43 -0
  97. package/dist/core/layout.js +3 -0
  98. package/dist/core/model-mock.d.ts +27 -0
  99. package/dist/core/model-mock.js +3 -0
  100. package/dist/core/refs.d.ts +62 -0
  101. package/dist/{refs.js → core/refs.js} +49 -23
  102. package/dist/core/runtime.d.ts +38 -0
  103. package/dist/core/runtime.js +3 -0
  104. package/dist/{types.d.ts → core/types.d.ts} +29 -0
  105. package/dist/{validate.d.ts → core/validate.d.ts} +1 -1
  106. package/dist/{validate.js → core/validate.js} +41 -4
  107. package/dist/e2e.d.ts +19 -0
  108. package/dist/e2e.js +39 -0
  109. package/dist/harness-assert.d.ts +60 -11
  110. package/dist/harness-assert.js +111 -9
  111. package/dist/integration.d.ts +16 -0
  112. package/dist/integration.js +32 -0
  113. package/dist/leaderboard.d.ts +33 -0
  114. package/dist/leaderboard.js +107 -0
  115. package/dist/linting.d.ts +9 -0
  116. package/dist/linting.js +25 -0
  117. package/dist/plugin-loader.d.ts +6 -3
  118. package/dist/plugin-loader.js +101 -49
  119. package/dist/scan.d.ts +49 -0
  120. package/dist/scan.js +177 -0
  121. package/dist/setup-plan.d.ts +51 -0
  122. package/dist/setup-plan.js +85 -0
  123. package/dist/skill-test.d.ts +1 -1
  124. package/dist/skill-test.js +1 -1
  125. package/dist/test-coverage.d.ts +71 -0
  126. package/dist/test-coverage.js +228 -0
  127. package/dist/testing.d.ts +12 -0
  128. package/dist/testing.js +28 -0
  129. package/dist/unit.d.ts +17 -0
  130. package/dist/unit.js +36 -0
  131. package/hooks/refs-nudge.sh +24 -0
  132. package/package.json +35 -16
  133. package/skills/audit-feedback-loop/SKILL.md +76 -0
  134. package/skills/edit-spec/SKILL.md +131 -0
  135. package/skills/enforce-rules-format/SKILL.md +71 -0
  136. package/skills/generate-logo/SKILL.md +103 -0
  137. package/skills/generate-rule/SKILL.md +64 -0
  138. package/skills/linter-docs/clippy.md +241 -0
  139. package/skills/linter-docs/eslint.md +384 -0
  140. package/skills/linter-docs/pylint.md +288 -0
  141. package/skills/linter-docs/rubocop.md +277 -0
  142. package/skills/linter-docs/ruff.md +187 -0
  143. package/skills/linter-docs/stylelint.md +247 -0
  144. package/skills/migrate-to-spec/SKILL.md +126 -0
  145. package/skills/pr-to-lint-rule/SKILL.md +97 -0
  146. package/skills/strengthen/SKILL.md +168 -0
  147. package/skills/test-harness/SKILL.md +157 -0
  148. package/dist/action.d.ts +0 -7
  149. package/dist/action.js +0 -180
  150. package/dist/refs.d.ts +0 -44
  151. package/dist/run-hook.d.ts +0 -77
  152. package/dist/run-hook.js +0 -80
  153. package/dist/run-scripts.d.ts +0 -20
  154. package/dist/run-scripts.js +0 -70
  155. /package/dist/{agent-result.js → adapters/claude-code/agent-result.js} +0 -0
  156. /package/dist/{agent-runtime.d.ts → adapters/claude-code/agent-runtime.d.ts} +0 -0
  157. /package/dist/{agent-runtime.js → adapters/claude-code/agent-runtime.js} +0 -0
  158. /package/dist/{judge.d.ts → adapters/claude-code/judge.d.ts} +0 -0
  159. /package/dist/{judge.js → adapters/claude-code/judge.js} +0 -0
  160. /package/dist/{mock-entry.d.ts → adapters/claude-code/mock-entry.d.ts} +0 -0
  161. /package/dist/{mock-entry.js → adapters/claude-code/mock-entry.js} +0 -0
  162. /package/dist/{mock-model.js → adapters/claude-code/mock-model.js} +0 -0
  163. /package/dist/{skill-driver.js → adapters/claude-code/skill-driver.js} +0 -0
  164. /package/dist/{skill-runtime.d.ts → adapters/claude-code/skill-runtime.d.ts} +0 -0
  165. /package/dist/{skill-runtime.js → adapters/claude-code/skill-runtime.js} +0 -0
  166. /package/dist/{stats.d.ts → adapters/claude-code/stats.d.ts} +0 -0
  167. /package/dist/{stats.js → adapters/claude-code/stats.js} +0 -0
  168. /package/dist/{compile-generator.d.ts → core/compile-generator.d.ts} +0 -0
  169. /package/dist/{compile-generator.js → core/compile-generator.js} +0 -0
  170. /package/dist/{coverage.d.ts → core/coverage.d.ts} +0 -0
  171. /package/dist/{coverage.js → core/coverage.js} +0 -0
  172. /package/dist/{doc-refs.d.ts → core/doc-refs.d.ts} +0 -0
  173. /package/dist/{doc-refs.js → core/doc-refs.js} +0 -0
  174. /package/dist/{evolve.d.ts → core/evolve.d.ts} +0 -0
  175. /package/dist/{evolve.js → core/evolve.js} +0 -0
  176. /package/dist/{frontmatter.d.ts → core/frontmatter.d.ts} +0 -0
  177. /package/dist/{frontmatter.js → core/frontmatter.js} +0 -0
  178. /package/dist/{generate-schema.d.ts → core/generate-schema.d.ts} +0 -0
  179. /package/dist/{generate-schema.js → core/generate-schema.js} +0 -0
  180. /package/dist/{generate-types.d.ts → core/generate-types.d.ts} +0 -0
  181. /package/dist/{generate-types.js → core/generate-types.js} +0 -0
  182. /package/dist/{hash.d.ts → core/hash.d.ts} +0 -0
  183. /package/dist/{hash.js → core/hash.js} +0 -0
  184. /package/dist/{inline.d.ts → core/inline.d.ts} +0 -0
  185. /package/dist/{inline.js → core/inline.js} +0 -0
  186. /package/dist/{integrity.d.ts → core/integrity.d.ts} +0 -0
  187. /package/dist/{integrity.js → core/integrity.js} +0 -0
  188. /package/dist/{linters.d.ts → core/linters.d.ts} +0 -0
  189. /package/dist/{linters.js → core/linters.js} +0 -0
  190. /package/dist/{mcp.d.ts → core/mcp.d.ts} +0 -0
  191. /package/dist/{mcp.js → core/mcp.js} +0 -0
  192. /package/dist/{orphans.d.ts → core/orphans.d.ts} +0 -0
  193. /package/dist/{orphans.js → core/orphans.js} +0 -0
  194. /package/dist/{proofs.d.ts → core/proofs.d.ts} +0 -0
  195. /package/dist/{proofs.js → core/proofs.js} +0 -0
  196. /package/dist/{session.d.ts → core/session.d.ts} +0 -0
  197. /package/dist/{session.js → core/session.js} +0 -0
  198. /package/dist/{sidecar.d.ts → core/sidecar.d.ts} +0 -0
  199. /package/dist/{sidecar.js → core/sidecar.js} +0 -0
  200. /package/dist/{spec.d.ts → core/spec.d.ts} +0 -0
  201. /package/dist/{spec.js → core/spec.js} +0 -0
  202. /package/dist/{symbols.d.ts → core/symbols.d.ts} +0 -0
  203. /package/dist/{symbols.js → core/symbols.js} +0 -0
  204. /package/dist/{test-utils.d.ts → core/test-utils.d.ts} +0 -0
  205. /package/dist/{test-utils.js → core/test-utils.js} +0 -0
  206. /package/dist/{types.js → core/types.js} +0 -0
  207. /package/{.claude-plugin/hooks → hooks}/post-edit.sh +0 -0
  208. /package/{.claude-plugin/hooks → hooks}/pre-edit.sh +0 -0
  209. /package/{.claude-plugin/hooks → hooks}/session-start.sh +0 -0
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BASELINE_VERSION = void 0;
4
+ exports.toBaselineFile = toBaselineFile;
5
+ exports.parseBaselineFile = parseBaselineFile;
6
+ exports.diffReports = diffReports;
7
+ exports.formatBaselineDiff = formatBaselineDiff;
8
+ exports.diffToJUnit = diffToJUnit;
9
+ exports.readBaseline = readBaseline;
10
+ exports.writeBaseline = writeBaseline;
11
+ /**
12
+ * vigiles — eval regression gating (Phase C).
13
+ *
14
+ * The eval tier reports mean ± se per arm; `src/stats.ts` turns a gap into a
15
+ * significance verdict. This module points that machinery at a *committed
16
+ * baseline*: record one run's `EvalReport`s to `.vigiles/eval-baseline.json`,
17
+ * then on a later run flag any arm×metric that moved **significantly in the bad
18
+ * direction** vs. that baseline. "jest snapshots for agent behaviour, with a real
19
+ * noise floor" — a bare pass-rate can't tell a true regression from sampling
20
+ * noise, but a Welch t-test over the two runs' summary stats can.
21
+ *
22
+ * Pure + model-free (the diff/serialize/JUnit are fully unit-tested); the only
23
+ * side effects are the two small fs helpers (`readBaseline` / `writeBaseline`).
24
+ * Reuses `welchTTest` from `src/stats.ts` — the current run is the "arm", the
25
+ * baseline is the "baseline", so `delta = current − baseline`.
26
+ */
27
+ const node_fs_1 = require("node:fs");
28
+ const node_path_1 = require("node:path");
29
+ const stats_js_1 = require("./stats.js");
30
+ /** Bumped only on a breaking change to the on-disk shape. */
31
+ exports.BASELINE_VERSION = 1;
32
+ /** Build a `BaselineFile` envelope from a run's reports (keyed by name). */
33
+ function toBaselineFile(reports, recordedAt = new Date().toISOString()) {
34
+ const byName = {};
35
+ for (const r of reports)
36
+ byName[r.name] = r;
37
+ return { version: exports.BASELINE_VERSION, recordedAt, reports: byName };
38
+ }
39
+ /** Parse + validate a baseline JSON string (throws on a bad version/shape). */
40
+ function parseBaselineFile(json) {
41
+ const data = JSON.parse(json);
42
+ if (typeof data !== "object" || data === null) {
43
+ throw new Error("baseline: expected a JSON object");
44
+ }
45
+ const obj = data;
46
+ if (obj.version !== exports.BASELINE_VERSION) {
47
+ throw new Error(`baseline: unsupported version ${String(obj.version)} (expected ${String(exports.BASELINE_VERSION)})`);
48
+ }
49
+ if (typeof obj.reports !== "object" || obj.reports === null) {
50
+ throw new Error("baseline: missing `reports`");
51
+ }
52
+ return {
53
+ version: exports.BASELINE_VERSION,
54
+ recordedAt: typeof obj.recordedAt === "string" ? obj.recordedAt : "",
55
+ reports: obj.reports,
56
+ };
57
+ }
58
+ /** Classify one comparison given the metric's direction. */
59
+ function classify(cmp, lowerIsBetter) {
60
+ if (!cmp.significant || cmp.delta === 0)
61
+ return "unchanged";
62
+ const improved = lowerIsBetter ? cmp.delta < 0 : cmp.delta > 0;
63
+ return improved ? "improved" : "regressed";
64
+ }
65
+ /** Append a diff entry for every arm×metric common to both reports. */
66
+ function collectReportDiffs(baseline, current, cfg, out) {
67
+ for (const [arm, curArm] of Object.entries(current.arms)) {
68
+ const baseArm = baseline.arms[arm];
69
+ if (!baseArm)
70
+ continue;
71
+ for (const [metric, curStat] of Object.entries(curArm.stats)) {
72
+ const baseStat = baseArm.stats[metric];
73
+ if (!baseStat)
74
+ continue;
75
+ const comparison = (0, stats_js_1.welchTTest)(curStat, baseStat, cfg.alpha);
76
+ out.push({
77
+ report: current.name,
78
+ arm,
79
+ metric,
80
+ status: classify(comparison, cfg.lower.has(metric)),
81
+ comparison,
82
+ });
83
+ }
84
+ }
85
+ }
86
+ /**
87
+ * Diff a current run against a committed baseline. Compares every arm×metric
88
+ * present in both (by report name), flagging a *significant* move in the
89
+ * undesired direction as a regression. Metrics absent from one side are skipped
90
+ * (a new arm/metric is not a regression).
91
+ */
92
+ function diffReports(baseline, current, opts = {}) {
93
+ const cfg = {
94
+ alpha: opts.alpha ?? 0.05,
95
+ lower: new Set(opts.lowerIsBetter ?? []),
96
+ };
97
+ const entries = [];
98
+ for (const cur of current) {
99
+ const base = baseline.reports[cur.name];
100
+ if (base)
101
+ collectReportDiffs(base, cur, cfg, entries);
102
+ }
103
+ const regressions = entries.filter((e) => e.status === "regressed");
104
+ const improvements = entries.filter((e) => e.status === "improved");
105
+ return {
106
+ entries,
107
+ regressions,
108
+ improvements,
109
+ passed: regressions.length === 0,
110
+ };
111
+ }
112
+ const STATUS_MARK = {
113
+ regressed: "✗",
114
+ improved: "✓",
115
+ unchanged: "·",
116
+ };
117
+ function formatDelta(c) {
118
+ const sign = c.delta >= 0 ? "+" : "";
119
+ return `Δ=${sign}${c.delta.toFixed(3)} p=${c.pValue.toFixed(3)}`;
120
+ }
121
+ /** Format a baseline diff as a compact console report. */
122
+ function formatBaselineDiff(diff) {
123
+ const head = diff.passed
124
+ ? "baseline OK — no significant regressions"
125
+ : `baseline FAIL — ${String(diff.regressions.length)} regression(s)`;
126
+ const lines = [head];
127
+ for (const e of diff.entries) {
128
+ lines.push(` ${STATUS_MARK[e.status]} ${e.report}/${e.arm}/${e.metric} ${formatDelta(e.comparison)}`);
129
+ }
130
+ return lines.join("\n");
131
+ }
132
+ function xmlEscape(s) {
133
+ return s
134
+ .replaceAll("&", "&amp;")
135
+ .replaceAll("<", "&lt;")
136
+ .replaceAll(">", "&gt;")
137
+ .replaceAll('"', "&quot;");
138
+ }
139
+ function junitCase(e) {
140
+ const name = xmlEscape(`${e.report}.${e.arm}.${e.metric}`);
141
+ const open = ` <testcase classname="${xmlEscape(e.report)}" name="${name}">`;
142
+ if (e.status !== "regressed")
143
+ return `${open}</testcase>`;
144
+ const msg = xmlEscape(`regression: ${formatDelta(e.comparison)}`);
145
+ return `${open}\n <failure message="${msg}"/>\n </testcase>`;
146
+ }
147
+ /**
148
+ * Render a baseline diff as JUnit XML — one `<testcase>` per arm×metric, a
149
+ * `<failure>` for each regression. Lets a CI provider show eval regressions in
150
+ * the same place as unit-test failures.
151
+ */
152
+ function diffToJUnit(diff) {
153
+ const cases = diff.entries.map(junitCase).join("\n");
154
+ return [
155
+ '<?xml version="1.0" encoding="UTF-8"?>',
156
+ `<testsuite name="vigiles-eval" tests="${String(diff.entries.length)}" failures="${String(diff.regressions.length)}">`,
157
+ cases,
158
+ "</testsuite>",
159
+ "",
160
+ ].join("\n");
161
+ }
162
+ /** Read + parse a baseline file, or null if it doesn't exist yet. */
163
+ function readBaseline(path) {
164
+ if (!(0, node_fs_1.existsSync)(path))
165
+ return null;
166
+ return parseBaselineFile((0, node_fs_1.readFileSync)(path, "utf-8"));
167
+ }
168
+ /** Write reports as the committed baseline (pretty JSON, parent dirs created). */
169
+ function writeBaseline(path, reports) {
170
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
171
+ (0, node_fs_1.writeFileSync)(path, JSON.stringify(toBaselineFile(reports), null, 2) + "\n");
172
+ }
173
+ //# sourceMappingURL=eval-baseline.js.map
@@ -1,4 +1,4 @@
1
- import { type SHA256Hash } from "./hash.js";
1
+ import { type SHA256Hash } from "../../core/hash.js";
2
2
  import type { RunOut } from "./eval.js";
3
3
  /** Cache behaviour: never touch the cache / read-only / read-and-write. */
4
4
  export type CacheMode = "off" | "read" | "readwrite";
@@ -24,7 +24,7 @@ exports.restoreDir = restoreDir;
24
24
  */
25
25
  const node_fs_1 = require("node:fs");
26
26
  const node_path_1 = require("node:path");
27
- const hash_js_1 = require("./hash.js");
27
+ const hash_js_1 = require("../../core/hash.js");
28
28
  const MAX_SNAPSHOT_FILE_BYTES = 1024 * 1024;
29
29
  const SKIP_DIRS = new Set(["node_modules", ".git"]);
30
30
  /**
@@ -213,6 +213,14 @@ export interface TriggerRateSpec {
213
213
  readonly pluginDir: string;
214
214
  /** The varied prompts to test the trigger against. */
215
215
  readonly prompts: readonly string[];
216
+ /**
217
+ * Optional *irrelevant* prompts the skill should **not** fire on — the
218
+ * precision side of triggering. Firing on these is a false positive (a skill
219
+ * whose description is too broad and hijacks unrelated work). When given, the
220
+ * report adds {@link TriggerRateReport.falsePositiveRate} and
221
+ * {@link TriggerRateReport.precision}; `prompts` alone measures recall only.
222
+ */
223
+ readonly irrelevantPrompts?: readonly string[];
216
224
  /** Did the behaviour fire on this run? e.g. `(t) => skillResolved(t, "x:y")`. */
217
225
  readonly fired: (trace: Trace) => boolean;
218
226
  /** Trials per prompt. Default 1. */
@@ -235,17 +243,33 @@ export interface PromptTriggerStat {
235
243
  readonly rate: number;
236
244
  }
237
245
  export interface TriggerRateReport {
238
- /** Overall fraction of runs in which the behaviour fired (0..1). */
246
+ /** Overall fraction of relevant runs in which the behaviour fired (recall, 0..1). */
239
247
  readonly rate: number;
240
- /** Total runs (prompts × trials). */
248
+ /** Total relevant runs (prompts × trials). */
241
249
  readonly n: number;
242
250
  readonly perPrompt: readonly PromptTriggerStat[];
251
+ /**
252
+ * Fraction of *irrelevant* runs that wrongly fired (lower is better). Present
253
+ * only when {@link TriggerRateSpec.irrelevantPrompts} was given.
254
+ */
255
+ readonly falsePositiveRate?: number;
256
+ /**
257
+ * `relevantFired / (relevantFired + irrelevantFired)` — of all firings, the
258
+ * share on the right prompts. Present only when irrelevant prompts were given
259
+ * AND something fired (undefined when nothing fired at all). Pairs with `rate`
260
+ * (recall) to catch a skill that fires on everything _or_ nothing.
261
+ */
262
+ readonly precision?: number;
263
+ /** Per-prompt stats for the irrelevant set. Present with irrelevant prompts. */
264
+ readonly perIrrelevant?: readonly PromptTriggerStat[];
243
265
  }
244
266
  /**
245
267
  * Trigger-rate orchestration — every prompt × trial via `runner`, the `fired`
246
268
  * predicate evaluated per run and aggregated into an overall + per-prompt rate.
247
- * Exported with an injectable `runner` so the loop is unit-testable without a
248
- * model; `measureTriggerRate` is this with the real agent runner.
269
+ * With `irrelevantPrompts`, also runs the precision side (firing there is a false
270
+ * positive) and adds `falsePositiveRate` + `precision`. Exported with an
271
+ * injectable `runner` so the loop is unit-testable without a model;
272
+ * `measureTriggerRate` is this with the real agent runner.
249
273
  */
250
274
  export declare function measureTriggerRateWith(spec: TriggerRateSpec, runner: AgentRunner): Promise<TriggerRateReport>;
251
275
  /**
@@ -40,6 +40,7 @@ const node_fs_1 = require("node:fs");
40
40
  const node_os_1 = require("node:os");
41
41
  const node_path_1 = require("node:path");
42
42
  const plugin_loader_js_1 = require("./plugin-loader.js");
43
+ const runtime_js_1 = require("./runtime.js");
43
44
  const harness_test_js_1 = require("./harness-test.js");
44
45
  const eval_cache_js_1 = require("./eval-cache.js");
45
46
  function writeFiles(cwd, files) {
@@ -72,7 +73,7 @@ function spawnAgent(a) {
72
73
  "--allowedTools",
73
74
  ...a.tools,
74
75
  ];
75
- const child = (0, node_child_process_1.spawn)("claude", args, {
76
+ const child = (0, node_child_process_1.spawn)(runtime_js_1.claudeCodeRuntime.agentBinary, args, {
76
77
  cwd: a.cwd,
77
78
  env: process.env,
78
79
  stdio: ["ignore", "pipe", "pipe"],
@@ -401,53 +402,78 @@ function formatEvalReport(report) {
401
402
  }
402
403
  return lines.join("\n");
403
404
  }
404
- /**
405
- * Trigger-rate orchestration — every prompt × trial via `runner`, the `fired`
406
- * predicate evaluated per run and aggregated into an overall + per-prompt rate.
407
- * Exported with an injectable `runner` so the loop is unit-testable without a
408
- * model; `measureTriggerRate` is this with the real agent runner.
409
- */
410
- async function measureTriggerRateWith(spec, runner) {
411
- const trials = spec.trials ?? 1;
412
- const model = spec.model ?? "haiku";
413
- const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash", "Skill"];
414
- const timeoutMs = spec.timeoutMs ?? 240000;
415
- const spacing = (spec.spacingSec ?? 4) * 1000;
405
+ /** Run one prompt set × trials through `runner`, aggregating fired counts. */
406
+ async function runTriggerSet(prompts, cfg, runner) {
416
407
  const perPrompt = [];
417
408
  let firedTotal = 0;
418
409
  let n = 0;
419
- for (const prompt of spec.prompts) {
410
+ for (const prompt of prompts) {
420
411
  let fired = 0;
421
- for (let t = 0; t < trials; t++) {
412
+ for (let t = 0; t < cfg.trials; t++) {
422
413
  const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-trigger-"));
423
414
  try {
424
415
  const out = await runner({
425
416
  task: prompt,
426
417
  cwd,
427
- model,
428
- tools,
418
+ model: cfg.model,
419
+ tools: cfg.tools,
429
420
  hasSettings: false,
430
- pluginDir: spec.pluginDir,
431
- timeoutMs,
421
+ pluginDir: cfg.pluginDir,
422
+ timeoutMs: cfg.timeoutMs,
432
423
  });
433
- if (spec.fired(makeContext(cwd, out)))
424
+ if (cfg.fired(makeContext(cwd, out)))
434
425
  fired++;
435
426
  }
436
427
  finally {
437
428
  (0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
438
- await sleep(spacing);
429
+ await sleep(cfg.spacing);
439
430
  }
440
431
  }
441
432
  perPrompt.push({
442
433
  prompt,
443
434
  fired,
444
- trials,
445
- rate: trials > 0 ? fired / trials : 0,
435
+ trials: cfg.trials,
436
+ rate: cfg.trials > 0 ? fired / cfg.trials : 0,
446
437
  });
447
438
  firedTotal += fired;
448
- n += trials;
439
+ n += cfg.trials;
449
440
  }
450
- return { rate: n > 0 ? firedTotal / n : 0, n, perPrompt };
441
+ return { perPrompt, fired: firedTotal, n };
442
+ }
443
+ /**
444
+ * Trigger-rate orchestration — every prompt × trial via `runner`, the `fired`
445
+ * predicate evaluated per run and aggregated into an overall + per-prompt rate.
446
+ * With `irrelevantPrompts`, also runs the precision side (firing there is a false
447
+ * positive) and adds `falsePositiveRate` + `precision`. Exported with an
448
+ * injectable `runner` so the loop is unit-testable without a model;
449
+ * `measureTriggerRate` is this with the real agent runner.
450
+ */
451
+ async function measureTriggerRateWith(spec, runner) {
452
+ const cfg = {
453
+ trials: spec.trials ?? 1,
454
+ model: spec.model ?? "haiku",
455
+ tools: spec.allowedTools ?? ["Read", "Edit", "Write", "Bash", "Skill"],
456
+ timeoutMs: spec.timeoutMs ?? 240000,
457
+ spacing: (spec.spacingSec ?? 4) * 1000,
458
+ pluginDir: spec.pluginDir,
459
+ fired: spec.fired,
460
+ };
461
+ const relevant = await runTriggerSet(spec.prompts, cfg, runner);
462
+ const base = {
463
+ rate: relevant.n > 0 ? relevant.fired / relevant.n : 0,
464
+ n: relevant.n,
465
+ perPrompt: relevant.perPrompt,
466
+ };
467
+ if ((spec.irrelevantPrompts?.length ?? 0) === 0)
468
+ return base;
469
+ const irrelevant = await runTriggerSet(spec.irrelevantPrompts ?? [], cfg, runner);
470
+ const fires = relevant.fired + irrelevant.fired;
471
+ return {
472
+ ...base,
473
+ falsePositiveRate: irrelevant.n > 0 ? irrelevant.fired / irrelevant.n : 0,
474
+ precision: fires > 0 ? relevant.fired / fires : undefined,
475
+ perIrrelevant: irrelevant.perPrompt,
476
+ };
451
477
  }
452
478
  /* v8 ignore start -- real claude subprocess; thin wrapper over measureTriggerRateWith */
453
479
  /**
@@ -465,6 +491,16 @@ function formatTriggerRateReport(report) {
465
491
  for (const p of report.perPrompt) {
466
492
  lines.push(` ${p.rate.toFixed(2)} ${p.prompt.slice(0, 60)}`);
467
493
  }
494
+ if (report.falsePositiveRate !== undefined) {
495
+ const fpr = (report.falsePositiveRate * 100).toFixed(0);
496
+ const prec = report.precision === undefined
497
+ ? "n/a"
498
+ : `${(report.precision * 100).toFixed(0)}%`;
499
+ lines.push(`false-positive: ${fpr}% precision: ${prec}`);
500
+ for (const p of report.perIrrelevant ?? []) {
501
+ lines.push(` ${p.rate.toFixed(2)} [irrelevant] ${p.prompt.slice(0, 48)}`);
502
+ }
503
+ }
468
504
  return lines.join("\n");
469
505
  }
470
506
  //# sourceMappingURL=eval.js.map
@@ -1,6 +1,8 @@
1
- import { type ModelTurn, type ModelRequest } from "./mock-model.js";
1
+ import type { HarnessAdapter } from "../../core/adapter.js";
2
+ import type { HarnessTestDriver, ToolCall, HookFire, ParsedRun, ModelTurn, ModelRequest } from "../../core/harness-driver.js";
2
3
  import { type SandboxMode } from "./sandbox.js";
3
- export { scriptModel, type ModelTurn, type ModelRequest, } from "./mock-model.js";
4
+ export { scriptModel } from "./mock-model.js";
5
+ export type { ModelTurn, ModelRequest, ToolCall, HookFire, HarnessTestDriver, } from "../../core/harness-driver.js";
4
6
  export { loadPlugin, resolveHarness } from "./plugin-loader.js";
5
7
  export { decideSandbox, specTrusted, sandboxAvailable, type SandboxMode, } from "./sandbox.js";
6
8
  export interface HarnessTestSpec {
@@ -54,24 +56,6 @@ export interface HarnessTestSpec {
54
56
  */
55
57
  readonly sandbox?: SandboxMode;
56
58
  }
57
- /**
58
- * A hook invocation observed during the run, recorded (not inferred) from the
59
- * `hook_response` system events the CLI emits in the stream — so a test can
60
- * assert which hook fired and whether it blocked, instead of inferring it from a
61
- * marker file the hook had to write.
62
- */
63
- export interface HookFire {
64
- /** The hook label, e.g. `"PreToolUse:Edit"` (`Event:Matcher`). */
65
- readonly name: string;
66
- /** The hook event, e.g. `"PreToolUse"`, `"PostToolUse"`, `"Stop"`. */
67
- readonly event: string;
68
- /** The hook process exit code (2 = block), or undefined if not reported. */
69
- readonly exitCode: number | undefined;
70
- /** Whether the hook blocked / errored (exit ≠ 0 or outcome "error"). */
71
- readonly blocked: boolean;
72
- /** What the hook printed (its block reason / diagnostic), or "". */
73
- readonly output: string;
74
- }
75
59
  /**
76
60
  * The observable record of ONE run — the unified shape produced by BOTH testing
77
61
  * tiers: `runHarnessTest`'s result and `runEval`'s `measure` ctx (`eval.ts`)
@@ -124,15 +108,6 @@ export interface HarnessTestResult extends Trace {
124
108
  /** Remove the temp working dir. */
125
109
  cleanup(): void;
126
110
  }
127
- /** A tool the agent invoked, paired with its result (transcript mode only). */
128
- export interface ToolCall {
129
- readonly name: string;
130
- readonly input: unknown;
131
- /** The tool_result text ("" if none / not captured). */
132
- readonly resultText: string;
133
- /** Whether the tool_result came back flagged as an error. */
134
- readonly isError: boolean;
135
- }
136
111
  /**
137
112
  * Parse `--output-format stream-json` (the `transcript: true` output) into the
138
113
  * tools the agent invoked, each joined to its result by id. Returns [] for the
@@ -155,16 +130,37 @@ export declare function parseHooks(stdout: string): HookFire[];
155
130
  * here. Pure, so the arg shape is unit-tested.
156
131
  */
157
132
  export declare function buildClaudeArgs(spec: HarnessTestSpec, hasSettings: boolean): string[];
158
- /** Whether the `claude` CLI is available harness tests need it. */
133
+ /** Parse the `claude` stdout/stream into the unified trace fields. */
134
+ export declare function parseClaudeRun(stdout: string): ParsedRun;
135
+ /** Whether the agent CLI is available — harness tests need it. */
159
136
  export declare function claudeAvailable(): boolean;
160
137
  /**
161
- * Run the real `claude` CLI against a scripted mock model, with the given
162
- * fixture and settings (hooks). Deterministic same script, same result.
138
+ * The Claude Code `HarnessTestDriver`: the existing argv/mock/parse seams bundled
139
+ * behind the port the adapter-driven runner dispatches through. Behaviourally
140
+ * identical to the previous hard-wired path.
141
+ */
142
+ export declare const claudeCodeDriver: HarnessTestDriver;
143
+ /** Options for {@link runHarnessTest}. */
144
+ export interface RunHarnessTestOptions {
145
+ /**
146
+ * Which harness to drive. Defaults to Claude Code. Pass `codexAdapter`
147
+ * (`vigiles/codex`) to drive real `codex exec` against its Responses mock.
148
+ * The adapter must support pillar 2 (`capabilities.harnessTesting`) and carry
149
+ * a `harnessTestDriver`.
150
+ */
151
+ readonly adapter?: HarnessAdapter;
152
+ }
153
+ /**
154
+ * Run the real agent CLI against a scripted mock model, with the given fixture
155
+ * and settings (hooks). Deterministic — same script, same result. Adapter-driven
156
+ * (`opts.adapter`, default Claude Code): the Claude Code path is unchanged
157
+ * (incl. the safe-by-default sandbox); pass `codexAdapter` to drive real codex.
163
158
  *
164
- * Safe by default: an external `plugin` / `pluginDir` brings in untrusted
165
- * third-party hooks and is confined under bubblewrap (`spec.sandbox`, default
166
- * `"auto"`); if no sandbox is available the run REFUSES rather than executing
167
- * unconfined. See `src/sandbox.ts`.
159
+ * Safe by default (Claude Code): an external `plugin` / `pluginDir` brings in
160
+ * untrusted third-party hooks and is confined under bubblewrap (`spec.sandbox`,
161
+ * default `"auto"`); if no sandbox is available the run REFUSES rather than
162
+ * executing unconfined. See `src/sandbox.ts`. The sandbox path is Claude Code
163
+ * only — requesting confinement for another harness throws.
168
164
  */
169
- export declare function runHarnessTest(spec: HarnessTestSpec): Promise<HarnessTestResult>;
165
+ export declare function runHarnessTest(spec: HarnessTestSpec, opts?: RunHarnessTestOptions): Promise<HarnessTestResult>;
170
166
  //# sourceMappingURL=harness-test.d.ts.map