@post-print/agent-test 0.3.4 → 0.3.5

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 (102) hide show
  1. package/README.md +122 -43
  2. package/dist/check.d.ts +38 -0
  3. package/dist/check.d.ts.map +1 -0
  4. package/dist/check.js +169 -0
  5. package/dist/check.js.map +1 -0
  6. package/dist/cli.d.ts +11 -7
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +327 -318
  9. package/dist/cli.js.map +1 -1
  10. package/dist/compare-scenario.d.ts +33 -0
  11. package/dist/compare-scenario.d.ts.map +1 -0
  12. package/dist/compare-scenario.js +128 -0
  13. package/dist/compare-scenario.js.map +1 -0
  14. package/dist/debug-bundle.d.ts +2 -1
  15. package/dist/debug-bundle.d.ts.map +1 -1
  16. package/dist/debug-bundle.js +12 -5
  17. package/dist/debug-bundle.js.map +1 -1
  18. package/dist/doctor.d.ts +6 -1
  19. package/dist/doctor.d.ts.map +1 -1
  20. package/dist/doctor.js +133 -11
  21. package/dist/doctor.js.map +1 -1
  22. package/dist/expect.d.ts +6 -5
  23. package/dist/expect.d.ts.map +1 -1
  24. package/dist/expect.js +67 -57
  25. package/dist/expect.js.map +1 -1
  26. package/dist/host-log.d.ts +17 -0
  27. package/dist/host-log.d.ts.map +1 -0
  28. package/dist/host-log.js +154 -0
  29. package/dist/host-log.js.map +1 -0
  30. package/dist/hosts.d.ts +22 -0
  31. package/dist/hosts.d.ts.map +1 -0
  32. package/dist/hosts.js +84 -0
  33. package/dist/hosts.js.map +1 -0
  34. package/dist/html-report.d.ts +0 -13
  35. package/dist/html-report.d.ts.map +1 -1
  36. package/dist/html-report.js +381 -318
  37. package/dist/html-report.js.map +1 -1
  38. package/dist/index.d.ts +14 -7
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +12 -6
  41. package/dist/index.js.map +1 -1
  42. package/dist/live-isolation.d.ts +11 -4
  43. package/dist/live-isolation.d.ts.map +1 -1
  44. package/dist/live-isolation.js +31 -10
  45. package/dist/live-isolation.js.map +1 -1
  46. package/dist/load-adapters.d.ts +13 -0
  47. package/dist/load-adapters.d.ts.map +1 -0
  48. package/dist/load-adapters.js +76 -0
  49. package/dist/load-adapters.js.map +1 -0
  50. package/dist/load-suite.d.ts.map +1 -1
  51. package/dist/load-suite.js +6 -2
  52. package/dist/load-suite.js.map +1 -1
  53. package/dist/mcp-config.d.ts +8 -0
  54. package/dist/mcp-config.d.ts.map +1 -1
  55. package/dist/mcp-config.js +46 -0
  56. package/dist/mcp-config.js.map +1 -1
  57. package/dist/progress.d.ts +7 -0
  58. package/dist/progress.d.ts.map +1 -1
  59. package/dist/progress.js +39 -9
  60. package/dist/progress.js.map +1 -1
  61. package/dist/record-trace.d.ts +9 -7
  62. package/dist/record-trace.d.ts.map +1 -1
  63. package/dist/record-trace.js +5 -16
  64. package/dist/record-trace.js.map +1 -1
  65. package/dist/report-preview-server.d.ts +2 -0
  66. package/dist/report-preview-server.d.ts.map +1 -0
  67. package/dist/report-preview-server.js +24 -0
  68. package/dist/report-preview-server.js.map +1 -0
  69. package/dist/report-preview.d.ts +11 -0
  70. package/dist/report-preview.d.ts.map +1 -0
  71. package/dist/report-preview.js +161 -0
  72. package/dist/report-preview.js.map +1 -0
  73. package/dist/run-suite.d.ts +34 -1
  74. package/dist/run-suite.d.ts.map +1 -1
  75. package/dist/run-suite.js +551 -82
  76. package/dist/run-suite.js.map +1 -1
  77. package/dist/scenario-story.d.ts +41 -0
  78. package/dist/scenario-story.d.ts.map +1 -0
  79. package/dist/scenario-story.js +198 -0
  80. package/dist/scenario-story.js.map +1 -0
  81. package/dist/suite-summary.d.ts +2 -0
  82. package/dist/suite-summary.d.ts.map +1 -1
  83. package/dist/suite-summary.js +70 -22
  84. package/dist/suite-summary.js.map +1 -1
  85. package/dist/theme.d.ts +29 -5
  86. package/dist/theme.d.ts.map +1 -1
  87. package/dist/theme.js +204 -45
  88. package/dist/theme.js.map +1 -1
  89. package/dist/types.d.ts +97 -11
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/validate-seeds.d.ts.map +1 -1
  92. package/dist/validate-seeds.js +21 -6
  93. package/dist/validate-seeds.js.map +1 -1
  94. package/dist/validate-suite.d.ts +3 -1
  95. package/dist/validate-suite.d.ts.map +1 -1
  96. package/dist/validate-suite.js +258 -23
  97. package/dist/validate-suite.js.map +1 -1
  98. package/package.json +4 -4
  99. package/dist/compare.d.ts +0 -79
  100. package/dist/compare.d.ts.map +0 -1
  101. package/dist/compare.js +0 -232
  102. package/dist/compare.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,30 +1,96 @@
1
1
  #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
- import { access, mkdir, writeFile } from "node:fs/promises";
3
2
  import { join, resolve } from "node:path";
4
3
  import { fileURLToPath } from "node:url";
5
- import { CLAUDE_AUTH_MODE_ENV, cleanupStaleScenarioWorktrees, isPathUnderRoot, parseClaudeAuthMode, } from "@post-print/agent-harness";
4
+ import { cleanupStaleScenarioWorktrees, isPathUnderRoot, knownAgentHosts, missingClassifierAuth, } from "@post-print/agent-harness";
5
+ import { formatCheckReport, formatCheckSummary, missingHostsAuth, runCheck } from "./check.js";
6
6
  import { isCliMain } from "./cli-entry.js";
7
- import { compareSuiteReports, labelForCompareSide, loadSuiteRunReport, parseComparePairToken, writeCompareReport, } from "./compare.js";
8
- import { discoverSuites } from "./discover-suites.js";
9
- import { runDoctor } from "./doctor.js";
7
+ import { missingAgentAuth } from "./doctor.js";
8
+ import { installHostLogFilter } from "./host-log.js";
9
+ import { parseHostList, uniqueHosts } from "./hosts.js";
10
10
  import { writeHtmlReport } from "./html-report.js";
11
+ import { loadHostAdapters } from "./load-adapters.js";
11
12
  import { assertDirectAgentPreflight } from "./preflight.js";
12
13
  import { logProgress } from "./progress.js";
13
14
  import { cleanupStagingSession, createLiveStagingSessionId, getLiveStagingSessionRoot, setLiveStagingRootOverride, } from "./record-trace.js";
14
- import { registerLiveRunHandlers, runAllSuites, runSuite } from "./run-suite.js";
15
+ import { shouldStartReportPreview, startDetachedReportPreview } from "./report-preview.js";
16
+ import { registerLiveRunHandlers, runAllSuites, selectedRunNeedsJudge } from "./run-suite.js";
15
17
  import { formatRunSummary, shouldFailScenario, summarizeReports, } from "./suite-summary.js";
16
18
  import { configureCliColor, theme } from "./theme.js";
17
- import { formatSeedValidationReport, validateSeedPatches } from "./validate-seeds.js";
18
- import { formatValidationReport, validateSuitePaths } from "./validate-suite.js";
19
19
  import { suppressNoisyRuntimeWarnings } from "./warnings.js";
20
20
  suppressNoisyRuntimeWarnings();
21
21
  configureCliColor();
22
+ function splitArgvFlag(token) {
23
+ if (!token.startsWith("-") || token === "-") {
24
+ return undefined;
25
+ }
26
+ if (token.startsWith("--")) {
27
+ const eq = token.indexOf("=");
28
+ if (eq === -1) {
29
+ return { flag: token };
30
+ }
31
+ return { flag: token.slice(0, eq), inline: token.slice(eq + 1) };
32
+ }
33
+ return { flag: token };
34
+ }
35
+ function readFlagValue(flag, inline, argv, index) {
36
+ if (inline !== undefined) {
37
+ if (inline.length === 0) {
38
+ throw new Error(`${flag} requires a value`);
39
+ }
40
+ return { value: inline, nextIndex: index };
41
+ }
42
+ const next = argv[index + 1];
43
+ if (!next || next.startsWith("--") || /^-[A-Za-z]/.test(next)) {
44
+ throw new Error(`${flag} requires a value`);
45
+ }
46
+ return { value: next, nextIndex: index + 1 };
47
+ }
48
+ function enableCheckMode(target) {
49
+ target.check = true;
50
+ target.validateOnly = true;
51
+ target.validatePaths = true;
52
+ target.validateSeeds = true;
53
+ }
54
+ /** Usage text for `--help`. */
55
+ export function formatHelp() {
56
+ return [
57
+ "agent-test [options] [suite]",
58
+ "",
59
+ "Launch a host agent and score the transcript.",
60
+ "A run checks the suite and host first.",
61
+ "",
62
+ " --check Check the suite and host. Do not launch an agent.",
63
+ " --help, -h Print this help.",
64
+ "",
65
+ " --host cursor|claude|openai|all Host adapter (default: suite or cursor).",
66
+ " Repeat or comma-separate for a matrix.",
67
+ " --adapter <module> Load a consumer host adapter module.",
68
+ " --suites-dir <path> Suite root (default: agent-suites)",
69
+ " --suite <name> Run one suite",
70
+ " --scenario <name> Run one scenario",
71
+ " --no-judge Skip the judge",
72
+ " --fail-on all|behavior|infra-only",
73
+ " --allow-user-input Let a user agent answer AskQuestion tools",
74
+ " --timeout-ms <n> Agent deadline",
75
+ " --no-timeout Disable the deadline",
76
+ " --scenario-retries <n> Announce-stop retries",
77
+ " --debug Keep recordings and write a debug bundle",
78
+ " --debug-dir <path> Debug session parent directory",
79
+ " --report-out <path> HTML report file or directory",
80
+ " --no-html-report Skip the HTML report",
81
+ " --no-worktree Run in the caller checkout (needs AGENT_TEST_ALLOW_IN_PLACE=1)",
82
+ "",
83
+ "--doctor, --validate-only, --validate-paths, and --validate-seeds run --check.",
84
+ ].join("\n");
85
+ }
22
86
  /** Parse agent-test CLI argv (exported for unit tests). */
23
87
  export function parseCliArgs(argv) {
24
88
  const cwd = process.cwd();
25
89
  let suitesDir = "agent-suites";
26
90
  let rubricsDir;
27
- let host;
91
+ const parsedHosts = [];
92
+ let hostAll = false;
93
+ const adapterModules = [];
28
94
  let filter;
29
95
  let scenarioFilter;
30
96
  let stagingSessionId;
@@ -34,6 +100,8 @@ export function parseCliArgs(argv) {
34
100
  let timeoutMs;
35
101
  let noTimeout = false;
36
102
  let allowUserInput = false;
103
+ let check = false;
104
+ let help = false;
37
105
  let doctor = false;
38
106
  let htmlReport = true;
39
107
  let reportOut;
@@ -44,141 +112,162 @@ export function parseCliArgs(argv) {
44
112
  let validatePaths = false;
45
113
  let failOn = "all";
46
114
  let scenarioRetries;
47
- let compareMode = false;
48
- let compareA;
49
- let compareB;
50
- let comparePairs;
51
- let compareOutDir;
52
- const startIndex = argv[2] === "compare" ? 3 : 2;
53
115
  if (argv[2] === "compare") {
54
- compareMode = true;
116
+ throw new Error("The compare subcommand is removed. Add compare.a and compare.b on one scenario. Set compare.faster or compare.cheaper when one arm must win on time or tokens. Add rubric.judge only when you want a pairwise judge.");
55
117
  }
118
+ const startIndex = 2;
119
+ const checkMode = {
120
+ check: false,
121
+ validateOnly: false,
122
+ validatePaths: false,
123
+ validateSeeds: false,
124
+ };
56
125
  for (let i = startIndex; i < argv.length; i++) {
57
126
  const token = argv[i];
58
- if (token === "--host" && argv[i + 1]) {
59
- const value = argv[++i];
60
- if (value === "replay") {
61
- throw new Error("Replay-based testing is deprecated and no longer supported; use --host cursor or --host claude.");
62
- }
63
- if (value !== "cursor" && value !== "claude") {
64
- throw new Error("--host must be cursor|claude");
65
- }
66
- host = value;
67
- }
68
- else if (token === "--suites-dir" && argv[i + 1]) {
69
- suitesDir = argv[++i];
70
- }
71
- else if (token === "--rubrics-dir" && argv[i + 1]) {
72
- rubricsDir = argv[++i];
73
- }
74
- else if (token === "--suite" && argv[i + 1]) {
75
- filter = argv[++i];
76
- }
77
- else if (token === "--scenario" && argv[i + 1]) {
78
- scenarioFilter = argv[++i];
127
+ if (!token) {
128
+ continue;
79
129
  }
80
- else if (token === "--staging-session-id" && argv[i + 1]) {
81
- stagingSessionId = argv[++i];
82
- }
83
- else if (token === "--live") {
84
- throw new Error("--live was removed because agent-test now always runs a real agent");
85
- }
86
- else if (token === "--record") {
87
- throw new Error("--record was removed; direct runs capture transient traces automatically (use --keep-recordings to retain them)");
88
- }
89
- else if (token === "--record-fixtures") {
90
- throw new Error("--record-fixtures was removed because replay-based testing is deprecated and no longer supported");
91
- }
92
- else if (token === "--keep-recordings") {
93
- keepRecordings = true;
94
- }
95
- else if (token === "--judge") {
96
- judge = true;
97
- }
98
- else if (token === "--no-judge") {
99
- judge = false;
100
- }
101
- else if (token === "--no-worktree") {
102
- worktree = false;
103
- }
104
- else if (token === "--timeout-ms" && argv[i + 1]) {
105
- const parsed = Number(argv[++i]);
106
- if (Number.isFinite(parsed) && parsed > 0) {
107
- timeoutMs = parsed;
130
+ const parsed = splitArgvFlag(token);
131
+ if (!parsed) {
132
+ filter = token;
133
+ continue;
134
+ }
135
+ const { flag, inline } = parsed;
136
+ const read = () => {
137
+ const result = readFlagValue(flag, inline, argv, i);
138
+ i = result.nextIndex;
139
+ return result.value;
140
+ };
141
+ switch (flag) {
142
+ case "--help":
143
+ case "-h":
144
+ help = true;
145
+ break;
146
+ case "--check":
147
+ enableCheckMode(checkMode);
148
+ break;
149
+ case "--doctor":
150
+ doctor = true;
151
+ enableCheckMode(checkMode);
152
+ break;
153
+ case "--validate-only":
154
+ enableCheckMode(checkMode);
155
+ break;
156
+ case "--validate-seeds":
157
+ enableCheckMode(checkMode);
158
+ break;
159
+ case "--validate-paths":
160
+ enableCheckMode(checkMode);
161
+ break;
162
+ case "--host": {
163
+ const value = read();
164
+ if (value
165
+ .split(",")
166
+ .map((token) => token.trim().toLowerCase())
167
+ .includes("all")) {
168
+ hostAll = true;
169
+ }
170
+ parsedHosts.push(...parseHostList(value));
171
+ break;
108
172
  }
109
- }
110
- else if (token === "--no-timeout") {
111
- noTimeout = true;
112
- }
113
- else if (token === "--allow-user-input") {
114
- allowUserInput = true;
115
- }
116
- else if (token === "--doctor") {
117
- doctor = true;
118
- }
119
- else if (token === "--validate-only") {
120
- validateOnly = true;
121
- }
122
- else if (token === "--validate-seeds") {
123
- validateSeeds = true;
124
- }
125
- else if (token === "--validate-paths") {
126
- validatePaths = true;
127
- }
128
- else if (token === "--fail-on" && argv[i + 1]) {
129
- const mode = argv[++i];
130
- if (mode !== "all" && mode !== "behavior" && mode !== "infra-only") {
131
- throw new Error("--fail-on must be all|behavior|infra-only");
173
+ case "--adapter":
174
+ adapterModules.push(resolve(cwd, read()));
175
+ break;
176
+ case "--suites-dir":
177
+ suitesDir = read();
178
+ break;
179
+ case "--rubrics-dir":
180
+ rubricsDir = read();
181
+ break;
182
+ case "--suite":
183
+ filter = read();
184
+ break;
185
+ case "--scenario":
186
+ scenarioFilter = read();
187
+ break;
188
+ case "--staging-session-id":
189
+ stagingSessionId = read();
190
+ break;
191
+ case "--live":
192
+ throw new Error("--live was removed because agent-test now always runs a real agent");
193
+ case "--record":
194
+ throw new Error("--record was removed; direct runs capture transient traces automatically (use --keep-recordings to retain them)");
195
+ case "--record-fixtures":
196
+ throw new Error("--record-fixtures was removed because replay-based testing is deprecated and no longer supported");
197
+ case "--keep-recordings":
198
+ keepRecordings = true;
199
+ break;
200
+ case "--judge":
201
+ judge = true;
202
+ break;
203
+ case "--no-judge":
204
+ judge = false;
205
+ break;
206
+ case "--no-worktree":
207
+ worktree = false;
208
+ break;
209
+ case "--timeout-ms": {
210
+ const parsedTimeout = Number(read());
211
+ if (Number.isFinite(parsedTimeout) && parsedTimeout > 0) {
212
+ timeoutMs = parsedTimeout;
213
+ }
214
+ break;
132
215
  }
133
- failOn = mode;
134
- }
135
- else if (token === "--scenario-retries" && argv[i + 1]) {
136
- const parsed = Number(argv[++i]);
137
- if (!Number.isInteger(parsed) || parsed < 0) {
138
- throw new Error("--scenario-retries must be an integer >= 0");
216
+ case "--no-timeout":
217
+ noTimeout = true;
218
+ break;
219
+ case "--allow-user-input":
220
+ allowUserInput = true;
221
+ break;
222
+ case "--fail-on": {
223
+ const mode = read();
224
+ if (mode !== "all" && mode !== "behavior" && mode !== "infra-only") {
225
+ throw new Error("--fail-on must be all|behavior|infra-only");
226
+ }
227
+ failOn = mode;
228
+ break;
139
229
  }
140
- scenarioRetries = parsed;
141
- }
142
- else if (token === "--compare-pairs" && argv[i + 1]) {
143
- comparePairs = argv[++i];
144
- }
145
- else if ((token === "--a" || token === "--compare-a") && argv[i + 1]) {
146
- compareA = argv[++i];
147
- compareMode = true;
148
- }
149
- else if ((token === "--b" || token === "--compare-b") && argv[i + 1]) {
150
- compareB = argv[++i];
151
- compareMode = true;
152
- }
153
- else if ((token === "--out-dir" || token === "--compare-out") && argv[i + 1]) {
154
- compareOutDir = argv[++i];
155
- }
156
- else if (token === "--report-out" && argv[i + 1]) {
157
- reportOut = argv[++i];
158
- }
159
- else if (token === "--no-html-report") {
160
- htmlReport = false;
161
- }
162
- else if (token === "--debug") {
163
- debug = true;
164
- }
165
- else if (token === "--debug-dir") {
166
- const value = argv[++i];
167
- if (!value || value.startsWith("-")) {
168
- throw new Error("--debug-dir requires a non-empty path argument");
230
+ case "--scenario-retries": {
231
+ const parsedRetries = Number(read());
232
+ if (!Number.isInteger(parsedRetries) || parsedRetries < 0) {
233
+ throw new Error("--scenario-retries must be an integer >= 0");
234
+ }
235
+ scenarioRetries = parsedRetries;
236
+ break;
169
237
  }
170
- debugDir = value;
171
- debug = true;
172
- }
173
- else if (token && !token.startsWith("-")) {
174
- filter = token;
175
- }
176
- }
177
- if (compareMode && !comparePairs) {
178
- if (!compareA || !compareB) {
179
- throw new Error("compare requires --a <report.json> and --b <report.json>");
238
+ case "--compare-pairs":
239
+ case "--a":
240
+ case "--b":
241
+ case "--compare-a":
242
+ case "--compare-b":
243
+ case "--out-dir":
244
+ case "--compare-out":
245
+ throw new Error(`${flag} is removed. Add compare.a and compare.b on one scenario. Set compare.faster or compare.cheaper when one arm must win on time or tokens. Add rubric.judge only when you want a pairwise judge.`);
246
+ case "--report-out":
247
+ reportOut = read();
248
+ break;
249
+ case "--no-html-report":
250
+ htmlReport = false;
251
+ break;
252
+ case "--debug":
253
+ debug = true;
254
+ break;
255
+ case "--debug-dir": {
256
+ const value = read();
257
+ debugDir = value;
258
+ debug = true;
259
+ break;
260
+ }
261
+ default:
262
+ throw new Error(`Unknown flag: ${flag}`);
180
263
  }
181
264
  }
265
+ check = checkMode.check;
266
+ validateOnly = checkMode.validateOnly;
267
+ validatePaths = checkMode.validatePaths;
268
+ validateSeeds = checkMode.validateSeeds;
269
+ const hosts = uniqueHosts(parsedHosts);
270
+ const host = hosts?.[0];
182
271
  judge = judge ?? true;
183
272
  worktree = worktree ?? true;
184
273
  if (debug) {
@@ -192,6 +281,9 @@ export function parseCliArgs(argv) {
192
281
  suitesDir,
193
282
  rubricsDir,
194
283
  host,
284
+ hosts,
285
+ hostAll,
286
+ adapterModules: adapterModules.length > 0 ? adapterModules : undefined,
195
287
  filter,
196
288
  scenarioFilter,
197
289
  stagingSessionId,
@@ -201,6 +293,8 @@ export function parseCliArgs(argv) {
201
293
  timeoutMs: noTimeout ? 0 : timeoutMs,
202
294
  noTimeout,
203
295
  allowUserInput,
296
+ check,
297
+ help,
204
298
  doctor,
205
299
  htmlReport,
206
300
  reportOut: reportOut ? resolve(cwd, reportOut) : undefined,
@@ -211,11 +305,6 @@ export function parseCliArgs(argv) {
211
305
  validatePaths,
212
306
  failOn,
213
307
  scenarioRetries,
214
- compareMode,
215
- compareA,
216
- compareB,
217
- comparePairs,
218
- compareOutDir: compareOutDir ? resolve(cwd, compareOutDir) : undefined,
219
308
  };
220
309
  }
221
310
  /** Resolve an explicit report target into the HTML path and optional artifact directory. */
@@ -228,62 +317,14 @@ export function resolveReportOutput(reportOut) {
228
317
  }
229
318
  return { htmlPath: join(reportOut, "report.html"), outDir: reportOut };
230
319
  }
231
- async function pathExists(path) {
232
- try {
233
- await access(path);
234
- return true;
235
- }
236
- catch {
237
- return false;
238
- }
239
- }
240
- async function resolveSuiteScenariosPath(cwd, suitesDir, side) {
241
- const candidates = [
242
- resolve(cwd, side, "scenarios.json"),
243
- resolve(cwd, suitesDir, side, "scenarios.json"),
244
- resolve(side, "scenarios.json"),
245
- ];
246
- for (const candidate of candidates) {
247
- if (await pathExists(candidate)) {
248
- return candidate;
249
- }
250
- }
251
- throw new Error(`compare side "${side}" is not a suite report JSON or suite dir (looked under ${suitesDir}/ and cwd)`);
252
- }
253
- async function loadOrRunCompareSide(args, side, stagingSessionId) {
254
- const resolved = resolve(args.cwd, side);
255
- if (side.endsWith(".json") || resolved.endsWith(".json")) {
256
- const jsonPath = side.endsWith(".json") ? resolved : resolve(args.cwd, `${side}.json`);
257
- const path = (await pathExists(resolved)) ? resolved : jsonPath;
258
- if (!(await pathExists(path))) {
259
- throw new Error(`compare report not found: ${side}`);
320
+ async function htmlReportTip(label, reportPath) {
321
+ if (shouldStartReportPreview()) {
322
+ const url = await startDetachedReportPreview(reportPath);
323
+ if (url) {
324
+ return theme.fileTip(label, url);
260
325
  }
261
- return loadSuiteRunReport(path);
262
326
  }
263
- const suitePath = await resolveSuiteScenariosPath(args.cwd, args.suitesDir, side);
264
- return runSuite({
265
- cwd: args.cwd,
266
- suitePath,
267
- host: args.host,
268
- scenarioFilter: args.scenarioFilter,
269
- judge: args.judge,
270
- worktree: args.worktree,
271
- stagingSessionId,
272
- keepRecordings: args.keepRecordings,
273
- suitesDir: args.suitesDir,
274
- rubricsDir: args.rubricsDir,
275
- timeoutMs: args.timeoutMs,
276
- allowUserInput: args.allowUserInput,
277
- debug: args.debug,
278
- debugDir: args.debugDir,
279
- scenarioRetries: args.scenarioRetries,
280
- });
281
- }
282
- async function writeSuiteReportDump(outDir, label, report) {
283
- await mkdir(outDir, { recursive: true });
284
- const path = join(outDir, `${label}.suite-report.json`);
285
- await writeFile(path, `${JSON.stringify(report, null, 2)}\n`, "utf8");
286
- return path;
327
+ return theme.fileTip(label, reportPath);
287
328
  }
288
329
  async function cleanupRunArtifacts(stagingSessionRoot, keepRecordings) {
289
330
  if (keepRecordings) {
@@ -302,65 +343,38 @@ async function main() {
302
343
  let args;
303
344
  try {
304
345
  args = parseCliArgs(process.argv);
346
+ installHostLogFilter();
305
347
  }
306
348
  catch (error) {
307
349
  console.error(error instanceof Error ? error.message : error);
308
350
  return 1;
309
351
  }
310
- if (args.doctor) {
311
- const report = runDoctor();
312
- for (const message of report.messages) {
313
- console.log(message);
352
+ if (args.help) {
353
+ console.log(formatHelp());
354
+ return 0;
355
+ }
356
+ try {
357
+ await loadHostAdapters({ cwd: args.cwd, adapterModules: args.adapterModules });
358
+ if (args.hostAll) {
359
+ args.hosts = uniqueHosts(knownAgentHosts());
360
+ args.host = args.hosts?.[0];
314
361
  }
315
- return report.ok ? 0 : 1;
316
362
  }
317
- if (args.compareMode && !args.comparePairs) {
318
- const aPath = resolve(args.cwd, args.compareA);
319
- const bPath = resolve(args.cwd, args.compareB);
320
- const aReport = await loadSuiteRunReport(aPath);
321
- const bReport = await loadSuiteRunReport(bPath);
322
- const outDir = args.compareOutDir ?? resolve(args.cwd, "compare-out");
323
- const compare = compareSuiteReports({
324
- aLabel: labelForCompareSide(aPath),
325
- bLabel: labelForCompareSide(bPath),
326
- a: aReport,
327
- b: bReport,
328
- });
329
- const written = await writeCompareReport({ outDir, report: compare });
330
- console.log(theme.tip(`compare JSON: ${written.jsonPath}`));
331
- console.log(theme.tip(`compare markdown: ${written.markdownPath}`));
332
- console.log(theme.fileTip("compare HTML", written.htmlPath));
333
- return compare.summary.passRegressions > 0 ? 1 : 0;
363
+ catch (error) {
364
+ console.error(error instanceof Error ? error.message : error);
365
+ return 1;
334
366
  }
335
- if (args.validateOnly || args.validateSeeds) {
336
- if (args.validateOnly) {
337
- const suitePaths = await discoverSuites(resolve(args.cwd, args.suitesDir));
338
- const filtered = args.filter
339
- ? suitePaths.filter((path) => path.includes(`/${args.filter}/`) || path.endsWith(`/${args.filter}/scenarios.json`))
340
- : suitePaths;
341
- const report = await validateSuitePaths(filtered, {
342
- validatePaths: args.validatePaths,
343
- repoRoot: args.cwd,
344
- rubricsDir: args.rubricsDir,
345
- });
346
- console.log(formatValidationReport(report));
347
- if (!report.ok) {
348
- return 1;
349
- }
350
- }
351
- if (args.validateSeeds) {
352
- const report = await validateSeedPatches({
353
- cwd: args.cwd,
354
- suitesDir: args.suitesDir,
355
- rubricsDir: args.rubricsDir,
356
- filter: args.filter,
357
- });
358
- console.log(formatSeedValidationReport(report));
359
- if (!report.ok) {
360
- return 1;
361
- }
362
- }
363
- return 0;
367
+ if (args.check || args.doctor || args.validateOnly || args.validateSeeds) {
368
+ const report = await runCheck({
369
+ cwd: args.cwd,
370
+ suitesDir: args.suitesDir,
371
+ filter: args.filter,
372
+ rubricsDir: args.rubricsDir,
373
+ host: args.host,
374
+ hosts: args.hosts,
375
+ });
376
+ console.log(formatCheckReport(report));
377
+ return report.ok ? 0 : 1;
364
378
  }
365
379
  if (args.debugDir) {
366
380
  setLiveStagingRootOverride(args.debugDir);
@@ -377,27 +391,59 @@ async function main() {
377
391
  ? getLiveStagingSessionRoot(stagingSessionId)
378
392
  : undefined;
379
393
  try {
380
- if (args.host === "claude") {
381
- try {
382
- const authMode = parseClaudeAuthMode(process.env[CLAUDE_AUTH_MODE_ENV]);
383
- if (authMode === "api-key" && !process.env.ANTHROPIC_API_KEY?.trim()) {
384
- console.error(`${CLAUDE_AUTH_MODE_ENV}=api-key requires ANTHROPIC_API_KEY`);
385
- return 1;
386
- }
394
+ let checkHosts = args.hosts ?? (args.host ? [args.host] : ["cursor"]);
395
+ if (!isChild) {
396
+ const report = await runCheck({
397
+ cwd: args.cwd,
398
+ suitesDir: args.suitesDir,
399
+ filter: args.filter,
400
+ rubricsDir: args.rubricsDir,
401
+ host: args.host,
402
+ hosts: args.hosts,
403
+ });
404
+ console.log(theme.tip(formatCheckSummary(report)));
405
+ if (!report.ok) {
406
+ console.error(formatCheckReport(report));
407
+ return 1;
387
408
  }
388
- catch (error) {
389
- console.error(error instanceof Error ? error.message : error);
409
+ checkHosts = report.hosts;
410
+ const missingHost = missingHostsAuth(report.hosts);
411
+ if (missingHost) {
412
+ console.error(missingHost);
390
413
  return 1;
391
414
  }
392
415
  }
393
- if (args.host === "cursor" && !process.env.CURSOR_API_KEY?.trim()) {
394
- console.error("CURSOR_API_KEY required for Cursor agent runs");
395
- return 1;
416
+ else {
417
+ const missingHost = missingAgentAuth(args.host ?? "cursor");
418
+ if (missingHost) {
419
+ console.error(missingHost);
420
+ return 1;
421
+ }
396
422
  }
397
- // Judge classifiers still use the Cursor SDK.
398
- if (args.judge !== false && !process.env.CURSOR_API_KEY?.trim()) {
399
- console.error("CURSOR_API_KEY required for judge classifiers (use --no-judge to skip)");
400
- return 1;
423
+ if (args.judge !== false) {
424
+ let needsJudge = false;
425
+ try {
426
+ needsJudge = await selectedRunNeedsJudge({
427
+ cwd: args.cwd,
428
+ suitesDir: args.suitesDir,
429
+ filter: args.filter,
430
+ scenarioFilter: args.scenarioFilter,
431
+ rubricsDir: args.rubricsDir,
432
+ judge: args.judge,
433
+ });
434
+ }
435
+ catch {
436
+ needsJudge = false;
437
+ }
438
+ if (needsJudge) {
439
+ for (const host of checkHosts) {
440
+ const missingJudge = missingClassifierAuth(host);
441
+ if (missingJudge) {
442
+ console.error(`${missingJudge} (use --no-judge to skip)`);
443
+ return 1;
444
+ }
445
+ }
446
+ }
401
447
  }
402
448
  {
403
449
  try {
@@ -412,7 +458,7 @@ async function main() {
412
458
  process.env.AGENT_TEST_NO_WORKTREE === "1" ||
413
459
  process.env.AGENT_TEST_NO_WORKTREE === "true";
414
460
  if (worktreeDisabled && !inPlaceAllowed) {
415
- console.error("Direct agent tests require git worktree isolation. Set AGENT_TEST_ALLOW_IN_PLACE=1 to run in repo cwd (--no-worktree leaks agent edits into your working tree).");
461
+ console.error("Direct agent tests require a sealed temp workspace. Set AGENT_TEST_ALLOW_IN_PLACE=1 to run in repo cwd (--no-worktree leaks agent edits into your working tree).");
416
462
  return 1;
417
463
  }
418
464
  registerLiveRunHandlers();
@@ -425,61 +471,28 @@ async function main() {
425
471
  if (stagingSessionRoot) {
426
472
  console.log(theme.bannerSession(stagingSessionRoot));
427
473
  }
428
- console.log(` ${theme.tip("Ctrl+C cancels in-flight scenarios")}`);
429
- if (worktreeDisabled) {
430
- console.warn(theme.warn("running in repo cwd — agent file edits will persist in your working tree"));
431
- }
474
+ const hints = ["Ctrl+C cancels"];
432
475
  if (args.debug || process.env.AGENT_TEST_VERBOSE === "1") {
433
- if (!args.keepRecordings) {
434
- console.log(` ${theme.tip("traces removed on exit unless --keep-recordings")}`);
435
- }
436
- else {
437
- console.log(` ${theme.tip("debug: recordings kept")}`);
438
- }
439
- console.log(` ${theme.tip("exit 137 = macOS OOM — isolated subprocesses (AGENT_TEST_NO_ISOLATE=1 to disable)")}`);
476
+ hints.push(args.keepRecordings ? "recordings kept" : "traces removed on exit");
477
+ hints.push("exit 137 = macOS OOM");
440
478
  }
441
479
  else if (!args.keepRecordings) {
442
- console.log(` ${theme.tip("traces removed on exit unless --keep-recordings")}`);
480
+ hints.push("traces removed on exit");
481
+ }
482
+ console.log(theme.bannerHints(hints));
483
+ if (worktreeDisabled) {
484
+ console.warn(theme.warn("running in repo cwd — agent file edits will persist in your working tree"));
443
485
  }
444
486
  }
445
487
  }
446
- let reports;
447
- let comparePassRegressions = 0;
448
- if (args.comparePairs) {
449
- const { a, b } = parseComparePairToken(args.comparePairs);
450
- const aReport = await loadOrRunCompareSide(args, a, stagingSessionId);
451
- const bReport = await loadOrRunCompareSide(args, b, stagingSessionId);
452
- reports = [aReport, bReport];
453
- if (!isChild) {
454
- const outDir = args.compareOutDir ??
455
- (stagingSessionRoot
456
- ? join(stagingSessionRoot, "compare")
457
- : resolve(args.cwd, "compare-out"));
458
- await writeSuiteReportDump(outDir, labelForCompareSide(a), aReport);
459
- await writeSuiteReportDump(outDir, labelForCompareSide(b), bReport);
460
- const compare = compareSuiteReports({
461
- aLabel: labelForCompareSide(a),
462
- bLabel: labelForCompareSide(b),
463
- a: aReport,
464
- b: bReport,
465
- });
466
- comparePassRegressions = compare.summary.passRegressions;
467
- const written = await writeCompareReport({ outDir, report: compare });
468
- console.log(`\n${theme.tip(`compare JSON: ${written.jsonPath}`)}`);
469
- console.log(theme.tip(`compare markdown: ${written.markdownPath}`));
470
- console.log(theme.fileTip("compare HTML", written.htmlPath));
471
- }
472
- }
473
- else {
474
- reports = await runAllSuites({
475
- ...args,
476
- stagingSessionId,
477
- timeoutMs: args.timeoutMs,
478
- allowUserInput: args.allowUserInput,
479
- debug: args.debug,
480
- debugDir: args.debugDir,
481
- });
482
- }
488
+ const reports = await runAllSuites({
489
+ ...args,
490
+ stagingSessionId,
491
+ timeoutMs: args.timeoutMs,
492
+ allowUserInput: args.allowUserInput,
493
+ debug: args.debug,
494
+ debugDir: args.debugDir,
495
+ });
483
496
  let exitCode = 0;
484
497
  if (!isChild) {
485
498
  for (const report of reports) {
@@ -505,7 +518,10 @@ async function main() {
505
518
  console.log(theme.summary(report.suite, report.passed, report.failed, report.skipped));
506
519
  }
507
520
  const runSummary = summarizeReports(reports);
508
- console.log(`\n${formatRunSummary(runSummary)}`);
521
+ const runSummaryText = formatRunSummary(runSummary);
522
+ if (runSummaryText) {
523
+ console.log(`\n${theme.runSummary(runSummaryText)}`);
524
+ }
509
525
  if (args.failOn === "behavior" && runSummary.infraFailures > 0 && exitCode === 0) {
510
526
  console.log(theme.tip(`${runSummary.infraFailures} infra failure(s) ignored (--fail-on=behavior)`));
511
527
  }
@@ -514,23 +530,16 @@ async function main() {
514
530
  }
515
531
  if (args.htmlReport && reports.length > 0) {
516
532
  try {
517
- const pair = args.comparePairs ? parseComparePairToken(args.comparePairs) : undefined;
518
533
  const reportPath = await writeHtmlReport(reports, {
519
534
  host: args.host,
520
535
  suitesDir: args.suitesDir,
521
- includeCompare: Boolean(args.comparePairs),
522
- compareALabel: pair ? labelForCompareSide(pair.a) : undefined,
523
- compareBLabel: pair ? labelForCompareSide(pair.b) : undefined,
524
536
  });
525
- console.log(`\n${theme.fileTip("HTML report", reportPath)}`);
537
+ console.log(`\n${await htmlReportTip("HTML report", reportPath)}`);
526
538
  }
527
539
  catch (error) {
528
540
  console.warn(theme.warn(`HTML report failed: ${error instanceof Error ? error.message : String(error)}`));
529
541
  }
530
542
  }
531
- if (comparePassRegressions > 0) {
532
- exitCode = 1;
533
- }
534
543
  }
535
544
  else {
536
545
  for (const report of reports) {