@post-print/agent-test 0.3.2 → 0.3.4

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 (49) hide show
  1. package/README.md +63 -159
  2. package/dist/cli.d.ts +8 -4
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +59 -54
  5. package/dist/cli.js.map +1 -1
  6. package/dist/debug-bundle.d.ts +0 -1
  7. package/dist/debug-bundle.d.ts.map +1 -1
  8. package/dist/debug-bundle.js +1 -8
  9. package/dist/debug-bundle.js.map +1 -1
  10. package/dist/doctor.d.ts.map +1 -1
  11. package/dist/doctor.js +15 -5
  12. package/dist/doctor.js.map +1 -1
  13. package/dist/expect.d.ts +5 -1
  14. package/dist/expect.d.ts.map +1 -1
  15. package/dist/expect.js +46 -3
  16. package/dist/expect.js.map +1 -1
  17. package/dist/html-report.d.ts +5 -2
  18. package/dist/html-report.d.ts.map +1 -1
  19. package/dist/html-report.js +13 -8
  20. package/dist/html-report.js.map +1 -1
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/live-isolation.d.ts +3 -1
  26. package/dist/live-isolation.d.ts.map +1 -1
  27. package/dist/live-isolation.js +9 -4
  28. package/dist/live-isolation.js.map +1 -1
  29. package/dist/load-suite.d.ts.map +1 -1
  30. package/dist/load-suite.js +12 -0
  31. package/dist/load-suite.js.map +1 -1
  32. package/dist/preflight.d.ts +2 -2
  33. package/dist/preflight.d.ts.map +1 -1
  34. package/dist/preflight.js +4 -4
  35. package/dist/preflight.js.map +1 -1
  36. package/dist/record-trace.d.ts +3 -14
  37. package/dist/record-trace.d.ts.map +1 -1
  38. package/dist/record-trace.js +7 -43
  39. package/dist/record-trace.js.map +1 -1
  40. package/dist/run-suite.d.ts +28 -10
  41. package/dist/run-suite.d.ts.map +1 -1
  42. package/dist/run-suite.js +158 -135
  43. package/dist/run-suite.js.map +1 -1
  44. package/dist/types.d.ts +11 -14
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/validate-suite.d.ts.map +1 -1
  47. package/dist/validate-suite.js +11 -12
  48. package/dist/validate-suite.js.map +1 -1
  49. package/package.json +7 -9
package/README.md CHANGED
@@ -1,196 +1,100 @@
1
1
  # @post-print/agent-test
2
2
 
3
- **Source of truth for** agent-test package.
3
+ **Source of truth for** the direct-agent test package.
4
4
 
5
- <!-- doc-meta: owner=eng | last-reviewed=2026-07-29 -->
5
+ <!-- doc-meta: owner=eng | last-reviewed=2026-09-02 -->
6
6
 
7
- Jest-shaped agent scenario runner built on `@post-print/agent-harness`.
7
+ Scenario runner for real Cursor and Claude agents, built on `@post-print/agent-harness`.
8
8
 
9
- ## In-repo smoke (this monorepo)
9
+ > **Deprecated and removed:** stored-trace replay is not an agent test. `host: "replay"`, `replayTrace`, `--record-fixtures`, and the old `--live` mode flag are rejected with migration guidance. Every scenario execution now launches Cursor or Claude and can incur provider usage.
10
10
 
11
- After `bun run build` at the repo root:
11
+ ## Direct TypeScript API
12
12
 
13
- ```bash
14
- node packages/test/dist/cli.js --suites-dir packages/test/fixtures --suite smoke
15
- node packages/test/dist/cli.js --doctor
13
+ `runAgentTest` is the execution core. JSON suites load into this same function.
14
+
15
+ ```ts
16
+ import { runAgentTest } from "@post-print/agent-test";
17
+
18
+ const result = await runAgentTest({
19
+ cwd: process.cwd(),
20
+ scenario: {
21
+ name: "uses the project instructions",
22
+ prompt: "Review the current change.",
23
+ rubric: { mustReadPath: ["AGENTS.md"] },
24
+ },
25
+ });
16
26
  ```
17
27
 
18
- There is no top-level `agent-suites/` here consumer examples below assume a consuming repo. Live `--live` needs an **exported** `CURSOR_API_KEY` for Cursor (default) or `ANTHROPIC_API_KEY` for `--host claude` (copy repo-root `.env.example`; the CLI does not auto-load `.env`). Judge classifiers still need `CURSOR_API_KEY` unless you pass `--no-judge`.
28
+ The default host is Cursor. Set `scenario.host: "claude"`, `defaults.host`, or the top-level `host` option for Claude. The scenario host wins over the top-level override, which wins over suite defaults. The default suite name is `direct`; worktree isolation, judging, timeout enforcement, and announce-stop retry are enabled by default.
29
+
30
+ ## JSON suites and CLI
19
31
 
20
- ## CLI (consumers, Node >= 22)
32
+ JSON is an authoring adapter, not a stored answer. Consumer repositories can keep suites under `agent-suites/<suite>/scenarios.json`:
21
33
 
22
- Works under **Node >= 22** (the published `agent-test` bin):
34
+ ```json
35
+ {
36
+ "name": "routing",
37
+ "defaults": { "host": "cursor", "profile": "cursor" },
38
+ "scenarios": [
39
+ {
40
+ "name": "reads instructions",
41
+ "prompt": "Review the current change.",
42
+ "rubric": { "mustReadPath": ["AGENTS.md"] }
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ Run Cursor by default or select Claude explicitly:
23
49
 
24
50
  ```bash
25
51
  npx agent-test --suites-dir agent-suites
26
- npx agent-test --suites-dir agent-suites --suite ambient-routing
27
- npx agent-test --suites-dir agent-suites --live --suite ambient-routing # exported CURSOR_API_KEY required
28
- npx agent-test --suites-dir agent-suites --live --host claude --suite ambient-routing # ANTHROPIC_API_KEY + claude CLI
29
- npx agent-test --live --compare-pairs skeleton-clean:skeleton-messy --out-dir "$TMPDIR/compare"
30
- npx agent-test compare --a clean.suite-report.json --b messy.suite-report.json --out-dir "$TMPDIR/compare"
52
+ npx agent-test --suites-dir agent-suites --suite routing
53
+ npx agent-test --suites-dir agent-suites --host claude
31
54
  npx agent-test --doctor
32
55
  ```
33
56
 
34
- Bun is fine for local package development (`bun install` / `bun run build` in this monorepo), but consumers do not need Bun to run suites.
35
-
36
- Default suites root: `agent-suites/` (must exist, or pass `--suites-dir`). Absolute `--suites-dir` is supported. Optional `--rubrics-dir` loads harness-only answer keys from `<rubricsDir>/<suite>/rubrics.json` (preferred over a sibling file when present).
37
-
38
- ### Harness-only rubrics
39
-
40
- `scenarios.json` may omit `rubric` (or use `{}`) and keep only prompts / `seedPatch` / `compareId`. Put `must` / `mustNot` / `judge` / tool matchers in:
41
-
42
- - sibling `rubrics.json` or `scenarios.rubric.json`: `{ "scenarios": { "<scenario name>": { "must": […] } } }`
43
- - or `--rubrics-dir <path>` → `<path>/<suiteName>/rubrics.json`
44
-
45
- External entries **replace** the inline rubric for that scenario name. Unknown names in the rubrics file are an error. Keep rubrics off agent-visible roots (absolute `--suites-dir` / `--rubrics-dir` under `$TMPDIR`, or outside the IDE-open workspace).
46
-
47
- Live output always uses ANSI color (including under Cursor agent shells that set `NO_COLOR`).
57
+ Direct runs require an exported `CURSOR_API_KEY` for Cursor or `ANTHROPIC_API_KEY` plus the Claude Code CLI for Claude. Judge criteria use `CURSOR_API_KEY` unless `--no-judge` is set. The CLI does not load `.env`.
48
58
 
49
- ## Environment variables
59
+ The old `--live` flag is removed because direct execution is now the only execution mode. Direct runs capture transient traces automatically; use `--keep-recordings` or `--debug` to retain diagnostics.
50
60
 
51
- See repo-root `.env.example`. Common knobs:
61
+ ## Validation, rubrics, and comparison
52
62
 
53
- | Variable | Purpose |
54
- | ------------------------------------------- | ------------------------------------------------------------- |
55
- | `CURSOR_API_KEY` | Required for `--live` Cursor and judge classifiers |
56
- | `ANTHROPIC_API_KEY` | Required for `--live --host claude` |
57
- | `CLAUDE_CODE_BIN` | Optional path to Claude Code CLI binary |
58
- | `CLAUDE_AGENT_MODEL` | Optional Claude model override |
59
- | `CLAUDE_CODE_ALLOWED_TOOLS` | Optional `--allowedTools` list for Claude live runs |
60
- | `AGENT_TEST_DEBUG` | Same as `--debug` when `1`/`true` |
61
- | `AGENT_TEST_VERBOSE` | Extra tips (e.g. OOM isolation) when `1` |
62
- | `AGENT_TEST_VERBOSE_PATHS` | Print full paths when `1` |
63
- | `AGENT_TEST_QUIET` | Suppress progress when `1` |
64
- | `AGENT_TEST_TIMEOUT_MS` | Live hard timeout (default 600000; `0` disables) |
65
- | `AGENT_TEST_LIVE_RETRIES` | Judge infra retry attempts (default 3) |
66
- | `AGENT_TEST_SCENARIO_RETRIES` | Live announce-stop scenario retries (default 1; `0` disables) |
67
- | `AGENT_TEST_ALLOW_IN_PLACE` | Allow `--no-worktree` live runs when `1` |
68
- | `AGENT_TEST_NO_WORKTREE` | Disable worktree isolation when `1`/`true` |
69
- | `AGENT_TEST_NO_ISOLATE` | Disable isolated subprocesses when `1` |
70
- | `AGENT_TEST_SCENARIO_SETTLE_MS` | Settle delay between live scenarios |
71
- | `CURSOR_AGENT_MODEL` / `CURSOR_JUDGE_MODEL` | Optional model overrides |
72
- | `CURSOR_JUDGE_TEMPERATURE` | Optional judge temperature |
73
-
74
- ## Debug mode
63
+ These commands inspect configuration or existing reports; they do not claim to execute an agent:
75
64
 
76
65
  ```bash
77
- npx agent-test --suites-dir agent-suites --suite smoke --debug
78
- npx agent-test --suites-dir agent-suites --live --debug
79
- npx agent-test --suites-dir agent-suites --live --debug --debug-dir "$TMPDIR/agent-test-debug"
80
- ```
81
-
82
- `--debug` (or `AGENT_TEST_DEBUG=1`) implies `--keep-recordings`, verbose failure detail, and full paths. Every non-skipped scenario writes a bundle under the session root:
83
-
84
- ```
85
- sessions/<id>/<suite>/<scenario>.debug/
86
- summary.md # verdict + Why (category hint + evidence + trace stats)
87
- transcript.md # Why, prompt, rubric, interleaved messages/tools (incl. results), failures
88
- scenario.json # prompt + rubric + seed metadata
89
- result.json # pass/fail, duration, failures, usage, skillsInvoked, routing, counts
90
- trace.json
91
- failures.json # includes category + evidence
92
- judge-debug.json # when judge criteria ran (SDK status/error, sizes, attempt)
93
- environment.json # versions/models/timeout/isolation; API keys only as booleans
94
- rerun.sh # shell-quoted exact re-run command (export API keys yourself)
66
+ npx agent-test --validate-only --validate-paths --suites-dir agent-suites
67
+ npx agent-test --validate-seeds --suites-dir agent-suites
68
+ npx agent-test --compare-pairs clean:changed --out-dir "$TMPDIR/compare"
69
+ npx agent-test compare --a clean.suite-report.json --b changed.suite-report.json --out-dir "$TMPDIR/compare"
95
70
  ```
96
71
 
97
- **Debug dir default:** omit `--debug-dir` to stage under `$TMPDIR/agent-spec/sessions/<id>/…` (outside the repo). Passing an in-repo `--debug-dir` (for example `./agent-test-debug`) is supported — harness staging paths under that dir are excluded from worktree leak checks but prefer `$TMPDIR` so debug artifacts never appear in `git status`.
98
-
99
- `--debug-dir <path>` replaces `$TMPDIR/agent-spec` as the sessions parent (`<path>/sessions/<id>/…`).
72
+ `scenarios.json` may omit inline rubric keys when they are supplied by sibling `rubrics.json` / `scenarios.rubric.json`, or by `--rubrics-dir <path>` at `<path>/<suite>/rubrics.json`. External rubric entries replace the inline rubric for the same scenario.
100
73
 
101
- Failure categories printed on FAIL lines and in `failures.json`:
74
+ Reports pair scenarios by `compareId` when present, otherwise by normalized scenario name. JSON, Markdown, and HTML reports include outcomes, token usage, duration, tools, and grounding signals.
102
75
 
103
- | Category | Meaning |
104
- | ----------------- | -------------------------------------------------------------------------------------- |
105
- | `rubric_miss` | Assertion/judge criterion miss |
106
- | `judge_infra` | Judge SDK/API failure (not a criterion miss) |
107
- | `agent_runtime` | Agent session error, timeout, AskQuestion, subprocess exit |
108
- | `worktree_leak` | Live agent mutated the caller working tree (harness `--debug-dir` staging is excluded) |
109
- | `recording_error` | Failed to persist a staging/fixture trace |
76
+ ## Isolation and diagnostics
110
77
 
111
- **Cancel:** `Ctrl+C` (SIGINT) kills in-flight isolated scenario subprocesses and best-effort cancels the active Cursor SDK run, then cleans scenario worktrees.
78
+ Each scenario uses a detached git worktree by default. This isolates edits, but it does not prevent a local host from reading the IDE-open caller checkout. Keep answer keys outside agent-visible roots when that distinction matters.
112
79
 
113
- ## Live dogfood
80
+ `--debug` retains an evidence bundle under `$TMPDIR/agent-spec/sessions/<id>/` by default. It includes the transcript, scenario, result, trace, failures, environment metadata, judge details, and an exact direct-run rerun command. Use `--debug-dir` to override the parent directory.
114
81
 
115
- Live runs need `CURSOR_API_KEY` and a suites directory that exists. Preflight fails when the resolved suites directory is missing (default `agent-suites/` if `--suites-dir` is omitted). `--suites-dir` may be relative to the repo cwd or an absolute path (for example a scrubbed suite tree under `$TMPDIR`).
82
+ `Ctrl+C` cancels active Cursor/Claude work and cleans worktrees. `--no-worktree` requires `AGENT_TEST_ALLOW_IN_PLACE=1` because agent edits will persist in the caller checkout.
116
83
 
117
- Passing live runs write staging traces under `$TMPDIR/agent-spec/sessions/<pid>-<timestamp>/` (removed on exit unless `--keep-recordings`). Use `--record-fixtures` to overwrite each scenario's committed `replayTrace` path. `--no-worktree` requires `AGENT_TEST_ALLOW_IN_PLACE=1`.
118
-
119
- ### Isolation model
120
-
121
- Live runs use a **detached git worktree** for agent file edits. That is not full filesystem isolation:
122
-
123
- - **Worktree does:** keep seed/apply edits and agent Write/Edit tools off the caller's working tree (leak checks catch escapes).
124
- - **Worktree does not:** stop context (skills/rules) from loading from the caller checkout by design, or stop Cursor **local** agents from Shell/Read against the IDE-open workspace instead of only `local.cwd`.
125
-
126
- Therefore: do **not** put answer keys, golden replays, or judge-bearing scenario text where a null-arm agent can forage them on the caller/IDE root. Prefer:
127
-
128
- - opaque prompts + `compareId`
129
- - seeds that only mutate fixtures (not skill bodies)
130
- - harness-only rubrics (sibling `rubrics.json` / `scenarios.rubric.json`, or `--rubrics-dir` outside the open workspace)
131
- - consumer orchestrators that park answer keys off the open workspace (toolbox pattern)
132
- - cloud runtime when true FS isolation is required
133
-
134
- Live agent runs have a **hard timeout** (default **10 minutes**, override with `--timeout-ms` or `AGENT_TEST_TIMEOUT_MS`; disable with `--no-timeout` or `AGENT_TEST_TIMEOUT_MS=0`). If the agent invokes `AskQuestion` or similar user-input tools, the harness fails fast with a clear error — live mode is single-shot and cannot supply follow-up turns. Use `--allow-user-input` only for intentional multi-turn dogfood (the run may still hang waiting for stdin).
135
-
136
- Announce-stop flakes (agent exits after Routing with no tools) are retried once by default on live runs (`AGENT_TEST_SCENARIO_RETRIES=1` or `--scenario-retries 1`; set `0` to disable). This is separate from `AGENT_TEST_LIVE_RETRIES` (judge infra only).
137
-
138
- ### Dialogue skills in live runs
139
-
140
- Skills that expect multi-turn Socratic dialogue (for example `crystallize`) will hang or fail in `--live` unless the scenario is written for one-shot completion:
141
-
142
- - **Replay-only** — commit a golden trace where the agent finishes without asking questions; use `host: "replay"` or `skip: true` with live skipped at suite level.
143
- - **One-shot live prompt** — instruct the agent to mirror intent and emit the final artifact in a single turn (`no AskQuestion; produce Crystallized idea now`).
144
- - **Ambient routing** — fuzzy-intent scenarios that only require mirroring + one assumption are naturally one-shot; full crystallize dialogue is not.
145
-
146
- Do not weaken dialogue-first product skills for CI; reshape the suite contract instead.
84
+ Dialogue-first skills must be tested with a one-shot prompt or an intentional `--allow-user-input` run. Do not weaken the production skill to make a headless test pass.
147
85
 
148
86
  ## MCP servers
149
87
 
150
- Live Cursor runs can attach **inline** MCP servers from suite/scenario JSON. Ambient project/user MCP (`.cursor/mcp.json`) is not loaded — tests stay hermetic.
151
-
152
- ```json
153
- {
154
- "defaults": {
155
- "mcpServers": {
156
- "docs": {
157
- "type": "http",
158
- "url": "https://example.com/mcp",
159
- "headers": { "Authorization": "Bearer ${DOCS_TOKEN}" }
160
- }
161
- }
162
- },
163
- "scenarios": [
164
- {
165
- "name": "use echo",
166
- "prompt": "Call the echo tool with text hello.",
167
- "mcpServers": {
168
- "echo": {
169
- "type": "stdio",
170
- "command": "node",
171
- "args": ["packages/test/fixtures/mcp-echo/server.mjs"]
172
- }
173
- },
174
- "rubric": {
175
- "mustCallTool": ["echo:hello"],
176
- "mustNotCallTool": ["shell"]
177
- }
178
- }
179
- ]
180
- }
181
- ```
88
+ Cursor and Claude scenarios can attach inline stdio or HTTP/SSE MCP servers through suite defaults or scenario overrides. Scenario server names replace matching defaults. `${ENV_VAR}` placeholders resolve at run time. Ambient project/user MCP configuration is not loaded.
182
89
 
183
- - Suite `defaults.mcpServers` merge with scenario `mcpServers` by server name (scenario wins).
184
- - `${ENV_VAR}` placeholders expand in `command`, `args`, `env`, `url`, `headers`, and OAuth fields at run time.
185
- - Rubric `mustCallTool` / `mustNotCallTool` match tool **name** substrings (works with MCP name prefixes). Use `name:argFragment` to also require a substring in JSON args.
186
- - Rubric `mustReadPath` / `mustNotReadPath` match substrings on **Read** tool JSON args (registry-first / avoid inventing paths). Keep hallucination scoring in live `judge` questions — no heavy factuality engine in v1.
187
- - Suite defaults may set `profile: "skeleton"` and/or `contextSources` (additive paths / `.skeleton/customize/` basenames). Shared/cursor/claude defaults stay backwards-compatible.
188
- - Live runs surface provider `usage` on traces/results; suite summary + HTML report include token sum / p50 / p95 when present.
189
- - `--compare-pairs A:B` (or `agent-test compare --a/--b`) pairs scenarios by **`compareId`** when present, else band-neutral scenario name (`outcome:` / `transfer:` stripped), and writes `compare-report.json` / `.md` / `.html` with pass/fail, tokens, toolCallCount, durationMs, and skill/registry hop proxies. Suite HTML also embeds an A/B table when two reports are present.
190
- - Replay hosts ignore `mcpServers` but still score recorded `toolCalls` against those matchers.
90
+ ## In-repo package checks
191
91
 
192
- ## Library
92
+ The repository does not run paid agents in its default CI package-integrity checks:
193
93
 
194
- ```ts
195
- import { runAllSuites, expectTrace } from "@post-print/agent-test";
94
+ ```bash
95
+ bun run build
96
+ node packages/test/dist/cli.js --validate-only --suites-dir packages/test/fixtures --suite smoke
97
+ node packages/test/dist/cli.js --doctor
196
98
  ```
99
+
100
+ Credentialed acceptance requires one TypeScript `runAgentTest` call and one JSON-suite CLI run against a real host.
package/dist/cli.d.ts CHANGED
@@ -10,9 +10,6 @@ export interface ParsedCliArgs {
10
10
  filter?: string;
11
11
  scenarioFilter?: string;
12
12
  stagingSessionId?: string;
13
- record: boolean;
14
- recordFixtures: boolean;
15
- live: boolean;
16
13
  judge?: boolean;
17
14
  worktree?: boolean;
18
15
  keepRecordings: boolean;
@@ -21,6 +18,8 @@ export interface ParsedCliArgs {
21
18
  allowUserInput: boolean;
22
19
  doctor: boolean;
23
20
  htmlReport: boolean;
21
+ /** Explicit HTML report path (ends in .html) or output directory for all report content. */
22
+ reportOut?: string;
24
23
  debug: boolean;
25
24
  debugDir?: string;
26
25
  validateOnly: boolean;
@@ -33,10 +32,15 @@ export interface ParsedCliArgs {
33
32
  compareMode: boolean;
34
33
  compareA?: string;
35
34
  compareB?: string;
36
- /** Live/replay A:B suite dirs or report JSON paths. */
35
+ /** Direct-run A:B suite dirs or report JSON paths. */
37
36
  comparePairs?: string;
38
37
  compareOutDir?: string;
39
38
  }
40
39
  /** Parse agent-test CLI argv (exported for unit tests). */
41
40
  export declare function parseCliArgs(argv: string[]): ParsedCliArgs;
41
+ /** Resolve an explicit report target into the HTML path and optional artifact directory. */
42
+ export declare function resolveReportOutput(reportOut?: string): {
43
+ htmlPath?: string;
44
+ outDir?: string;
45
+ };
42
46
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EACN,KAAK,SAAS,EAGd,MAAM,2BAA2B,CAAC;AAuBnC,OAAO,EACN,KAAK,UAAU,EAIf,MAAM,oBAAoB,CAAC;AAU5B,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CA6K1D"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAKA,OAAO,EACN,KAAK,SAAS,EAKd,MAAM,2BAA2B,CAAC;AAsBnC,OAAO,EACN,KAAK,UAAU,EAIf,MAAM,oBAAoB,CAAC;AAU5B,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAmL1D;AAED,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAQ9F"}
package/dist/cli.js CHANGED
@@ -2,15 +2,15 @@
2
2
  import { access, mkdir, writeFile } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { cleanupStaleScenarioWorktrees, isPathUnderRoot, } from "@post-print/agent-harness";
5
+ import { CLAUDE_AUTH_MODE_ENV, cleanupStaleScenarioWorktrees, isPathUnderRoot, parseClaudeAuthMode, } from "@post-print/agent-harness";
6
6
  import { isCliMain } from "./cli-entry.js";
7
7
  import { compareSuiteReports, labelForCompareSide, loadSuiteRunReport, parseComparePairToken, writeCompareReport, } from "./compare.js";
8
8
  import { discoverSuites } from "./discover-suites.js";
9
9
  import { runDoctor } from "./doctor.js";
10
10
  import { writeHtmlReport } from "./html-report.js";
11
- import { assertLiveDogfoodPreflight } from "./preflight.js";
11
+ import { assertDirectAgentPreflight } from "./preflight.js";
12
12
  import { logProgress } from "./progress.js";
13
- import { cleanupLegacyRepoRecordings, cleanupStagingSession, createLiveStagingSessionId, getLiveStagingSessionRoot, setLiveStagingRootOverride, } from "./record-trace.js";
13
+ import { cleanupStagingSession, createLiveStagingSessionId, getLiveStagingSessionRoot, setLiveStagingRootOverride, } from "./record-trace.js";
14
14
  import { registerLiveRunHandlers, runAllSuites, runSuite } from "./run-suite.js";
15
15
  import { formatRunSummary, shouldFailScenario, summarizeReports, } from "./suite-summary.js";
16
16
  import { configureCliColor, theme } from "./theme.js";
@@ -28,9 +28,6 @@ export function parseCliArgs(argv) {
28
28
  let filter;
29
29
  let scenarioFilter;
30
30
  let stagingSessionId;
31
- let record = false;
32
- let recordFixtures = false;
33
- let live = false;
34
31
  let judge;
35
32
  let worktree;
36
33
  let keepRecordings = false;
@@ -39,6 +36,7 @@ export function parseCliArgs(argv) {
39
36
  let allowUserInput = false;
40
37
  let doctor = false;
41
38
  let htmlReport = true;
39
+ let reportOut;
42
40
  let debug = process.env.AGENT_TEST_DEBUG === "1" || process.env.AGENT_TEST_DEBUG === "true";
43
41
  let debugDir;
44
42
  let validateOnly = false;
@@ -58,7 +56,14 @@ export function parseCliArgs(argv) {
58
56
  for (let i = startIndex; i < argv.length; i++) {
59
57
  const token = argv[i];
60
58
  if (token === "--host" && argv[i + 1]) {
61
- host = argv[++i];
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;
62
67
  }
63
68
  else if (token === "--suites-dir" && argv[i + 1]) {
64
69
  suitesDir = argv[++i];
@@ -75,15 +80,14 @@ export function parseCliArgs(argv) {
75
80
  else if (token === "--staging-session-id" && argv[i + 1]) {
76
81
  stagingSessionId = argv[++i];
77
82
  }
83
+ else if (token === "--live") {
84
+ throw new Error("--live was removed because agent-test now always runs a real agent");
85
+ }
78
86
  else if (token === "--record") {
79
- record = true;
87
+ throw new Error("--record was removed; direct runs capture transient traces automatically (use --keep-recordings to retain them)");
80
88
  }
81
89
  else if (token === "--record-fixtures") {
82
- record = true;
83
- recordFixtures = true;
84
- }
85
- else if (token === "--live") {
86
- live = true;
90
+ throw new Error("--record-fixtures was removed because replay-based testing is deprecated and no longer supported");
87
91
  }
88
92
  else if (token === "--keep-recordings") {
89
93
  keepRecordings = true;
@@ -149,6 +153,9 @@ export function parseCliArgs(argv) {
149
153
  else if ((token === "--out-dir" || token === "--compare-out") && argv[i + 1]) {
150
154
  compareOutDir = argv[++i];
151
155
  }
156
+ else if (token === "--report-out" && argv[i + 1]) {
157
+ reportOut = argv[++i];
158
+ }
152
159
  else if (token === "--no-html-report") {
153
160
  htmlReport = false;
154
161
  }
@@ -172,12 +179,8 @@ export function parseCliArgs(argv) {
172
179
  throw new Error("compare requires --a <report.json> and --b <report.json>");
173
180
  }
174
181
  }
175
- if (live) {
176
- host = host ?? "cursor";
177
- record = true;
178
- judge = judge ?? true;
179
- worktree = worktree ?? true;
180
- }
182
+ judge = judge ?? true;
183
+ worktree = worktree ?? true;
181
184
  if (debug) {
182
185
  keepRecordings = true;
183
186
  process.env.AGENT_TEST_DEBUG = "1";
@@ -192,9 +195,6 @@ export function parseCliArgs(argv) {
192
195
  filter,
193
196
  scenarioFilter,
194
197
  stagingSessionId,
195
- record,
196
- recordFixtures,
197
- live,
198
198
  judge,
199
199
  worktree,
200
200
  keepRecordings,
@@ -203,6 +203,7 @@ export function parseCliArgs(argv) {
203
203
  allowUserInput,
204
204
  doctor,
205
205
  htmlReport,
206
+ reportOut: reportOut ? resolve(cwd, reportOut) : undefined,
206
207
  debug,
207
208
  debugDir: debugDir ? resolve(cwd, debugDir) : undefined,
208
209
  validateOnly,
@@ -217,6 +218,16 @@ export function parseCliArgs(argv) {
217
218
  compareOutDir: compareOutDir ? resolve(cwd, compareOutDir) : undefined,
218
219
  };
219
220
  }
221
+ /** Resolve an explicit report target into the HTML path and optional artifact directory. */
222
+ export function resolveReportOutput(reportOut) {
223
+ if (!reportOut) {
224
+ return {};
225
+ }
226
+ if (reportOut.toLowerCase().endsWith(".html")) {
227
+ return { htmlPath: reportOut };
228
+ }
229
+ return { htmlPath: join(reportOut, "report.html"), outDir: reportOut };
230
+ }
220
231
  async function pathExists(path) {
221
232
  try {
222
233
  await access(path);
@@ -255,8 +266,6 @@ async function loadOrRunCompareSide(args, side, stagingSessionId) {
255
266
  suitePath,
256
267
  host: args.host,
257
268
  scenarioFilter: args.scenarioFilter,
258
- record: args.record,
259
- recordFixtures: args.recordFixtures,
260
269
  judge: args.judge,
261
270
  worktree: args.worktree,
262
271
  stagingSessionId,
@@ -276,7 +285,7 @@ async function writeSuiteReportDump(outDir, label, report) {
276
285
  await writeFile(path, `${JSON.stringify(report, null, 2)}\n`, "utf8");
277
286
  return path;
278
287
  }
279
- async function cleanupLiveRunArtifacts(cwd, stagingSessionRoot, keepRecordings) {
288
+ async function cleanupRunArtifacts(stagingSessionRoot, keepRecordings) {
280
289
  if (keepRecordings) {
281
290
  return;
282
291
  }
@@ -288,10 +297,6 @@ async function cleanupLiveRunArtifacts(cwd, stagingSessionRoot, keepRecordings)
288
297
  // best-effort
289
298
  }
290
299
  }
291
- const legacyRemoved = await cleanupLegacyRepoRecordings(cwd);
292
- if (legacyRemoved.length > 0) {
293
- console.log(`Removed legacy in-repo recording dir(s):\n ${legacyRemoved.join("\n ")}`);
294
- }
295
300
  }
296
301
  async function main() {
297
302
  let args;
@@ -362,37 +367,41 @@ async function main() {
362
367
  }
363
368
  const isChild = process.env.AGENT_TEST_CHILD === "1";
364
369
  if (args.debugDir && isPathUnderRoot(args.debugDir, args.cwd) && !isChild) {
365
- 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.`));
370
+ 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.`));
366
371
  }
367
372
  const verbose = args.debug || process.env.AGENT_TEST_VERBOSE === "1" || process.env.AGENT_TEST_DEBUG === "1";
368
373
  const stagingSessionId = args.stagingSessionId?.trim() ||
369
374
  process.env.AGENT_TEST_STAGING_SESSION_ID?.trim() ||
370
- (args.live || args.debug || (args.record && !args.recordFixtures)
371
- ? createLiveStagingSessionId()
372
- : undefined);
375
+ createLiveStagingSessionId();
373
376
  const stagingSessionRoot = stagingSessionId
374
377
  ? getLiveStagingSessionRoot(stagingSessionId)
375
378
  : undefined;
376
379
  try {
377
- if (args.live) {
378
- const host = args.host ?? "cursor";
379
- if (host === "claude" && !process.env.ANTHROPIC_API_KEY?.trim()) {
380
- console.error("ANTHROPIC_API_KEY required for --live --host claude (Claude Code CLI)");
381
- return 1;
382
- }
383
- if (host === "cursor" && !process.env.CURSOR_API_KEY?.trim()) {
384
- console.error("CURSOR_API_KEY required for --live (Cursor SDK runs)");
385
- return 1;
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
+ }
386
387
  }
387
- // Judge classifiers still use the Cursor SDK.
388
- if (args.judge !== false && !process.env.CURSOR_API_KEY?.trim()) {
389
- console.error("CURSOR_API_KEY required for live judge classifiers (use --no-judge to skip)");
388
+ catch (error) {
389
+ console.error(error instanceof Error ? error.message : error);
390
390
  return 1;
391
391
  }
392
392
  }
393
- if (args.live) {
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;
396
+ }
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;
401
+ }
402
+ {
394
403
  try {
395
- await assertLiveDogfoodPreflight(args.cwd, args.suitesDir);
404
+ await assertDirectAgentPreflight(args.cwd, args.suitesDir);
396
405
  }
397
406
  catch (error) {
398
407
  console.error(error instanceof Error ? error.message : error);
@@ -403,7 +412,7 @@ async function main() {
403
412
  process.env.AGENT_TEST_NO_WORKTREE === "1" ||
404
413
  process.env.AGENT_TEST_NO_WORKTREE === "true";
405
414
  if (worktreeDisabled && !inPlaceAllowed) {
406
- 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).");
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).");
407
416
  return 1;
408
417
  }
409
418
  registerLiveRunHandlers();
@@ -412,7 +421,7 @@ async function main() {
412
421
  if (removed.length > 0) {
413
422
  console.log(theme.warn(`Cleaned ${removed.length} stale agent-test worktree(s) from a prior crash`));
414
423
  }
415
- console.log(theme.banner(args.debug ? "live debug" : "live"));
424
+ console.log(theme.banner(args.debug ? "direct debug" : "direct"));
416
425
  if (stagingSessionRoot) {
417
426
  console.log(theme.bannerSession(stagingSessionRoot));
418
427
  }
@@ -434,10 +443,6 @@ async function main() {
434
443
  }
435
444
  }
436
445
  }
437
- else if (args.debug && !isChild && stagingSessionRoot) {
438
- console.log(theme.banner("debug"));
439
- console.log(theme.bannerSession(stagingSessionRoot));
440
- }
441
446
  let reports;
442
447
  let comparePassRegressions = 0;
443
448
  if (args.comparePairs) {
@@ -543,7 +548,7 @@ async function main() {
543
548
  }
544
549
  finally {
545
550
  if (stagingSessionId && !isChild) {
546
- await cleanupLiveRunArtifacts(args.cwd, stagingSessionRoot, args.keepRecordings);
551
+ await cleanupRunArtifacts(stagingSessionRoot, args.keepRecordings);
547
552
  }
548
553
  }
549
554
  }