@post-print/agent-test 0.3.3 → 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 (106) hide show
  1. package/README.md +126 -157
  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 +12 -14
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +342 -366
  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 -2
  15. package/dist/debug-bundle.d.ts.map +1 -1
  16. package/dist/debug-bundle.js +12 -12
  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 +136 -14
  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 +15 -8
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +13 -7
  41. package/dist/index.js.map +1 -1
  42. package/dist/live-isolation.d.ts +11 -5
  43. package/dist/live-isolation.d.ts.map +1 -1
  44. package/dist/live-isolation.js +32 -14
  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 +16 -0
  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/preflight.d.ts +2 -2
  58. package/dist/preflight.d.ts.map +1 -1
  59. package/dist/preflight.js +4 -4
  60. package/dist/preflight.js.map +1 -1
  61. package/dist/progress.d.ts +7 -0
  62. package/dist/progress.d.ts.map +1 -1
  63. package/dist/progress.js +39 -9
  64. package/dist/progress.js.map +1 -1
  65. package/dist/record-trace.d.ts +11 -20
  66. package/dist/record-trace.d.ts.map +1 -1
  67. package/dist/record-trace.js +10 -57
  68. package/dist/record-trace.js.map +1 -1
  69. package/dist/report-preview-server.d.ts +2 -0
  70. package/dist/report-preview-server.d.ts.map +1 -0
  71. package/dist/report-preview-server.js +24 -0
  72. package/dist/report-preview-server.js.map +1 -0
  73. package/dist/report-preview.d.ts +11 -0
  74. package/dist/report-preview.d.ts.map +1 -0
  75. package/dist/report-preview.js +161 -0
  76. package/dist/report-preview.js.map +1 -0
  77. package/dist/run-suite.d.ts +62 -11
  78. package/dist/run-suite.d.ts.map +1 -1
  79. package/dist/run-suite.js +697 -206
  80. package/dist/run-suite.js.map +1 -1
  81. package/dist/scenario-story.d.ts +41 -0
  82. package/dist/scenario-story.d.ts.map +1 -0
  83. package/dist/scenario-story.js +198 -0
  84. package/dist/scenario-story.js.map +1 -0
  85. package/dist/suite-summary.d.ts +2 -0
  86. package/dist/suite-summary.d.ts.map +1 -1
  87. package/dist/suite-summary.js +70 -22
  88. package/dist/suite-summary.js.map +1 -1
  89. package/dist/theme.d.ts +29 -5
  90. package/dist/theme.d.ts.map +1 -1
  91. package/dist/theme.js +204 -45
  92. package/dist/theme.js.map +1 -1
  93. package/dist/types.d.ts +107 -24
  94. package/dist/types.d.ts.map +1 -1
  95. package/dist/validate-seeds.d.ts.map +1 -1
  96. package/dist/validate-seeds.js +21 -6
  97. package/dist/validate-seeds.js.map +1 -1
  98. package/dist/validate-suite.d.ts +3 -1
  99. package/dist/validate-suite.d.ts.map +1 -1
  100. package/dist/validate-suite.js +256 -22
  101. package/dist/validate-suite.js.map +1 -1
  102. package/package.json +5 -5
  103. package/dist/compare.d.ts +0 -79
  104. package/dist/compare.d.ts.map +0 -1
  105. package/dist/compare.js +0 -232
  106. package/dist/compare.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,42 +1,107 @@
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 { assertLiveDogfoodPreflight } from "./preflight.js";
11
+ import { loadHostAdapters } from "./load-adapters.js";
12
+ import { assertDirectAgentPreflight } from "./preflight.js";
12
13
  import { logProgress } from "./progress.js";
13
- import { cleanupLegacyRepoRecordings, cleanupStagingSession, createLiveStagingSessionId, getLiveStagingSessionRoot, setLiveStagingRootOverride, } from "./record-trace.js";
14
- import { registerLiveRunHandlers, runAllSuites, runSuite } from "./run-suite.js";
14
+ import { cleanupStagingSession, createLiveStagingSessionId, getLiveStagingSessionRoot, setLiveStagingRootOverride, } from "./record-trace.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;
31
- let record = false;
32
- let recordFixtures = false;
33
- let live = false;
34
97
  let judge;
35
98
  let worktree;
36
99
  let keepRecordings = false;
37
100
  let timeoutMs;
38
101
  let noTimeout = false;
39
102
  let allowUserInput = false;
103
+ let check = false;
104
+ let help = false;
40
105
  let doctor = false;
41
106
  let htmlReport = true;
42
107
  let reportOut;
@@ -47,141 +112,164 @@ export function parseCliArgs(argv) {
47
112
  let validatePaths = false;
48
113
  let failOn = "all";
49
114
  let scenarioRetries;
50
- let compareMode = false;
51
- let compareA;
52
- let compareB;
53
- let comparePairs;
54
- let compareOutDir;
55
- const startIndex = argv[2] === "compare" ? 3 : 2;
56
115
  if (argv[2] === "compare") {
57
- 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.");
58
117
  }
118
+ const startIndex = 2;
119
+ const checkMode = {
120
+ check: false,
121
+ validateOnly: false,
122
+ validatePaths: false,
123
+ validateSeeds: false,
124
+ };
59
125
  for (let i = startIndex; i < argv.length; i++) {
60
126
  const token = argv[i];
61
- if (token === "--host" && argv[i + 1]) {
62
- host = argv[++i];
63
- }
64
- else if (token === "--suites-dir" && argv[i + 1]) {
65
- suitesDir = argv[++i];
66
- }
67
- else if (token === "--rubrics-dir" && argv[i + 1]) {
68
- rubricsDir = argv[++i];
69
- }
70
- else if (token === "--suite" && argv[i + 1]) {
71
- filter = argv[++i];
72
- }
73
- else if (token === "--scenario" && argv[i + 1]) {
74
- scenarioFilter = argv[++i];
75
- }
76
- else if (token === "--staging-session-id" && argv[i + 1]) {
77
- stagingSessionId = argv[++i];
78
- }
79
- else if (token === "--record") {
80
- record = true;
81
- }
82
- else if (token === "--record-fixtures") {
83
- record = true;
84
- recordFixtures = true;
127
+ if (!token) {
128
+ continue;
85
129
  }
86
- else if (token === "--live") {
87
- live = true;
88
- }
89
- else if (token === "--keep-recordings") {
90
- keepRecordings = true;
91
- }
92
- else if (token === "--judge") {
93
- judge = true;
94
- }
95
- else if (token === "--no-judge") {
96
- judge = false;
97
- }
98
- else if (token === "--no-worktree") {
99
- worktree = false;
100
- }
101
- else if (token === "--timeout-ms" && argv[i + 1]) {
102
- const parsed = Number(argv[++i]);
103
- if (Number.isFinite(parsed) && parsed > 0) {
104
- 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;
105
172
  }
106
- }
107
- else if (token === "--no-timeout") {
108
- noTimeout = true;
109
- }
110
- else if (token === "--allow-user-input") {
111
- allowUserInput = true;
112
- }
113
- else if (token === "--doctor") {
114
- doctor = true;
115
- }
116
- else if (token === "--validate-only") {
117
- validateOnly = true;
118
- }
119
- else if (token === "--validate-seeds") {
120
- validateSeeds = true;
121
- }
122
- else if (token === "--validate-paths") {
123
- validatePaths = true;
124
- }
125
- else if (token === "--fail-on" && argv[i + 1]) {
126
- const mode = argv[++i];
127
- if (mode !== "all" && mode !== "behavior" && mode !== "infra-only") {
128
- 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;
129
215
  }
130
- failOn = mode;
131
- }
132
- else if (token === "--scenario-retries" && argv[i + 1]) {
133
- const parsed = Number(argv[++i]);
134
- if (!Number.isInteger(parsed) || parsed < 0) {
135
- 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;
136
229
  }
137
- scenarioRetries = parsed;
138
- }
139
- else if (token === "--compare-pairs" && argv[i + 1]) {
140
- comparePairs = argv[++i];
141
- }
142
- else if ((token === "--a" || token === "--compare-a") && argv[i + 1]) {
143
- compareA = argv[++i];
144
- compareMode = true;
145
- }
146
- else if ((token === "--b" || token === "--compare-b") && argv[i + 1]) {
147
- compareB = argv[++i];
148
- compareMode = true;
149
- }
150
- else if ((token === "--out-dir" || token === "--compare-out") && argv[i + 1]) {
151
- compareOutDir = argv[++i];
152
- }
153
- else if (token === "--report-out" && argv[i + 1]) {
154
- reportOut = argv[++i];
155
- }
156
- else if (token === "--no-html-report") {
157
- htmlReport = false;
158
- }
159
- else if (token === "--debug") {
160
- debug = true;
161
- }
162
- else if (token === "--debug-dir") {
163
- const value = argv[++i];
164
- if (!value || value.startsWith("-")) {
165
- 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;
166
237
  }
167
- debugDir = value;
168
- debug = true;
169
- }
170
- else if (token && !token.startsWith("-")) {
171
- filter = token;
172
- }
173
- }
174
- if (compareMode && !comparePairs) {
175
- if (!compareA || !compareB) {
176
- 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}`);
177
263
  }
178
264
  }
179
- if (live) {
180
- host = host ?? "cursor";
181
- record = true;
182
- judge = judge ?? true;
183
- worktree = worktree ?? true;
184
- }
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];
271
+ judge = judge ?? true;
272
+ worktree = worktree ?? true;
185
273
  if (debug) {
186
274
  keepRecordings = true;
187
275
  process.env.AGENT_TEST_DEBUG = "1";
@@ -193,18 +281,20 @@ export function parseCliArgs(argv) {
193
281
  suitesDir,
194
282
  rubricsDir,
195
283
  host,
284
+ hosts,
285
+ hostAll,
286
+ adapterModules: adapterModules.length > 0 ? adapterModules : undefined,
196
287
  filter,
197
288
  scenarioFilter,
198
289
  stagingSessionId,
199
- record,
200
- recordFixtures,
201
- live,
202
290
  judge,
203
291
  worktree,
204
292
  keepRecordings,
205
293
  timeoutMs: noTimeout ? 0 : timeoutMs,
206
294
  noTimeout,
207
295
  allowUserInput,
296
+ check,
297
+ help,
208
298
  doctor,
209
299
  htmlReport,
210
300
  reportOut: reportOut ? resolve(cwd, reportOut) : undefined,
@@ -215,70 +305,9 @@ export function parseCliArgs(argv) {
215
305
  validatePaths,
216
306
  failOn,
217
307
  scenarioRetries,
218
- compareMode,
219
- compareA,
220
- compareB,
221
- comparePairs,
222
- compareOutDir: compareOutDir ? resolve(cwd, compareOutDir) : undefined,
223
308
  };
224
309
  }
225
- async function pathExists(path) {
226
- try {
227
- await access(path);
228
- return true;
229
- }
230
- catch {
231
- return false;
232
- }
233
- }
234
- async function resolveSuiteScenariosPath(cwd, suitesDir, side) {
235
- const candidates = [
236
- resolve(cwd, side, "scenarios.json"),
237
- resolve(cwd, suitesDir, side, "scenarios.json"),
238
- resolve(side, "scenarios.json"),
239
- ];
240
- for (const candidate of candidates) {
241
- if (await pathExists(candidate)) {
242
- return candidate;
243
- }
244
- }
245
- throw new Error(`compare side "${side}" is not a suite report JSON or suite dir (looked under ${suitesDir}/ and cwd)`);
246
- }
247
- async function loadOrRunCompareSide(args, side, stagingSessionId) {
248
- const resolved = resolve(args.cwd, side);
249
- if (side.endsWith(".json") || resolved.endsWith(".json")) {
250
- const jsonPath = side.endsWith(".json") ? resolved : resolve(args.cwd, `${side}.json`);
251
- const path = (await pathExists(resolved)) ? resolved : jsonPath;
252
- if (!(await pathExists(path))) {
253
- throw new Error(`compare report not found: ${side}`);
254
- }
255
- return loadSuiteRunReport(path);
256
- }
257
- const suitePath = await resolveSuiteScenariosPath(args.cwd, args.suitesDir, side);
258
- return runSuite({
259
- cwd: args.cwd,
260
- suitePath,
261
- host: args.host,
262
- scenarioFilter: args.scenarioFilter,
263
- record: args.record,
264
- recordFixtures: args.recordFixtures,
265
- judge: args.judge,
266
- worktree: args.worktree,
267
- stagingSessionId,
268
- keepRecordings: args.keepRecordings,
269
- suitesDir: args.suitesDir,
270
- rubricsDir: args.rubricsDir,
271
- timeoutMs: args.timeoutMs,
272
- allowUserInput: args.allowUserInput,
273
- debug: args.debug,
274
- debugDir: args.debugDir,
275
- scenarioRetries: args.scenarioRetries,
276
- });
277
- }
278
- /**
279
- * Split `--report-out` into an HTML file path and, when a directory was given,
280
- * the directory every other report artifact is written to.
281
- */
310
+ /** Resolve an explicit report target into the HTML path and optional artifact directory. */
282
311
  export function resolveReportOutput(reportOut) {
283
312
  if (!reportOut) {
284
313
  return {};
@@ -288,13 +317,16 @@ export function resolveReportOutput(reportOut) {
288
317
  }
289
318
  return { htmlPath: join(reportOut, "report.html"), outDir: reportOut };
290
319
  }
291
- async function writeSuiteReportDump(outDir, label, report) {
292
- await mkdir(outDir, { recursive: true });
293
- const path = join(outDir, `${label}.suite-report.json`);
294
- await writeFile(path, `${JSON.stringify(report, null, 2)}\n`, "utf8");
295
- return path;
320
+ async function htmlReportTip(label, reportPath) {
321
+ if (shouldStartReportPreview()) {
322
+ const url = await startDetachedReportPreview(reportPath);
323
+ if (url) {
324
+ return theme.fileTip(label, url);
325
+ }
326
+ }
327
+ return theme.fileTip(label, reportPath);
296
328
  }
297
- async function cleanupLiveRunArtifacts(cwd, stagingSessionRoot, keepRecordings) {
329
+ async function cleanupRunArtifacts(stagingSessionRoot, keepRecordings) {
298
330
  if (keepRecordings) {
299
331
  return;
300
332
  }
@@ -306,123 +338,116 @@ async function cleanupLiveRunArtifacts(cwd, stagingSessionRoot, keepRecordings)
306
338
  // best-effort
307
339
  }
308
340
  }
309
- const legacyRemoved = await cleanupLegacyRepoRecordings(cwd);
310
- if (legacyRemoved.length > 0) {
311
- console.log(`Removed legacy in-repo recording dir(s):\n ${legacyRemoved.join("\n ")}`);
312
- }
313
341
  }
314
342
  async function main() {
315
343
  let args;
316
344
  try {
317
345
  args = parseCliArgs(process.argv);
346
+ installHostLogFilter();
318
347
  }
319
348
  catch (error) {
320
349
  console.error(error instanceof Error ? error.message : error);
321
350
  return 1;
322
351
  }
323
- if (args.doctor) {
324
- const report = runDoctor();
325
- for (const message of report.messages) {
326
- 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];
327
361
  }
328
- return report.ok ? 0 : 1;
329
362
  }
330
- if (args.compareMode && !args.comparePairs) {
331
- const aPath = resolve(args.cwd, args.compareA);
332
- const bPath = resolve(args.cwd, args.compareB);
333
- const aReport = await loadSuiteRunReport(aPath);
334
- const bReport = await loadSuiteRunReport(bPath);
335
- const outDir = args.compareOutDir ??
336
- resolveReportOutput(args.reportOut).outDir ??
337
- resolve(args.cwd, "compare-out");
338
- const compare = compareSuiteReports({
339
- aLabel: labelForCompareSide(aPath),
340
- bLabel: labelForCompareSide(bPath),
341
- a: aReport,
342
- b: bReport,
343
- });
344
- const written = await writeCompareReport({ outDir, report: compare });
345
- console.log(theme.tip(`compare JSON: ${written.jsonPath}`));
346
- console.log(theme.tip(`compare markdown: ${written.markdownPath}`));
347
- console.log(theme.fileTip("compare HTML", written.htmlPath));
348
- return compare.summary.passRegressions > 0 ? 1 : 0;
363
+ catch (error) {
364
+ console.error(error instanceof Error ? error.message : error);
365
+ return 1;
349
366
  }
350
- if (args.validateOnly || args.validateSeeds) {
351
- if (args.validateOnly) {
352
- const suitePaths = await discoverSuites(resolve(args.cwd, args.suitesDir));
353
- const filtered = args.filter
354
- ? suitePaths.filter((path) => path.includes(`/${args.filter}/`) || path.endsWith(`/${args.filter}/scenarios.json`))
355
- : suitePaths;
356
- const report = await validateSuitePaths(filtered, {
357
- validatePaths: args.validatePaths,
358
- repoRoot: args.cwd,
359
- rubricsDir: args.rubricsDir,
360
- });
361
- console.log(formatValidationReport(report));
362
- if (!report.ok) {
363
- return 1;
364
- }
365
- }
366
- if (args.validateSeeds) {
367
- const report = await validateSeedPatches({
368
- cwd: args.cwd,
369
- suitesDir: args.suitesDir,
370
- rubricsDir: args.rubricsDir,
371
- filter: args.filter,
372
- });
373
- console.log(formatSeedValidationReport(report));
374
- if (!report.ok) {
375
- return 1;
376
- }
377
- }
378
- 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;
379
378
  }
380
379
  if (args.debugDir) {
381
380
  setLiveStagingRootOverride(args.debugDir);
382
381
  }
383
382
  const isChild = process.env.AGENT_TEST_CHILD === "1";
384
383
  if (args.debugDir && isPathUnderRoot(args.debugDir, args.cwd) && !isChild) {
385
- console.warn(theme.warn(`--debug-dir is inside the repo (${args.debugDir}). Default is $TMPDIR/agent-spec — prefer that for live runs so debug output stays out of git status.`));
384
+ console.warn(theme.warn(`--debug-dir is inside the repo (${args.debugDir}). Default is $TMPDIR/agent-spec — prefer that for direct runs so debug output stays out of git status.`));
386
385
  }
387
386
  const verbose = args.debug || process.env.AGENT_TEST_VERBOSE === "1" || process.env.AGENT_TEST_DEBUG === "1";
388
387
  const stagingSessionId = args.stagingSessionId?.trim() ||
389
388
  process.env.AGENT_TEST_STAGING_SESSION_ID?.trim() ||
390
- (args.live || args.debug || (args.record && !args.recordFixtures)
391
- ? createLiveStagingSessionId()
392
- : undefined);
389
+ createLiveStagingSessionId();
393
390
  const stagingSessionRoot = stagingSessionId
394
391
  ? getLiveStagingSessionRoot(stagingSessionId)
395
392
  : undefined;
396
- const reportOutput = resolveReportOutput(args.reportOut);
397
393
  try {
398
- if (args.live) {
399
- const host = args.host ?? "cursor";
400
- if (host === "claude") {
401
- try {
402
- const authMode = parseClaudeAuthMode(process.env[CLAUDE_AUTH_MODE_ENV]);
403
- if (authMode === "api-key" && !process.env.ANTHROPIC_API_KEY?.trim()) {
404
- console.error(`${CLAUDE_AUTH_MODE_ENV}=api-key requires ANTHROPIC_API_KEY for --live --host claude`);
405
- return 1;
406
- }
407
- }
408
- catch (error) {
409
- console.error(error instanceof Error ? error.message : error);
410
- return 1;
411
- }
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;
412
408
  }
413
- if (host === "cursor" && !process.env.CURSOR_API_KEY?.trim()) {
414
- console.error("CURSOR_API_KEY required for --live (Cursor SDK runs)");
409
+ checkHosts = report.hosts;
410
+ const missingHost = missingHostsAuth(report.hosts);
411
+ if (missingHost) {
412
+ console.error(missingHost);
415
413
  return 1;
416
414
  }
417
- // Judge classifiers still use the Cursor SDK.
418
- if (args.judge !== false && !process.env.CURSOR_API_KEY?.trim()) {
419
- console.error("CURSOR_API_KEY required for live judge classifiers (use --no-judge to skip)");
415
+ }
416
+ else {
417
+ const missingHost = missingAgentAuth(args.host ?? "cursor");
418
+ if (missingHost) {
419
+ console.error(missingHost);
420
420
  return 1;
421
421
  }
422
422
  }
423
- if (args.live) {
423
+ if (args.judge !== false) {
424
+ let needsJudge = false;
424
425
  try {
425
- await assertLiveDogfoodPreflight(args.cwd, args.suitesDir);
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
+ }
447
+ }
448
+ {
449
+ try {
450
+ await assertDirectAgentPreflight(args.cwd, args.suitesDir);
426
451
  }
427
452
  catch (error) {
428
453
  console.error(error instanceof Error ? error.message : error);
@@ -433,7 +458,7 @@ async function main() {
433
458
  process.env.AGENT_TEST_NO_WORKTREE === "1" ||
434
459
  process.env.AGENT_TEST_NO_WORKTREE === "true";
435
460
  if (worktreeDisabled && !inPlaceAllowed) {
436
- console.error("Live dogfood requires 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).");
437
462
  return 1;
438
463
  }
439
464
  registerLiveRunHandlers();
@@ -442,70 +467,32 @@ async function main() {
442
467
  if (removed.length > 0) {
443
468
  console.log(theme.warn(`Cleaned ${removed.length} stale agent-test worktree(s) from a prior crash`));
444
469
  }
445
- console.log(theme.banner(args.debug ? "live debug" : "live"));
470
+ console.log(theme.banner(args.debug ? "direct debug" : "direct"));
446
471
  if (stagingSessionRoot) {
447
472
  console.log(theme.bannerSession(stagingSessionRoot));
448
473
  }
449
- console.log(` ${theme.tip("Ctrl+C cancels in-flight scenarios")}`);
450
- if (worktreeDisabled) {
451
- console.warn(theme.warn("running in repo cwd — agent file edits will persist in your working tree"));
452
- }
474
+ const hints = ["Ctrl+C cancels"];
453
475
  if (args.debug || process.env.AGENT_TEST_VERBOSE === "1") {
454
- if (!args.keepRecordings) {
455
- console.log(` ${theme.tip("traces removed on exit unless --keep-recordings")}`);
456
- }
457
- else {
458
- console.log(` ${theme.tip("debug: recordings kept")}`);
459
- }
460
- 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");
461
478
  }
462
479
  else if (!args.keepRecordings) {
463
- 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"));
464
485
  }
465
486
  }
466
487
  }
467
- else if (args.debug && !isChild && stagingSessionRoot) {
468
- console.log(theme.banner("debug"));
469
- console.log(theme.bannerSession(stagingSessionRoot));
470
- }
471
- let reports;
472
- let comparePassRegressions = 0;
473
- if (args.comparePairs) {
474
- const { a, b } = parseComparePairToken(args.comparePairs);
475
- const aReport = await loadOrRunCompareSide(args, a, stagingSessionId);
476
- const bReport = await loadOrRunCompareSide(args, b, stagingSessionId);
477
- reports = [aReport, bReport];
478
- if (!isChild) {
479
- const outDir = args.compareOutDir ??
480
- reportOutput.outDir ??
481
- (stagingSessionRoot
482
- ? join(stagingSessionRoot, "compare")
483
- : resolve(args.cwd, "compare-out"));
484
- await writeSuiteReportDump(outDir, labelForCompareSide(a), aReport);
485
- await writeSuiteReportDump(outDir, labelForCompareSide(b), bReport);
486
- const compare = compareSuiteReports({
487
- aLabel: labelForCompareSide(a),
488
- bLabel: labelForCompareSide(b),
489
- a: aReport,
490
- b: bReport,
491
- });
492
- comparePassRegressions = compare.summary.passRegressions;
493
- const written = await writeCompareReport({ outDir, report: compare });
494
- console.log(`\n${theme.tip(`compare JSON: ${written.jsonPath}`)}`);
495
- console.log(theme.tip(`compare markdown: ${written.markdownPath}`));
496
- console.log(theme.fileTip("compare HTML", written.htmlPath));
497
- }
498
- }
499
- else {
500
- reports = await runAllSuites({
501
- ...args,
502
- stagingSessionId,
503
- timeoutMs: args.timeoutMs,
504
- allowUserInput: args.allowUserInput,
505
- debug: args.debug,
506
- debugDir: args.debugDir,
507
- });
508
- }
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
+ });
509
496
  let exitCode = 0;
510
497
  if (!isChild) {
511
498
  for (const report of reports) {
@@ -531,7 +518,10 @@ async function main() {
531
518
  console.log(theme.summary(report.suite, report.passed, report.failed, report.skipped));
532
519
  }
533
520
  const runSummary = summarizeReports(reports);
534
- console.log(`\n${formatRunSummary(runSummary)}`);
521
+ const runSummaryText = formatRunSummary(runSummary);
522
+ if (runSummaryText) {
523
+ console.log(`\n${theme.runSummary(runSummaryText)}`);
524
+ }
535
525
  if (args.failOn === "behavior" && runSummary.infraFailures > 0 && exitCode === 0) {
536
526
  console.log(theme.tip(`${runSummary.infraFailures} infra failure(s) ignored (--fail-on=behavior)`));
537
527
  }
@@ -540,30 +530,16 @@ async function main() {
540
530
  }
541
531
  if (args.htmlReport && reports.length > 0) {
542
532
  try {
543
- const pair = args.comparePairs ? parseComparePairToken(args.comparePairs) : undefined;
544
533
  const reportPath = await writeHtmlReport(reports, {
545
534
  host: args.host,
546
535
  suitesDir: args.suitesDir,
547
- includeCompare: Boolean(args.comparePairs),
548
- compareALabel: pair ? labelForCompareSide(pair.a) : undefined,
549
- compareBLabel: pair ? labelForCompareSide(pair.b) : undefined,
550
- }, reportOutput.htmlPath);
551
- console.log(`\n${theme.fileTip("HTML report", reportPath)}`);
552
- // A directory collects everything, not just the HTML.
553
- if (reportOutput.outDir && !args.comparePairs) {
554
- for (const report of reports) {
555
- const dumpPath = await writeSuiteReportDump(reportOutput.outDir, report.suite, report);
556
- console.log(theme.tip(`suite JSON: ${dumpPath}`));
557
- }
558
- }
536
+ });
537
+ console.log(`\n${await htmlReportTip("HTML report", reportPath)}`);
559
538
  }
560
539
  catch (error) {
561
540
  console.warn(theme.warn(`HTML report failed: ${error instanceof Error ? error.message : String(error)}`));
562
541
  }
563
542
  }
564
- if (comparePassRegressions > 0) {
565
- exitCode = 1;
566
- }
567
543
  }
568
544
  else {
569
545
  for (const report of reports) {
@@ -581,7 +557,7 @@ async function main() {
581
557
  }
582
558
  finally {
583
559
  if (stagingSessionId && !isChild) {
584
- await cleanupLiveRunArtifacts(args.cwd, stagingSessionRoot, args.keepRecordings);
560
+ await cleanupRunArtifacts(stagingSessionRoot, args.keepRecordings);
585
561
  }
586
562
  }
587
563
  }