cowork-harness 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.claude/skills/cowork-harness/SKILL.md +74 -5
  2. package/.claude/skills/cowork-harness/references/ci-recipe.md +4 -4
  3. package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +1 -1
  4. package/.claude/skills/cowork-harness/references/scenario-schema.md +43 -9
  5. package/.claude/skills/cowork-harness/references/task-recipes.md +4 -2
  6. package/.claude/skills/cowork-harness/scripts/assertion-keys.json +22 -1
  7. package/.claude/skills/cowork-harness/scripts/scenario.py +35 -2
  8. package/.env.example +7 -1
  9. package/CHANGELOG.md +213 -0
  10. package/CONTRIBUTING.md +0 -3
  11. package/DESIGN.md +2 -2
  12. package/README.md +157 -63
  13. package/SPEC.md +38 -20
  14. package/dist/agent/session.js +105 -7
  15. package/dist/agent/timeline.js +137 -0
  16. package/dist/assert.js +346 -44
  17. package/dist/boundary-paths.js +7 -6
  18. package/dist/cli.js +148 -51
  19. package/dist/egress/proxy.js +23 -8
  20. package/dist/egress/sidecar.js +43 -7
  21. package/dist/errors.js +1 -1
  22. package/dist/hostloop/cowork-handler.js +224 -0
  23. package/dist/io.js +1 -1
  24. package/dist/run/artifacts.js +42 -1
  25. package/dist/run/assemble-run-result.js +17 -0
  26. package/dist/run/cassette.js +747 -127
  27. package/dist/run/chat-result.js +111 -0
  28. package/dist/run/chat.js +40 -7
  29. package/dist/run/computer-links.js +43 -25
  30. package/dist/run/diff.js +4 -4
  31. package/dist/run/display-translate.js +3 -3
  32. package/dist/run/doctor.js +1 -1
  33. package/dist/run/envelope.js +30 -11
  34. package/dist/run/execute.js +251 -33
  35. package/dist/run/matrix.js +21 -12
  36. package/dist/run/pre-run-manifest.js +111 -10
  37. package/dist/run/renderer.js +11 -4
  38. package/dist/run/repeat.js +10 -6
  39. package/dist/run/run-index.js +21 -3
  40. package/dist/run/run-status.js +28 -13
  41. package/dist/run/run.js +416 -24
  42. package/dist/run/skill-hash.js +14 -4
  43. package/dist/run/skill-metadata.js +82 -0
  44. package/dist/run/timeline-fold.js +85 -0
  45. package/dist/run/trace-view.js +77 -8
  46. package/dist/run/verdict.js +15 -3
  47. package/dist/run/vm-path-ctx-file.js +2 -2
  48. package/dist/runtime/argv.js +9 -5
  49. package/dist/runtime/container.js +21 -2
  50. package/dist/runtime/hostloop.js +11 -13
  51. package/dist/runtime/resource-sampler.js +246 -0
  52. package/dist/session.js +71 -8
  53. package/dist/sync/baseline-diff.js +1 -1
  54. package/dist/sync/cowork-sync.js +3 -3
  55. package/dist/types.js +116 -8
  56. package/docs/README.md +1 -1
  57. package/docs/cassette.md +70 -20
  58. package/docs/decisions/2026-07-07-verification-strictness-and-fidelity.md +74 -0
  59. package/docs/gotchas.md +10 -0
  60. package/docs/maintenance.md +1 -1
  61. package/docs/run-status.md +5 -1
  62. package/docs/scenario.md +39 -10
  63. package/docs/session.md +2 -0
  64. package/examples/README.md +17 -3
  65. package/examples/replays/README.md +28 -1
  66. package/examples/replays/example-pdf-skill.cassette.json +30 -24
  67. package/examples/replays/hostloop-computer-links.cassette.json +83 -23
  68. package/llms.txt +1 -1
  69. package/package.json +1 -1
  70. package/python/README.md +4 -1
  71. package/schema/cassette.v8.json +166 -0
  72. package/schema/cassette.v9.json +182 -0
  73. package/schema/run-result.json +209 -1
  74. package/schema/scenario.schema.json +162 -7
  75. package/schema/session.schema.json +5 -0
  76. package/schema/verify-cassettes.json +17 -5
@@ -3,8 +3,8 @@ name: cowork-harness
3
3
  description: Test or debug a Claude Code skill/plugin under Claude Cowork's runtime — sandboxed agent, default-deny egress, the can_use_tool permission/question protocol — using the cowork-harness CLI. Use when validating or regression-testing a skill, authoring or debugging a scenario YAML (prompt + scripted answers + assert:), choosing a fidelity tier, scripting AskUserQuestion / tool-permission answers, or asserting artifacts, egress, or sub-agent dispatch. Especially when a harness run no-ops an assertion, fails on an unanswered gate, false-greens, a steered answer never reaches the model, or a web_fetch is unexpectedly denied or gated. NOT for generic unit testing (pytest/vitest of your own scripts) or non-Cowork CI. Covers the skill / run / chat / record / replay / trace / decide / assertions / scaffold commands and the session-vs-scenario split.
4
4
  metadata:
5
5
  author: cowork-harness
6
- version: 0.25.0
7
- tracks-harness: cowork-harness 0.25.0 (baseline desktop-1.18286.0)
6
+ version: 0.27.0
7
+ tracks-harness: cowork-harness 0.27.0 (baseline desktop-1.18286.0)
8
8
  ---
9
9
 
10
10
  # cowork-harness
@@ -22,7 +22,7 @@ flagged with a loud `::warning::`, not silent — auto-answer a gate, observe an
22
22
  allowlist). This skill exists mostly to keep you out of those traps — the Gotchas section below is
23
23
  the highest-value part. Read it.
24
24
 
25
- > **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.25.0` (baseline
25
+ > **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.27.0` (baseline
26
26
  > `desktop-1.18286.0`). If your checkout is newer, prefer the live `--help`, `SPEC.md`, and
27
27
  > `docs/*.md` over this snapshot, and re-run the bundled linter.
28
28
 
@@ -38,10 +38,11 @@ cowork-harness skill ./my-skill "do X" # run the skill once against the sta
38
38
  Before the first command, confirm the CLI is reachable and **fail loud** (never fake a pass) when a tier's dependencies are missing:
39
39
 
40
40
  - **One-shot check.** Run `cowork-harness doctor [--tier <tier>]` first — a read-only prerequisite check that inspects Docker, the staged agent, the token, and the baseline in one pass. The bullets below explain each thing it checks (and how to fix it).
41
- - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.25.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.25.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.25.0"`. **Pin `@>=0.25.0`, never `@latest`** — `@latest` can silently fetch an older CLI and the new commands fail as "unknown command", whereas the floor **fails loud** if no compatible version is published. (≥ 0.25.0 is what gates the commands/assertions this skill teaches: `assertions --list`, `scaffold <run-id>`, `trace --view dispatches`, `artifact_json` incl. the `in:` operator, `verify-cassettes`, batch `record <dir>`/`--rerecord-stale`, `record --concurrency <N>`, record-time redaction, multiSelect/`answer:`, `verify-run` answer-coverage, `record --max-artifact-bytes`, live record-time deciders, `verify-cassettes --allow-domain`/`--allow-email`/`--allow-path`/`--allow-patterns-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0; `--allow-patterns-file <path>` is a FILE of patterns, one regex per line — not a path to allow, unlike `--allow <regex>`), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field, and `computer_links_resolve` (new in 0.22.0).)
41
+ - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.27.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.27.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.27.0"`. **Pin `@>=0.27.0`, never `@latest`** — `@latest` can silently fetch an older CLI and the new commands fail as "unknown command", whereas the floor **fails loud** if no compatible version is published. (≥ 0.27.0 is what gates the commands/assertions this skill teaches: `assertions --list`, `scaffold <run-id>`, `trace --view dispatches`, `artifact_json` incl. the `in:` operator (passes when the resolved value deep-equals one of the listed members — value ∈ your list, not the reverse), `verify-cassettes`, batch `record <dir>`/`--rerecord-stale`, `record --concurrency <N>`, record-time redaction, multiSelect/`answer:`, `verify-run` answer-coverage, `record --max-artifact-bytes`, live record-time deciders, `verify-cassettes --allow-domain`/`--allow-email`/`--allow-path`/`--allow-patterns-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0; `--allow-patterns-file <path>` is a FILE of patterns, one regex per line — not a path to allow, unlike `--allow <regex>`), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field, and `computer_links_resolve` (new in 0.22.0).)
42
42
  - **Agent binary (sandboxed live tiers — `container`/`microvm`/`hostloop`/`cowork`).** The staged Claude Code agent is **bind-mounted** from a local Claude Desktop install, or point `COWORK_AGENT_BINARY` at a `claude-code-vm/<ver>/claude` ELF. Nothing is bundled. `protocol` (L0) and `replay` need no staged agent; for the sandboxed tiers, no agent → no run; report that, don't skip silently.
43
43
  - **Docker / Lima.** Only `--fidelity protocol` (L0) runs without them. `container` / `microvm` / `hostloop` / `cowork` need Docker (Lima for L2). If they're absent, drop to `--fidelity protocol` and **say so** — a green that never exercised the sandbox is not a sandbox pass.
44
44
  - **Auth.** `CLAUDE_CODE_OAUTH_TOKEN` (preferred) or `ANTHROPIC_API_KEY`, via env or `.env`. Minting an OAuth token needs the **`claude` CLI** (`npm i -g @anthropic-ai/claude-code`, then `claude setup-token`).
45
+ - **`--dotenv` is a GLOBAL flag — put it BEFORE the subcommand.** `cowork-harness --dotenv .env record …`, never `cowork-harness record … --dotenv .env`. Every *other* flag is subcommand-level, so muscle memory fights this one; the harness rejects the misplaced form with an exact-fix error, but placing it first avoids the round-trip.
45
46
 
46
47
  ## 1. Pick the loop
47
48
 
@@ -55,7 +56,7 @@ Before the first command, confirm the CLI is reachable and **fail loud** (never
55
56
 
56
57
  Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
57
58
  verify-run · trace · inspect · diff · stats · decide · gates · answer · scaffold · assertions --list · sync ·
58
- list · boundary-check · status · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
59
+ list · boundary-check · status · vm <init|status|delete|prune> · doctor · init-redact`. Always check `cowork-harness <cmd> --help`.
59
60
 
60
61
  **Two different `scaffold` tools — don't confuse them.** The native `cowork-harness scaffold <run-id>`
61
62
  above turns an already-*recorded* run into a scenario (needs a run to exist first). The bundled
@@ -157,6 +158,12 @@ checkable regardless of PID namespace. The harness prints `[status] <outDir>` to
157
158
  run starts, so capture stderr to get the exact directory. `--follow` fails loud on a timeout/staleness
158
159
  rather than hanging forever. See `docs/run-status.md` for the full recipe.
159
160
 
161
+ **A multi-minute `record`/`run` outlives a short-lived wrapper.** Don't launch a long record from a
162
+ subagent that returns before it finishes — the returning agent tears down its process tree and kills the
163
+ in-flight run mid-artifact-write. Run it foreground, or detached from any process that will exit first.
164
+ (The `status.json` liveness above is exactly what surfaces such a teardown as `"error"`/`stale` rather
165
+ than a stuck `"running"`.)
166
+
160
167
  ## 6. Assertions: two orthogonal axes
161
168
 
162
169
  Conflating these is the **biggest landmine**. An assertion key has two independent properties:
@@ -177,6 +184,35 @@ catches it before you push — run it (§9).
177
184
 
178
185
  See `references/scenario-schema.md` for the full assertion catalog with each key's replay class.
179
186
 
187
+ ### Which assertion for which question (goal → key)
188
+
189
+ Beyond the outcome/content keys most scenarios reach for first (`result`, `transcript_*`,
190
+ `file_exists`/`user_visible_artifact`, `artifact_json`), the harness surfaces the agent's *behavior*
191
+ — tool health, sub-agent work, panels, skill attribution, resources — as assertable keys. Reach for
192
+ them by what you're trying to prove:
193
+
194
+ | You want to check that… | Reach for |
195
+ |---|---|
196
+ | the skill didn't error out of a tool | `tool_no_error: <regex>`, `max_tool_errors: <N>` |
197
+ | it didn't waste repeated identical calls | `max_redundant_tool_calls: <N>` |
198
+ | a deliverable reached the user | `user_visible_artifact: <path>` (+ `no_scratchpad_leak: true` if it delivers via `present_files` — **`container` only**) |
199
+ | a to-do workflow finished | `all_tasks_completed: true`, `task_status: {match, status}` |
200
+ | a skill / connector / tool was **offered** | `skill_available`, `connector_available`, `tool_available` (all `<regex>`) |
201
+ | a skill actually **ran** (or must NOT) | `skill_triggered: <regex>`, `no_skill_triggered: <regex>` |
202
+ | a tool ran **inside** a skill's scope | `skill_tool_used: {skill, tool}` |
203
+ | a sub-agent did the work | `subagent_output_contains: {contains}`, `subagent_dispatched: <regex>`, `dispatch_count_max: <N>` |
204
+ | a pre-existing input wasn't mutated | `input_unmodified: [<glob>]` (live/verify-run; not microvm) |
205
+ | a resource ceiling held | `max_peak_rss_bytes: <N>` (**live-only**) |
206
+ | a hook blocked / didn't block a tool | `hook_blocked: <regex>`, `no_hook_blocked: true` (replay needs a `controlOut` cassette) |
207
+ | every MCP round-trip succeeded | `no_mcp_error: true` (**live-only**) |
208
+ | a context compaction happened | `compaction_occurred: true` |
209
+
210
+ Every one of these still obeys the two axes above — several are live-only or need a `controlOut`
211
+ cassette on replay, so check the catalog's replay class before putting one on a PR gate.
212
+ `cowork-harness assertions --list` prints the full, always-current key set with one-line semantics
213
+ straight from the schema — treat it (and the catalog) as the source of truth; this map is a
214
+ goal-oriented index into it, not a second catalog.
215
+
180
216
  ## 7. web_fetch (fail-closed, two-path)
181
217
 
182
218
  `web_fetch` behaves unlike `curl`. A URL is gated by **provenance**, not the egress allowlist:
@@ -263,6 +299,26 @@ The run verdict may include `WARN`-severity signals in addition to pass/fail. On
263
299
  the run can still green. If you see it, fix the asset path — a green with a missing asset is
264
300
  not a valid pass.
265
301
 
302
+ ### Inspecting a run's observability output
303
+
304
+ A verdict is only the top of what a run records. Beyond pass/fail, every `run`/`skill`/`chat` writes a
305
+ `result.json` and a trace you can read without a re-record — use these to *observe* behavior (and to decide
306
+ which assertions from §6 are worth adding), and to diagnose a failure:
307
+
308
+ - **`cowork-harness trace <run-dir> --view <view>`** — `tool-durations` (per-tool call count + timing),
309
+ `dispatches` (the sub-agent tree, each dispatch's prompt / model / output), `questions` (gates +
310
+ sub-question totals), `tools`. Default digests the run.
311
+ - **`cowork-harness stats [--metric <m>]`** — aggregate across the run index: `cost`, `duration`,
312
+ `tokens`, `cache-tokens`, `model-cost`, `turns`, `pass-rate`.
313
+ - **`result.json` carries the raw fields** the assertions read: `toolDurations`, `models`, `toolErrors`,
314
+ `redundantToolCalls`, `modelUsage`, `thinking`, `skillActivity`, `subagents[]` (prompt/model/output/
315
+ `attributedSkillId`), `context` (tools/mcpServers/availableSkills), `tasks`, `workspaceFiles`,
316
+ `presentedFiles`, `hookEvents`, `mcpErrors`, `contextEvents`, `resources` — see the README's
317
+ "Observability fields" for the full field semantics.
318
+ - **Opaque failure?** A failed run also records **`errorSource`** (where the failure originated) and
319
+ **`stderrLogPath`** (the captured agent stderr) — read those and `trace <run-dir>` *before* re-running;
320
+ a re-record rarely tells you more than the captured stderr already does.
321
+
266
322
  ## 9. Scaffold a valid scenario, lint before you push, then place in CI
267
323
 
268
324
  Don't hand-write the YAML from memory — that's how invented keys (`assertions:` vs `assert:`,
@@ -308,6 +364,11 @@ the four-stage pipeline.
308
364
  **not** an asserted test — no `assert:` block, no cassette. Use it to explore behavior, reproduce a
309
365
  bug interactively, or test a prompt before committing it to a scenario.
310
366
 
367
+ Each session still writes an informational `result.json` (`mode: "chat"`, no `assertions`) plus a
368
+ trace and index row under its run dir — the same telemetry (tool durations, model usage, resources,
369
+ etc.) that `run`/`skill` produce — so `cowork-harness trace <chat-run-dir>` / `stats` work on a chat
370
+ session too, even though it never yields a verdict.
371
+
311
372
  **`--plugin <dir>` flag (repeatable).** Load additional skill folders alongside the primary session
312
373
  plugin. Each `--plugin <dir>` appends the folder to `local_plugins`. Useful when the skill-under-test
313
374
  depends on a sibling plugin:
@@ -479,6 +540,14 @@ are the ones that bite hardest.
479
540
  `RunResult.artifacts`). A `mode: rw`/`rwd` folder's contents are captured with a full body, same as
480
541
  `outputs/`.
481
542
 
543
+ 21. **A `fidelity: cowork` cassette can go stale in a way `skill`/`format` drift won't catch.** Its recorded
544
+ `effectiveFidelity` field pins which concrete tier (`hostloop` or `container`) the baseline resolved to
545
+ AT RECORD TIME. If a later Desktop baseline flips that resolution, `verify-cassettes` reports it as a
546
+ `resolved-tier` finding (re-record — the recording now exercises the wrong tier); a cassette with no
547
+ `effectiveFidelity` at all, or an unloadable pinned `baseline:`, reports `unverifiable-tier` instead
548
+ (couldn't check — also re-record). Both are `fidelity: cowork`-only; an explicit-tier scenario never
549
+ produces them. See `docs/cassette.md` § tier staleness.
550
+
482
551
  For the complete gotcha list, the assertion catalog, the YAML schema, the fidelity/answer tables,
483
552
  and the CI recipe, read the files in `references/`.
484
553
 
@@ -1,6 +1,6 @@
1
1
  # CI recipe — replay vs live lanes
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.25.0` (baseline `desktop-1.18286.0`).
3
+ Self-contained reference. Tracks `cowork-harness 0.27.0` (baseline `desktop-1.18286.0`).
4
4
 
5
5
  **Fastest path: the packaged Action.** One step gets you `replay`/`lint`/`verify-cassettes` plus a PR
6
6
  job-summary reporter (verdict table, staleness findings, cost/turns when available):
@@ -53,7 +53,7 @@ sha256-*checked* but not hard-blocking on mismatch — it's advisory for an inte
53
53
  GitHub-hosted runners, no token/Docker/agent:
54
54
 
55
55
  ```yaml
56
- - run: npm i -g "cowork-harness@>=0.25.0"
56
+ - run: npm i -g "cowork-harness@>=0.27.0"
57
57
  - run: cowork-harness lint scenarios/*.yaml # no silent false-greens
58
58
  - run: cowork-harness verify-cassettes cassettes/ # privacy + staleness
59
59
  - run: cowork-harness replay cassettes/ # token-free content/structure
@@ -187,7 +187,7 @@ jobs:
187
187
  with: { node-version: '20' }
188
188
  - uses: actions/setup-python@v5
189
189
  with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
190
- - run: npm i -g "cowork-harness@>=0.25.0"
190
+ - run: npm i -g "cowork-harness@>=0.27.0"
191
191
  - run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
192
192
  - run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
193
193
  - run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
@@ -216,7 +216,7 @@ jobs:
216
216
  echo "live=true" >> "$GITHUB_OUTPUT"
217
217
  fi
218
218
  - if: steps.guard.outputs.live == 'true'
219
- run: npm i -g "cowork-harness@>=0.25.0"
219
+ run: npm i -g "cowork-harness@>=0.27.0"
220
220
  - if: steps.guard.outputs.live == 'true'
221
221
  run: cowork-harness run scenarios/ --output-format json
222
222
  env:
@@ -1,6 +1,6 @@
1
1
  # Fidelity tiers & answer paths
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.25.0` (baseline `desktop-1.18286.0`).
3
+ Self-contained reference. Tracks `cowork-harness 0.27.0` (baseline `desktop-1.18286.0`).
4
4
 
5
5
  ## Fidelity tiers (`fidelity:` in the scenario)
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Scenario & session schema, assertion catalog, web_fetch, full gotchas
2
2
 
3
- Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.25.0`
3
+ Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.27.0`
4
4
  (baseline `desktop-1.18286.0`). If your checkout is newer, prefer the live `docs/scenario.md`,
5
5
  `docs/session.md`, and `SPEC.md`.
6
6
 
@@ -209,6 +209,11 @@ that accidentally returns `"first"` will fail the gate rather than silently gree
209
209
  Each list item under `assert:` is one assertion. **An item with multiple keys is an AND** — it
210
210
  passes only if every key passes. Keep one concern per item unless you mean conjunction.
211
211
 
212
+ Looking for a key *by what you want to prove* (tool health, sub-agent work, panels, skill
213
+ attribution, resources, diagnostics)? SKILL.md §6's "goal → key" map is the by-purpose index into this
214
+ table; the table below is the full per-key reference, and `cowork-harness assertions --list` prints the
215
+ same set live from the schema.
216
+
212
217
  | Assertion | Passes when |
213
218
  |---|---|
214
219
  | `result: success \| error` | the run ended with that status |
@@ -220,6 +225,7 @@ passes only if every key passes. Keep one concern per item unless you mean conju
220
225
  | `user_visible_artifact: <path>` | exists **and** under a user-visible root (`outputs/` + each connected folder's mount name) — the right primitive for a workspace deliverable when a folder is connected |
221
226
  | `no_delete_in_outputs: true` | no delete op touched `mnt/outputs` — **only `true` is valid**; `false` is rejected (omit to allow deletes) |
222
227
  | `no_unexpected_files: [<glob>, …]` | every **newly created** file under a user-visible root matches ≥1 glob (workRoot-relative paths; `**` = whole path segment for any depth — use `outputs/handoff/**` for per-run subdirs); `[]` = no new files; **new-files-only** — overwriting a pre-existing file in place is invisible (use content-level producer stamping); live/verify-run without a pre-run manifest ⇒ evidence-unavailable hard-fail (live runs capture the baseline only when this key is asserted; recordings always capture); **microvm cannot capture** (use container/hostloop); replay needs `cassette.preRunPaths` (≥0.24 container/hostloop recordings) — cassettes without it **exclude** the key with a loud warning |
228
+ | `input_unmodified: [<glob>, …]` | every **pre-existing** file whose workRoot-relative path matches ≥1 glob keeps an unchanged content hash after the run — the in-place-mutation companion to `no_unexpected_files`'s new-files check (`[]` is rejected by the schema — list at least one glob); a matched file that was deleted counts as a content change (fails); live/verify-run without a pre-run hash manifest ⇒ evidence-unavailable hard-fail; **microvm cannot capture**; replay needs `cassette.preRunHashes` — cassettes without it **exclude** the key with a loud warning; on replay it compares against the manifest's recorded `sha256`, never a re-hash of the materialized tree |
223
229
  | `self_heal_ran: <bool>` | a plugin-root self-heal script was (not) invoked |
224
230
  | `tool_called: <Tool>` | the agent invoked the tool (actually ran it) |
225
231
  | `tool_not_called: <Tool>` | the agent never invoked it |
@@ -229,18 +235,34 @@ passes only if every key passes. Keep one concern per item unless you mean conju
229
235
  | `subagent_tool_absent: <Tool>` | no sub-agent used the tool |
230
236
  | `subagent_dispatched: <regex>` | a sub-agent whose `agentType` **or dispatch description** matches |
231
237
  | `subagent_declared_but_unused: <Tool>` | a sub-agent declared the tool but never used **that** tool (even if it used others) |
238
+ | `subagent_output_contains: {match?, contains}` | a dispatched sub-agent's own output contains the substring `contains` — `match` (optional regex over `agentType`/`description`) narrows to specific dispatch(es); omitted, checks whether ANY dispatch's output contains it (existence check, not "all") |
232
239
  | `dispatch_count_max: <N>` | at most N sub-agents dispatched — an author-chosen budget (Cowork imposes no in-conversation Task-dispatch cap; records only, enforces nothing — see gotcha 12) |
233
240
  | `skill_triggered: <regex>` | a skill matching the regex (invoked id, e.g. `"plugin:skill"`) was invoked via the `Skill` tool — evidence-unavailable (not a normal fail) if the agent's init tools have no `Skill` tool |
234
241
  | `no_skill_triggered: <regex>` | no invoked skill id matched — the negative-control / description-collision catcher; evidence-unavailable (never a vacuous pass) if invocation data is absent or the `Skill` tool is unobservable |
242
+ | `skill_available: <regex>` | a staged skill's id matched the regex (offered, not necessarily invoked — see `skill_triggered` for invocation) — content-class: the id list comes from the agent's init `skills` listing, so it replays from the frozen init event (id-only; the `whenToUse` enrichment is live-disk and thus absent on replay, but the id is what's matched); evidence-unavailable only if `RunResult.context.availableSkills` is absent entirely (an older cassette recorded before the available-skills listing was captured) |
243
+ | `connector_available: <regex>` | an MCP server/connector's name matched the regex (available, not necessarily used) — evidence-unavailable if `RunResult.context.mcpServers` is absent |
244
+ | `tool_available: <regex>` | a tool in the init manifest matched the regex (available, not necessarily called — see `tool_called` for invocation) — evidence-unavailable if `RunResult.context.tools` is absent |
245
+ | `skill_tool_used: {skill, tool}` | a tool whose name matches `tool` ran inside a skill-activation window whose `skillId` matches `skill` (`RunResult.skillActivity`) — evidence-unavailable if skill-activity telemetry is absent; heuristic for inline skills (a sticky, sequential window matching the agent's `activeSkill` scope, not an exact per-tool boundary) |
235
246
  | `max_cost_usd: <N>` | the run's SDK-reported cost is ≤ N USD — evidence-unavailable if cost telemetry is absent. **Replay asserts the frozen recording's cost, not fresh spend** — a real regression needs a live `run` |
236
247
  | `max_tokens: <N>` | `usage.input_tokens + usage.output_tokens` ≤ N (cache tokens excluded) — same replay caveat as `max_cost_usd` |
237
248
  | `tool_calls_max: <N>` | total top-level tool calls (sum of `toolCounts`) ≤ N — meaningfully replay-checkable (re-drive recomputes `toolCounts` deterministically) |
249
+ | `tool_no_error: <regex>` | no tool whose name matches the regex recorded any error (`RunResult.toolErrors[name].errors === 0` for every match) — **requires ≥1 matching tool call** (a regex matching nothing fails, so a typo can't silently pass); evidence-unavailable if tool-error telemetry is absent |
250
+ | `tool_no_error_if_called: <regex>` | like `tool_no_error` but passes vacuously when no tool matches the regex — the presence-free variant |
251
+ | `max_tool_errors: <N>` | total tool errors across all tools (sum of `RunResult.toolErrors[*].errors`) ≤ N — evidence-unavailable if tool-error telemetry is absent |
252
+ | `max_redundant_tool_calls: <N>` | total WASTED repeated tool calls (sum of `(count-1)` across every redundant `{name,args}` group in `RunResult.redundantToolCalls`) ≤ N — not the raw count of redundant groups; evidence-unavailable if redundant-call telemetry is absent |
238
253
  | `max_turns: <N>` | the SDK-reported (or fallback-counted) turn count ≤ N — meaningfully replay-checkable (re-drive recounts turns deterministically, same as `tool_calls_max`) |
254
+ | `compaction_occurred: true` | a context-compaction boundary occurred (a `compact_boundary` system event was recorded) — lives in the stdout stream, so meaningfully replay-checkable; evidence-unavailable if context-event telemetry is absent. **Only `true` is valid** — omit to not require it |
255
+ | `all_tasks_completed: true` | every task in `RunResult.tasks[]` reached status `"completed"` — **requires ≥1 task** (a zero-task run fails; assert `task_count_min` for presence); evidence-unavailable if tasks telemetry is absent |
256
+ | `task_count_min: <N>` | at least N tasks were created (`RunResult.tasks.length >= N`) — the presence companion for task assertions |
257
+ | `task_status: {match, status}` | a task whose `subject` OR `id` matches the regex `match` reached `status` — evidence-unavailable if tasks telemetry is absent |
258
+ | `no_scratchpad_leak: true` | every file presented via `present_files` that was in the scratchpad was successfully promoted to `mnt/outputs` (none left behind) — vacuously passes if nothing was presented (pair with a presence check to require a delivery); content-class: both the `present_files` tool_use and its own tool_result live in the ordinary events stream, so `RunResult.presentedFiles` re-derives identically on replay (meaningfully replay-checkable, same as `skill_triggered`); evidence-unavailable if `presentedFiles` telemetry is absent (an older run predating the feature). **`fidelity: container` only** — `present_files` is not served on hostloop/microvm, so a scratchpad-delivered file is neither promoted to `mnt/outputs` nor detected there (a skill that delivers via write-to-cwd→`present_files` will false-red `user_visible_artifact` on those tiers; use `container`, or write directly to `outputs/`). **Only `true` is valid** |
239
259
  | `question_asked: <regex>` | the agent asked an AskUserQuestion whose text matches |
240
260
  | `questions_count_max: <N>` | at most N **sub-questions** asked — a bundled `AskUserQuestion` with K sub-questions counts as K, not 1; `trace --view questions`'s footer total uses the same definition |
241
261
  | `gate_answers_delivered: true` | every answered gate's answer reached the model (observed `tool_result`; unobserved = fail); **zero gates fired passes vacuously** — pair with `gate_answer_count_min` to also require a gate |
242
262
  | `gate_answers_delivered: false` | asserts at least one answered gate's answer was **confirmed not delivered** (an observed delivery failure); an unobserved/null delivery does **not** satisfy this — for negative-path delivery tests |
243
263
  | `gate_answer_count_min: <N>` | at least N AskUserQuestion gates fired AND were delivered non-error — presence companion to `gate_answers_delivered`'s vacuous-pass |
264
+ | `hook_blocked: <regex>` | a PreToolUse hook blocked a tool whose name matches the regex (`RunResult.hookEvents`) — evidence-unavailable if hook telemetry is absent. Replay: needs a `controlOut` cassette (a custom hook's decision lives only there, not the recorded stream) |
265
+ | `no_hook_blocked: true` | no tool was hook-blocked during the run (distinguishes a real tool crash from an intentional hook block) — evidence-unavailable if hook telemetry is absent. Replay: needs a `controlOut` cassette. **Only `true` is valid** |
244
266
  | `allow_permissive_auto_allow: true` | verdict modifier — suppresses the default-fail when the run recorded a cowork-parity permissive auto-allow; for tests that deliberately assert Cowork's permissive behavior |
245
267
  | `allow_missing_capability: true` | verdict modifier — suppresses the default-fail when the (partial "core") agent image omits a capability the skill used but real Cowork ships (OCR/LibreOffice/markitdown/opencv/PDF-tables). Assert only when the skill's fallback is genuinely equivalent; otherwise rebuild full parity (`--build-arg COWORK_FULL_PARITY=1`). Also opts out of the `requires_capabilities` declared-need check. Live tiers only |
246
268
  | `allow_l0_plugin_divergence: true` | verdict modifier — opt into L0/protocol plugin divergence: suppresses the default-fail when a plugin behaves differently at `protocol` (L0) fidelity than under a sandboxed tier. Live tiers only |
@@ -248,8 +270,11 @@ passes only if every key passes. Keep one concern per item unless you mean conju
248
270
  | `transcript_no_host_path: true` | no host path (`/Users/`, `/opt/cowork/`, `/home/`, `/root/`) leaked into model-visible text — **incompatible with `hostloop` AND `protocol`**: hostloop's native file tools legitimately expose real host paths, and protocol (L0) runs the agent's file tools on the real host cwd with no sealed filesystem, so this fails BY DESIGN on both (the harness warns at run start if asserted anyway); use `container`/`microvm` for this check |
249
271
  | `egress_denied: <host>` | the host was blocked by the egress proxy |
250
272
  | `egress_allowed: <host>` | the host was allowed through |
273
+ | `no_mcp_error: true` | no MCP round-trip failed (`RunResult.mcpErrors` is empty — no unhandled server, no handler throw) — live-only: MCP round-trips are harness-computed, not in the SDK stdout stream, so evidence-unavailable on replay (never a vacuous pass). **Only `true` is valid** |
274
+ | `max_peak_rss_bytes: <N>` | peak sampled RSS of the agent sandbox ≤ N bytes (`RunResult.resources.peakRssBytes`) — live-only: replay never spawns a sandbox to sample, so evidence-unavailable on replay/protocol (never a vacuous pass); also evidence-unavailable when sampling captured no RSS value |
251
275
  | `artifact_json: {artifact, path, …}` | assert a JSON artifact's contents — `equals`/`gt`/`in`/`exists`/`absent`/`is_null` over a dotted `path` (`in` = membership in a list, for a stochastic/LLM value; `absent` ≠ `is_null`; an unresolved intermediate fails loud) |
252
- | `computer_links_resolve: true` | every `computer://` link in the model-visible transcript resolves to an artifact that exists in the run's collected outputs/mounts — a dangling link fails, naming which target was checked (a live host path, the collected work tree, or the replay manifest). Zero links in the transcript **passes** (presence-gated separately — pair with `transcript_contains` if you also need a link to show up). **Only `true` is valid** (`false` is rejected by the schema) |
276
+ | `computer_links_resolve: true` | every `computer://` link in the model-visible transcript resolves to an artifact that exists in the run's collected outputs/mounts — a dangling link fails, naming which target was checked (a live host path, the collected work tree, or the replay manifest). **Requires ≥1 link** (zero links fails — use `computer_links_resolve_if_present` for the presence-free variant). **Only `true` is valid** (`false` is rejected by the schema) |
277
+ | `computer_links_resolve_if_present: true` | like `computer_links_resolve` but passes vacuously when the transcript has zero `computer://` links — the presence-free variant. **Only `true` is valid** |
253
278
 
254
279
  `expect_denied: [host, …]` adds one `egress_denied` per host. Run `cowork-harness assertions --list` for this
255
280
  table from the live schema. Example: `artifact_json: { artifact: outputs/cap.json, path: me.run_id, equals: "r1" }`.
@@ -288,16 +313,21 @@ sourcing ≠ evaluation (replay warns when you edit one). `verify-run` is the on
288
313
  *run dir*; `--assert-from` is the equivalent for a *cassette*.
289
314
 
290
315
  **Evaluated on replay (content):** `transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`,
291
- `skill_triggered`, `no_skill_triggered`, `max_cost_usd`, `max_tokens`, `tool_calls_max`, `max_turns`, `result`
316
+ `skill_triggered`, `no_skill_triggered`, `skill_available`, `connector_available`, `tool_available`,
317
+ `skill_tool_used`, `max_cost_usd`, `max_tokens`, `tool_calls_max`, `tool_no_error`,
318
+ `max_tool_errors`, `max_redundant_tool_calls`, `max_turns`, `compaction_occurred`, `all_tasks_completed`, `task_status`, `result`
292
319
  (`max_cost_usd`/`max_tokens` assert the frozen recording's spend on replay, not fresh spend). The verdict
293
320
  modifiers `allow_permissive_auto_allow` / `allow_missing_capability` / `allow_l0_plugin_divergence` /
294
321
  `allow_stall` are also kept on replay, evaluated as no-op passes.
295
322
 
296
323
  **Gate keys — replay only with a `controlOut` cassette:** `question_asked`, `questions_count_max`,
297
- `gate_answers_delivered`, `gate_answer_count_min`. With `controlOut` present they evaluate; on an old
324
+ `gate_answers_delivered`, `gate_answer_count_min`, `hook_blocked`, `no_hook_blocked`. With `controlOut` present they evaluate; on an old
298
325
  cassette without it, a **loud warning** fires and they are **excluded** (not vacuously passed). Re-record to enable them.
299
326
  `questions_count_max` counts sub-questions, not gates/tool-calls — see the catalog row above and
300
- `trace --view questions`.
327
+ `trace --view questions`. `hook_blocked`/`no_hook_blocked` need `controlOut` for a different reason than
328
+ the question keys: a custom hook's block/allow decision is an opaque async reply recorded only in
329
+ `control-out.jsonl`, not the `events` stream — reconstructing from the stream alone would show only the
330
+ built-in Task hook's view and could vacuously pass `no_hook_blocked` even if a custom hook genuinely blocked.
301
331
 
302
332
  **Filesystem — replay-checkable WITH an artifact manifest:** `file_exists`, `user_visible_artifact`,
303
333
  `artifact_json`, `computer_links_resolve` run on replay when the cassette carries an `artifacts` snapshot
@@ -308,13 +338,16 @@ by first normalizing it to a mount-relative path (recorded connected-folder pref
308
338
  mounts) — replay has no live filesystem to check a host path against directly (that only happens on a live
309
339
  `run`/`verify-run`). Without a manifest (older cassettes) all five are skipped; `no_unexpected_files` also
310
340
  needs `preRunPaths` (≥0.24 recordings) — without it the key is excluded with a loud warning (live/verify-run
311
- hard-fails evidence-unavailable instead). A green replay re-confirms
341
+ hard-fails evidence-unavailable instead). `input_unmodified` is the same shape but needs `preRunHashes`
342
+ (the pre-run per-path sha256 baseline) instead of `preRunPaths`; without it, likewise excluded with a loud
343
+ warning. A green replay re-confirms
312
344
  *record-time* artifacts, not that the current skill still produces them — `replay --strict` fails when the
313
345
  staleness `fingerprint` shows ANY skill/baseline drift, or `replay --fail-on-skill-drift` only on
314
346
  skill-source drift; every replay result also reports it class-tagged in `staleness[]` for a JSON gate.
315
347
 
316
348
  **Egress + other filesystem — still skipped on replay (live-only):** `no_delete_in_outputs`,
317
- `self_heal_ran`, `transcript_no_host_path`, `egress_*` / `expect_denied`. These run only on a live `run`/`record`.
349
+ `self_heal_ran`, `transcript_no_host_path`, `egress_*` / `expect_denied`, `no_mcp_error`, `max_peak_rss_bytes`.
350
+ These run only on a live `run`/`record`.
318
351
 
319
352
  **Mixed assertions on replay:** before evaluating, `replay` strips each assertion to its replay-checkable
320
353
  keys and drops any left empty. So `{result, egress_denied}` evaluates on replay as `{result}` alone — its
@@ -377,8 +410,9 @@ at the top of this file.
377
410
  concern per item; run the linter. (`contentKeys` in `src/run/cassette.ts`.)
378
411
 
379
412
  2. **Gate keys need a `controlOut` cassette.** `question_asked`, `questions_count_max`,
380
- `gate_answers_delivered`, `gate_answer_count_min` only evaluate on replay with `controlOut`; on an
381
- old cassette they warn and are excluded (not passed). `gate_answers_delivered` **fails on
413
+ `gate_answers_delivered`, `gate_answer_count_min`, `hook_blocked`, `no_hook_blocked` only evaluate on
414
+ replay with `controlOut`; on an old cassette they warn and are excluded (not passed).
415
+ `gate_answers_delivered` **fails on
382
416
  unobserved delivery** (`delivered: null`) — absence of evidence is failure — but **passes
383
417
  vacuously when zero gates fired**; use `gate_answer_count_min: 1` to also require a gate to have
384
418
  fired. A **header-only gate** (empty `question`, only `header`) can never be keyed and is rejected
@@ -47,11 +47,11 @@ what production would do. Two layers of defense:
47
47
 
48
48
  ### Cassette anatomy (what you're looking at when you open one)
49
49
 
50
- Top-level fields of a `*.cassette.json` (schema `schema/cassette.v7.json`):
50
+ Top-level fields of a `*.cassette.json` (schema `schema/cassette.v9.json`):
51
51
 
52
52
  | Field | What it is |
53
53
  |---|---|
54
- | `$schema`, `generator`, `cassetteVersion` | Provenance: schema URL, producing tool, format version (current: 7) |
54
+ | `$schema`, `generator`, `cassetteVersion` | Provenance: schema URL, producing tool, format version (current: 9) |
55
55
  | `scenario` | The embedded scenario snapshot at record time |
56
56
  | `events` | The recorded agent event stream (the replay source) |
57
57
  | `controlOut` | Driver→agent control responses — presence unlocks gate asserts on replay |
@@ -63,6 +63,8 @@ Top-level fields of a `*.cassette.json` (schema `schema/cassette.v7.json`):
63
63
  | `preRunPaths` | Pre-run file-path baseline for `no_unexpected_files` (workRoot-relative; co-present with `userVisibleRoots`) |
64
64
  | `scenarioSource` | Relative path to the authored YAML this was recorded from |
65
65
  | `authoring` | Present iff a live decider answered ≥1 gate during recording (`nonDeterministic: true`) |
66
+ | `sessionFingerprint` | v9+: hash of the session's content-relevant SHAPE (folders/plugins/skills/mcp/egress). Checked ONLY by `verify-cassettes`, never the default replay verdict; absent on a pre-v9 cassette → not checked |
67
+ | `folderPrefixMap` | v9+: the record-time connected-folder host-path → mount-name map. Replay's `computer_links_resolve` uses THIS (never the current session file) for a v9+ cassette; a pre-v9 cassette keeps reconstructing from the current session |
66
68
 
67
69
  ## Recipe 3 — Set up redaction BEFORE your first hostloop/protocol record
68
70
 
@@ -1,22 +1,34 @@
1
1
  {
2
2
  "$comment": "GENERATED from the Zod schemas (src/types.ts) by scripts/gen-schema.ts — do not edit; run `npm run schema`.",
3
3
  "keys": [
4
+ "all_tasks_completed",
4
5
  "allow_l0_plugin_divergence",
5
6
  "allow_missing_capability",
6
7
  "allow_permissive_auto_allow",
7
8
  "allow_stall",
8
9
  "artifact_json",
10
+ "compaction_occurred",
9
11
  "computer_links_resolve",
12
+ "computer_links_resolve_if_present",
13
+ "connector_available",
10
14
  "dispatch_count_max",
11
15
  "egress_allowed",
12
16
  "egress_denied",
13
17
  "file_exists",
14
18
  "gate_answer_count_min",
15
19
  "gate_answers_delivered",
20
+ "hook_blocked",
21
+ "input_unmodified",
16
22
  "max_cost_usd",
23
+ "max_peak_rss_bytes",
24
+ "max_redundant_tool_calls",
17
25
  "max_tokens",
26
+ "max_tool_errors",
18
27
  "max_turns",
19
28
  "no_delete_in_outputs",
29
+ "no_hook_blocked",
30
+ "no_mcp_error",
31
+ "no_scratchpad_leak",
20
32
  "no_skill_triggered",
21
33
  "no_unexpected_files",
22
34
  "question_asked",
@@ -24,13 +36,21 @@
24
36
  "replay_protocol_fidelity",
25
37
  "result",
26
38
  "self_heal_ran",
39
+ "skill_available",
40
+ "skill_tool_used",
27
41
  "skill_triggered",
28
42
  "subagent_declared_but_unused",
29
43
  "subagent_dispatched",
44
+ "subagent_output_contains",
30
45
  "subagent_tool_absent",
31
46
  "subagent_tool_used",
47
+ "task_count_min",
48
+ "task_status",
49
+ "tool_available",
32
50
  "tool_called",
33
51
  "tool_calls_max",
52
+ "tool_no_error",
53
+ "tool_no_error_if_called",
34
54
  "tool_not_called",
35
55
  "tool_result_contains",
36
56
  "tool_result_not_contains",
@@ -53,7 +73,8 @@
53
73
  "prompt",
54
74
  "requires_capabilities",
55
75
  "session",
56
- "skills"
76
+ "skills",
77
+ "timeout_ms"
57
78
  ],
58
79
  "verdictModifierKeys": [
59
80
  "allow_l0_plugin_divergence",
@@ -63,21 +63,50 @@ CONTENT_KEYS = {
63
63
  "subagent_tool_absent",
64
64
  "subagent_dispatched",
65
65
  "subagent_declared_but_unused",
66
+ "subagent_output_contains",
66
67
  "dispatch_count_max",
67
68
  "skill_triggered",
68
69
  "no_skill_triggered",
70
+ "skill_available",
71
+ "connector_available",
72
+ "tool_available",
73
+ "skill_tool_used",
69
74
  "max_cost_usd",
70
75
  "max_tokens",
71
76
  "tool_calls_max",
77
+ "tool_no_error",
78
+ "tool_no_error_if_called",
79
+ "max_tool_errors",
80
+ "max_redundant_tool_calls",
72
81
  "max_turns",
82
+ "compaction_occurred",
83
+ "all_tasks_completed",
84
+ "task_count_min",
85
+ "task_status",
86
+ "no_scratchpad_leak",
73
87
  }
74
88
  # content keys, but only evaluated on replay when the cassette carries controlOut
75
- GATE_KEYS = {"question_asked", "questions_count_max", "gate_answers_delivered", "gate_answer_count_min"}
89
+ GATE_KEYS = {
90
+ "question_asked",
91
+ "questions_count_max",
92
+ "gate_answers_delivered",
93
+ "gate_answer_count_min",
94
+ "hook_blocked",
95
+ "no_hook_blocked",
96
+ }
76
97
  # manifest-backed: replay-checkable when the cassette carries an `artifacts` manifest (record snapshots one);
77
98
  # a manifest-less cassette skips them. Since the 0.3.0 artifact-manifest these are NOT always live-only.
78
99
  # computer_links_resolve joins this bucket (not CONTENT_KEYS): resolving a non-empty link set needs
79
100
  # either a live filesystem or the cassette's artifacts manifest — see cassette.ts's manifestKeys comment.
80
- MANIFEST_KEYS = {"file_exists", "user_visible_artifact", "artifact_json", "computer_links_resolve", "no_unexpected_files"}
101
+ MANIFEST_KEYS = {
102
+ "file_exists",
103
+ "user_visible_artifact",
104
+ "artifact_json",
105
+ "computer_links_resolve",
106
+ "computer_links_resolve_if_present",
107
+ "no_unexpected_files",
108
+ "input_unmodified",
109
+ }
81
110
  # live-only: ALWAYS skipped on replay, with a loud warning (no filesystem, no network on the token-free lane)
82
111
  LIVE_ONLY_KEYS = {
83
112
  "egress_denied",
@@ -85,6 +114,8 @@ LIVE_ONLY_KEYS = {
85
114
  "no_delete_in_outputs",
86
115
  "self_heal_ran",
87
116
  "transcript_no_host_path",
117
+ "no_mcp_error",
118
+ "max_peak_rss_bytes",
88
119
  }
89
120
  EGRESS_KEYS = {"egress_denied", "egress_allowed"}
90
121
  # verdict modifiers — don't verify anything themselves (e.g. suppress a default-fail)
@@ -141,6 +172,7 @@ _EMBEDDED_TOP_LEVEL_KEYS = {
141
172
  "fidelity",
142
173
  "on_unanswered",
143
174
  "prompt",
175
+ "timeout_ms", # wall-clock budget → kill + errorSource:timeout on expiry
144
176
  "answers",
145
177
  "expect_denied",
146
178
  "assert",
@@ -178,6 +210,7 @@ REGEX_KEYS = {
178
210
  "when_question",
179
211
  "subagent_dispatched",
180
212
  "question_asked",
213
+ "hook_blocked",
181
214
  }
182
215
  VALID_ON_UNANSWERED = {"fail", "prompt", "first", "llm"}
183
216
  VALID_TIERS = ("protocol", "container", "microvm", "hostloop", "cowork")
package/.env.example CHANGED
@@ -1,6 +1,8 @@
1
1
  # cowork-harness host-side credentials.
2
2
  #
3
- # Copy to `.env` (auto-loaded from the working dir at startup; already exported vars win).
3
+ # Copy to `.env`. Resolution order: exported env > `--dotenv <path>` > `./.env` (cwd) >
4
+ # `<install-root>/.env` — so a global/npm-linked install still finds this file at the package
5
+ # root even when you run from an unrelated directory. Already-exported vars always win.
4
6
  # `.env` is gitignored. SECURITY: keep this at your repo/working-dir ROOT — never inside a
5
7
  # mounted skill/project folder, or its contents would be copied into the agent's sandbox.
6
8
  # The token value is also scrubbed from all persisted run logs regardless of source.
@@ -12,5 +14,9 @@ CLAUDE_CODE_OAUTH_TOKEN=
12
14
  # token is present, mirroring the desktop).
13
15
  # ANTHROPIC_API_KEY=
14
16
 
17
+ # Alternative: a raw Anthropic auth token. Same precedence as ANTHROPIC_API_KEY (used only when
18
+ # no OAuth token is set) — set at most one of the two.
19
+ # ANTHROPIC_AUTH_TOKEN=
20
+
15
21
  # Optional: timezone passed through to the agent.
16
22
  # TZ=America/New_York