cowork-harness 0.22.0 → 0.24.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 (72) hide show
  1. package/.claude/skills/cowork-harness/SKILL.md +54 -31
  2. package/.claude/skills/cowork-harness/references/ci-recipe.md +28 -7
  3. package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +4 -3
  4. package/.claude/skills/cowork-harness/references/scenario-schema.md +25 -10
  5. package/.claude/skills/cowork-harness/references/task-recipes.md +108 -0
  6. package/.claude/skills/cowork-harness/scripts/assertion-keys.json +1 -0
  7. package/.claude/skills/cowork-harness/scripts/scenario.py +85 -9
  8. package/.cowork-redact.json +11 -0
  9. package/AGENTS.md +2 -2
  10. package/CHANGELOG.md +342 -4
  11. package/CONTRIBUTING.md +1 -0
  12. package/DESIGN.md +2 -2
  13. package/README.md +33 -17
  14. package/SPEC.md +99 -32
  15. package/baselines/desktop-1.18286.0.json +54 -8
  16. package/baselines/prompts/desktop-1.18286.0/system-prompt-append.md +1 -2
  17. package/dist/assert.js +17 -1
  18. package/dist/cli-args.js +1 -1
  19. package/dist/cli.js +138 -47
  20. package/dist/decide/llm-transport.js +19 -4
  21. package/dist/errors.js +12 -0
  22. package/dist/glob.js +43 -0
  23. package/dist/loop-decision.js +0 -3
  24. package/dist/run/artifacts.js +109 -0
  25. package/dist/run/cassette.js +351 -76
  26. package/dist/run/chat.js +13 -3
  27. package/dist/run/display-translate.js +180 -1
  28. package/dist/run/doctor.js +25 -1
  29. package/dist/run/execute.js +53 -89
  30. package/dist/run/pre-run-manifest.js +61 -0
  31. package/dist/run/renderer.js +8 -3
  32. package/dist/run/trace-view.js +17 -7
  33. package/dist/run/verdict.js +8 -6
  34. package/dist/run/vm-path-ctx-file.js +123 -0
  35. package/dist/runtime/argv.js +13 -0
  36. package/dist/runtime/container.js +3 -0
  37. package/dist/runtime/hostloop.js +15 -2
  38. package/dist/runtime/lima.js +3 -3
  39. package/dist/runtime/microvm.js +5 -0
  40. package/dist/runtime/protocol.js +3 -0
  41. package/dist/scan.js +31 -9
  42. package/dist/session.js +6 -0
  43. package/dist/sync/cowork-sync.js +550 -11
  44. package/dist/types.js +7 -21
  45. package/dist/vm-paths.js +8 -4
  46. package/docker/Dockerfile.agent +8 -4
  47. package/docs/README.md +1 -1
  48. package/docs/cassette.md +48 -28
  49. package/docs/chat.md +4 -3
  50. package/docs/cowork-spawn-contract-1.12603.1.md +1 -1
  51. package/docs/debugging.md +10 -0
  52. package/docs/discovery.md +6 -0
  53. package/docs/fidelity-gaps.md +98 -0
  54. package/docs/invariants.md +1 -1
  55. package/docs/maintenance.md +1 -1
  56. package/docs/run-status.md +33 -0
  57. package/docs/scenario.md +15 -10
  58. package/docs/session.md +2 -0
  59. package/examples/replays/README.md +1 -1
  60. package/examples/replays/hostloop-computer-links.cassette.json +77 -0
  61. package/llms.txt +1 -1
  62. package/package.json +3 -2
  63. package/python/test_scenario_lint.py +92 -0
  64. package/schema/cassette.v7.json +18 -0
  65. package/schema/run-result.json +32 -0
  66. package/schema/scenario.schema.json +9 -1
  67. package/schema/verify-cassettes.json +70 -0
  68. package/scripts/boot-rootfs-vz.ts +4 -4
  69. package/scripts/build-rootfs-image.ts +1 -1
  70. package/scripts/capture-rootfs-manifest.ts +1 -1
  71. package/scripts/check-versions.ts +80 -3
  72. package/scripts/gen-schema.ts +4 -3
@@ -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.22.0
7
- tracks-harness: cowork-harness 0.22.0 (baseline desktop-1.18286.0)
6
+ version: 0.24.0
7
+ tracks-harness: cowork-harness 0.24.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.22.0` (baseline
25
+ > **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.24.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,7 +38,7 @@ 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.22.0**. If it's missing or older, prefix every command with the version floor `npx cowork-harness@>=0.22.0 <cmd>` (Node ≥ 20), or install once with `npm i -g cowork-harness@>=0.22.0`. **Pin `@>=0.22.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.22.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-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0), 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.24.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.24.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.24.0"`. **Pin `@>=0.24.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.24.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-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0), 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`).
@@ -55,7 +55,12 @@ Before the first command, confirm the CLI is reachable and **fail loud** (never
55
55
 
56
56
  Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
57
57
  verify-run · trace · inspect · diff · stats · decide · gates · answer · scaffold · assertions --list · sync ·
58
- list · boundary-check · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
58
+ list · boundary-check · status · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
59
+
60
+ **Two different `scaffold` tools — don't confuse them.** The native `cowork-harness scaffold <run-id>`
61
+ above turns an already-*recorded* run into a scenario (needs a run to exist first). The bundled
62
+ `scripts/scenario.py scaffold --name … --skill …` in §9 builds a scenario from flags alone, no run
63
+ required. Passing §9's flag set to the native command fails with `unknown flag: --name` (exit 2).
59
64
 
60
65
  ## 2. Two files: session vs scenario
61
66
 
@@ -94,7 +99,8 @@ the folder basename (collision-resolved); there is no `to:` override. See `refer
94
99
  | `hostloop` / `cowork` | Production split-exec: the agent loop is a **native process on the host** (no container around the file tools — matching production), with native Bash/WebFetch disabled and routed host-side via the workspace SDK-MCP server into a Docker VM sidecar | Highest-fidelity / parity runs. A writable connected folder needs `allow_host_writes: true` (see scenario-schema.md). |
95
100
 
96
101
  Set the tier in the **scenario's `fidelity:` field**, not a flag — `run` rejects `--fidelity`
97
- (it's a `skill`-only flag). See `references/fidelity-and-answers.md`.
102
+ (it's a `skill`/`chat` flag; `run` takes fidelity only from the scenario). See
103
+ `references/fidelity-and-answers.md`.
98
104
 
99
105
  ## 5. Choose an answer path (gates: AskUserQuestion + tool-permission)
100
106
 
@@ -265,7 +271,7 @@ known-good skeleton (right tier, scripted `answers:` + `on_unanswered: fail`, co
265
271
  separated from live-only ones, one concern per item) and **self-lints its own output**:
266
272
 
267
273
  ```bash
268
- S="${CLAUDE_PLUGIN_ROOT}/skills/cowork-harness/scripts/scenario.py"
274
+ S="${CLAUDE_PLUGIN_ROOT}/scripts/scenario.py"
269
275
  # Working from a repo checkout instead of an installed plugin?
270
276
  # S=".claude/skills/cowork-harness/scripts/scenario.py"
271
277
  python3 "$S" scaffold --name report-check --skill ./skills/report-gen \
@@ -282,10 +288,14 @@ cowork-harness lint scenarios/*.yaml
282
288
  ```
283
289
 
284
290
  `lint` flags: filesystem/egress-only assertions on a `replay` gate (silent no-op), bad regex
285
- quoting, an egress assert on `protocol` fidelity, a `controlOut`-gated key on a non-`controlOut`
286
- replay, mixed-class assertion items, and hallucinated schema (`assertions:` vs `assert:`, unknown
287
- keys). Exit code is non-zero on errors (CI-friendly). `scaffold` auto-upgrades the tier if you ask
288
- for egress on `protocol`, so it never emits a scenario `lint` would reject.
291
+ quoting, an egress assert on `protocol` fidelity, `transcript_no_host_path` on `hostloop`/`protocol`
292
+ (ERROR — fails by design at those tiers; WARN on `fidelity: cowork`, whose tier resolves per the
293
+ baseline's host-loop gate), non-empty `requires_capabilities` on `protocol` without
294
+ `allow_missing_capability` (ERROR — the capability probe can't run there, so the run hard-fails as
295
+ unverifiable), a `controlOut`-gated key on a non-`controlOut` replay, mixed-class assertion items,
296
+ and hallucinated schema (`assertions:` vs `assert:`, unknown keys). Exit code is non-zero on errors
297
+ (CI-friendly). `scaffold` auto-upgrades the tier if you ask for egress on `protocol`, so it never
298
+ emits a scenario `lint` would reject.
289
299
 
290
300
  CI placement: a **token-free `replay` PR gate** (content/structure only) + a **nightly live `run`**
291
301
  (filesystem/egress). Fastest setup: `uses: yaniv-golan/cowork-harness@v1` (a packaged GitHub Action with a
@@ -356,9 +366,12 @@ are the ones that bite hardest.
356
366
  sub-agent used other tools. And `subagent_dispatched` matches by dispatch **description** too —
357
367
  skills often dispatch with no `subagent_type` (`agentType:"unknown"`), so match the description.
358
368
 
359
- 6. **`dispatch_count_max` asserts but does NOT enforce.** The harness records the count; it does not
360
- reproduce Cowork's skip-on-cap (`{perTask:1, global:3}`, deferred). Passing means "happened to
361
- dispatch ≤N this run," not "the harness capped it." Don't read enforcement into the assert.
369
+ 6. **`dispatch_count_max` is an author-chosen budget, not a production cap.** It's a post-hoc count
370
+ assertion: passing means "happened to dispatch ≤N this run," nothing more. Cowork imposes **no**
371
+ in-conversation `Task`-dispatch cap — gate `1648655587`'s `{perTask:1, global:3}` governs the
372
+ separate scheduled/cron-task session scheduler, not the `Task` tool (binary-verified; see SPEC
373
+ §10). So there is no "skip-on-cap" for the harness to reproduce; use this key only to catch a
374
+ fan-out you don't want.
362
375
 
363
376
  7. **`protocol` is rejected (not silently passed) if the scenario asserts egress** — boundary
364
377
  assertions need a sandboxed tier (`container`+). Good: this one fails loud by design.
@@ -392,23 +405,29 @@ are the ones that bite hardest.
392
405
  prompt. *Fix:* treat `prompt_asset_missing` as a blocking error in CI by checking the signals
393
406
  array.
394
407
  13. **`result: success` means the agent didn't error, NOT that the task completed — always assert on
395
- artifacts/content.** A turn that ends on a plain-text re-ask ("which file did you mean?") still
396
- reports `result: success`. The harness catches a run that ends on a question and did **no productive
397
- work after its last gate** — both the no-gate case ("which file?" with no tool calls) AND the
398
- *answered-gate-then-re-ask* case (the agent answers an `AskUserQuestion`, then asks again in plain text
399
- and stops) — with a **`stalled`** verdict signal (suppress with `allow_stall: true` if ending on a
400
- question is intended). The signal is a **tool-position heuristic**, not deliverable detection, so it is
401
- imprecise both ways: (a) a post-gate tool *call* clears the flag whether it **succeeded or errored** (false
402
- negative — an agent that ran a tool after the gate and still stalled is not caught); and (b) a deliverable
403
- written *before* a final confirmation gate does **not** clear it, so a write-then-confirm-then-question run
404
- is flagged (false positive — use `allow_stall: true` for a deliberate confirm-terminal skill). The broad
405
- guard is therefore YOUR assertions — assert the deliverable
406
- (`file_exists` / `artifact_json` / `transcript_matches`), never just `result: success`. Note:
407
- `on_unanswered` governs **unanswered** `AskUserQuestion` gates; the `stalled` signal covers stalling
408
- *after* one is answered. A "type-it-in-notes" option has **no scripted deterministic answer** today (the
409
- `OTHER:` directive works only on the LLM-decider path, not scripted `choose:`; on an options-bearing gate
410
- a bare out-of-set LLM answer fails loud (exit 2) — see the LLM-decider free-text note in
411
- `references/fidelity-and-answers.md`).
408
+ artifacts/content.**
409
+ - A turn that ends on a plain-text re-ask ("which file did you mean?") still reports
410
+ `result: success`.
411
+ - The harness catches this with a **`stalled`** verdict signal: a run that ends on a question and
412
+ did **no productive work after its last gate** — both the no-gate case ("which file?" with no
413
+ tool calls) AND the *answered-gate-then-re-ask* case (the agent answers an `AskUserQuestion`,
414
+ then asks again in plain text and stops). Suppress with `allow_stall: true` if ending on a
415
+ question is intended.
416
+ - The signal is a **tool-position heuristic**, not deliverable detection, so it is imprecise both
417
+ ways:
418
+ - **False negative:** a post-gate tool *call* clears the flag whether it **succeeded or
419
+ errored** — an agent that ran a tool after the gate and still stalled is not caught.
420
+ - **False positive:** a deliverable written *before* a final confirmation gate does **not**
421
+ clear it, so a write-then-confirm-then-question run is flagged — use `allow_stall: true` for a
422
+ deliberate confirm-terminal skill.
423
+ - The broad guard is therefore YOUR assertions — assert the deliverable (`file_exists` /
424
+ `artifact_json` / `transcript_matches`), never just `result: success`.
425
+ - `on_unanswered` governs **unanswered** `AskUserQuestion` gates; the `stalled` signal covers
426
+ stalling *after* one is answered — two different failure modes.
427
+ - **Free-text aside:** a "type-it-in-notes" option has **no scripted deterministic answer** today
428
+ (the `OTHER:` directive works only on the LLM-decider path, not scripted `choose:`; on an
429
+ options-bearing gate a bare out-of-set LLM answer fails loud (exit 2) — see the LLM-decider
430
+ free-text note in `references/fidelity-and-answers.md`).
412
431
  14. **A positional `choose` (`first` / index) is order-dependent.** `choose: "2"` survives label drift
413
432
  but NOT option *re-ordering* — if the gate presents its options in a different order run-to-run, the
414
433
  index lands on a different option (a silent re-record flake). Prefer an exact label when order is
@@ -444,6 +463,10 @@ and the CI recipe, read the files in `references/`.
444
463
 
445
464
  ## References
446
465
 
466
+ - `references/task-recipes.md` — end-to-end recipes for the four jobs fleet owners actually hit:
467
+ evolve a cassette's `assert:` (usually no re-record), audit a fleet for tier drift, set up
468
+ redaction before the first hostloop/protocol record, derive budget assertions without a
469
+ two-pass record. Start here when the question is "how do I do X", not "what does flag Y mean".
447
470
  - `references/scenario-schema.md` — scenario/session YAML schema, full assertion catalog (with each
448
471
  key's replay class), the web_fetch model, and the complete gotcha list.
449
472
  - `references/fidelity-and-answers.md` — fidelity tiers, answer paths, the determinism contract.
@@ -1,6 +1,6 @@
1
1
  # CI recipe — replay vs live lanes
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.22.0` (baseline `desktop-1.18286.0`).
3
+ Self-contained reference. Tracks `cowork-harness 0.24.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.22.0
56
+ - run: npm i -g "cowork-harness@>=0.24.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
@@ -125,11 +125,20 @@ dollar figures). In a skill repo these cassettes get **committed**. So:
125
125
  customer's cap table.
126
126
  - **Opt-in redaction** rewrites configured PII out of the cassette at record time. Drop a
127
127
  `.cowork-redact.json` next to your scenarios (or set `COWORK_HARNESS_REDACT_PATTERNS` /
128
- `_KEYS`); empty by default. Redaction is **verdict-preserving** — `record` refuses to write if
128
+ `_KEYS`); empty by default. The policy is searched in **cwd → the scenario's dir → the cassette's
129
+ dir** (first file found per dir; env vars merge on top). `cowork-harness init-redact` copies the
130
+ packaged reference template (local-path prefixes + a generic email regex) into the cwd as a starting
131
+ point — review and tailor it. Redaction is **verdict-preserving** — `record` refuses to write if
129
132
  redaction would flip an assertion (a manufactured green). `--no-redact` skips it for known-synthetic
130
133
  inputs.
131
- - **Always-on scan gate** — `verify-cassettes` flags email / currency / bare-domain / local-path matches
132
- it finds in the committed cassettes and **exits non-zero**, so "no leak" is a gate, not discipline.
134
+ - **Pre-spawn preflight**: `record` warns (`::warning::`, before the paid run starts — once per batch
135
+ for `record <dir>` / `--rerecord-stale`) when a scenario about to record at a host-path-bearing tier
136
+ (`hostloop`, `protocol`) has an **empty** redaction policy — that combination commits real host paths
137
+ the `path` scanner then hard-fails at `verify-cassettes` time. The always-on scanner remains the
138
+ universal net (container-tier recordings can trip it too).
139
+ - **Always-on scan gate** — `verify-cassettes` flags email / currency / bare-domain / local-path /
140
+ machine-inventory matches it finds in the committed cassettes and **exits non-zero**, so "no leak" is
141
+ a gate, not discipline.
133
142
  Suppress synthetic / public reference names (NVCA, Cooley GO, …) with `--allow <regex>`. (Multi-word
134
143
  proper names are NOT a default class — too noisy to gate on; add a pattern via config if your corpus
135
144
  needs it.)
@@ -178,7 +187,7 @@ jobs:
178
187
  with: { node-version: '20' }
179
188
  - uses: actions/setup-python@v5
180
189
  with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
181
- - run: npm i -g cowork-harness@>=0.22.0
190
+ - run: npm i -g "cowork-harness@>=0.24.0"
182
191
  - run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
183
192
  - run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
184
193
  - run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
@@ -207,7 +216,7 @@ jobs:
207
216
  echo "live=true" >> "$GITHUB_OUTPUT"
208
217
  fi
209
218
  - if: steps.guard.outputs.live == 'true'
210
- run: npm i -g cowork-harness@>=0.22.0
219
+ run: npm i -g "cowork-harness@>=0.24.0"
211
220
  - if: steps.guard.outputs.live == 'true'
212
221
  run: cowork-harness run scenarios/ --output-format json
213
222
  env:
@@ -234,6 +243,11 @@ scenario = `result === "success" && assertions.every(pass)`. Exit code is non-ze
234
243
  fails or a run errors, so a plain `cowork-harness run scenarios/` is already CI-ready without parsing
235
244
  JSON.
236
245
 
246
+ `verify-cassettes` emits its **own** envelope (`{command, ok, coverage, results[]}` with per-file
247
+ `findings`/`staleness`/`notes`/`version`/`error`), published as `schema/verify-cassettes.json` in the
248
+ npm package. Both envelope schemas are covered 1.0 contract surfaces (SPEC §12) — parse the JSON, not
249
+ the human-readable text (which is explicitly NOT stable).
250
+
237
251
  A run writes to `~/.cowork-harness/runs/<name>/<sessionId>/` by default — outside any working tree. In CI,
238
252
  set `COWORK_HARNESS_RUNS_DIR` (or pass `--run-dir`) to a workspace-relative path (e.g. `runs`) so an
239
253
  artifact-upload step can collect them. Each run dir holds `events.jsonl`, `control-out.jsonl`, `run.jsonl`,
@@ -260,6 +274,13 @@ does **not** imply the recording is still valid. Each replay result carries `sta
260
274
  | `format` | recorded under an older hash format | re-record once |
261
275
  | `unverifiable-baseline` | the latest baseline couldn't be loaded | couldn't verify (env, not skill) |
262
276
  | `unverifiable-skill` | skill dirs unresolvable — skill staleness couldn't be checked | couldn't verify the skill |
277
+ | `resolved-tier` | a `fidelity: cowork` cassette's recorded `effectiveFidelity` no longer matches what the baseline resolves to today (the host-loop gate flipped) | **high** (the recording exercises the wrong tier) |
278
+ | `unverifiable-tier` | tier check couldn't run for a `fidelity: cowork` cassette (no recorded `effectiveFidelity`, or its pinned baseline failed to load) | couldn't verify the tier — re-record |
279
+
280
+ (A pre-`effectiveFidelity` cassette with an **explicit** tier is statically knowable — it passes the tier
281
+ check with a non-failing informational note in the `verify-cassettes` envelope's per-file `notes[]`, a
282
+ `·`-prefixed row in text output. On `verify-cassettes` every staleness *finding* above is a hard fail —
283
+ the gate is class-blind; notes never fail it.)
263
284
 
264
285
  To gate in CI, pick the severity you want:
265
286
 
@@ -1,6 +1,6 @@
1
1
  # Fidelity tiers & answer paths
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.22.0` (baseline `desktop-1.18286.0`).
3
+ Self-contained reference. Tracks `cowork-harness 0.24.0` (baseline `desktop-1.18286.0`).
4
4
 
5
5
  ## Fidelity tiers (`fidelity:` in the scenario)
6
6
 
@@ -187,5 +187,6 @@ and `scaffold` refuse to treat a partial run's half-finished output as a passing
187
187
  redact from logs (beyond the auth tokens + `ANTHROPIC_CUSTOM_HEADERS`).
188
188
  - `COWORK_HARNESS_SOFT_MISSING` — downgrade a missing mount source from hard-error to warn-and-skip.
189
189
  - `COWORK_VM_GATEWAY` / `COWORK_VM_PROXY_PORT` / `COWORK_LIMA_INSTANCE` — L2 (microVM) knobs.
190
- - `COWORK_LOCKDOWN` — default `on`: **aborts loudly** if the L2 guest firewall fails to apply (no
191
- silent unprotected run). Set `=off` to opt out and run without isolation deliberately.
190
+ - `COWORK_LOCKDOWN` — default `on`: gates sandbox hardening on every isolated tier — **aborts loudly**
191
+ if the L2 guest firewall fails to apply (no silent unprotected run), and also gates the
192
+ `container`/`hostloop` Docker hardening. Set `=off` to opt out and run without isolation deliberately.
@@ -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.22.0`
3
+ Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.24.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
 
@@ -61,6 +61,12 @@ skills: [report-gen] # OPTIONAL — scope the cassette-staleness
61
61
  # `skills/<name>` dir under a mounted plugin-root) + the plugin's shared
62
62
  # roots. Fail-closed to whole-tree on an unknown name. Omit = whole tree.
63
63
 
64
+ requires_capabilities: [ocr] # OPTIONAL — declare a capability the skill needs (e.g. office_convert,
65
+ # ocr, pdf_tables, opencv). If the running agent image provably omits
66
+ # one, the harness ABORTS before the paid run (exit 3) — unless the
67
+ # scenario also asserts `allow_missing_capability: true`, which downgrades
68
+ # the abort to a notice and proceeds. Live tiers only.
69
+
64
70
  allow_host_writes: true # OPTIONAL — required to run `hostloop` fidelity with a WRITABLE
65
71
  # connected folder (session `mode: rw`/`rwd`): with no container
66
72
  # around hostloop's native file tools, that combination gives the
@@ -80,8 +86,10 @@ session = your setup.**
80
86
  ```yaml
81
87
  # model & reasoning
82
88
  model: claude-opus-4-8 # omit for the agent default
89
+ account_name: my-account # OPTIONAL — which configured account/credential set to run as
83
90
  effort: high # low | medium | high | xhigh
84
91
  max_thinking_tokens: 31999 # positive int, or per-model map {default, <model>: <n>}; default 31999
92
+ extended_thinking: true # INERT — not a real Cowork toggle; use max_thinking_tokens instead
85
93
  permission_mode: default # default | acceptEdits | plan | bypassPermissions
86
94
  permission_parity: cowork # cowork (unscripted tool calls allowed) | strict (deny unscripted)
87
95
 
@@ -97,8 +105,9 @@ plugins:
97
105
  marketplaces: [] # plugin_marketplaces (git URLs or local paths)
98
106
  local_marketplaces: [] # local marketplace dirs (each has a marketplace.json)
99
107
  enabled: [my-skill@local] # enabledPlugins (name@marketplace)
100
- local_plugins: [./skills/my-skill] # host plugin dirs → mnt/.local-plugins/marketplaces/<marketplace>/<plugin>
101
- # (≥1.14271.0; older baselines use mnt/.local-plugins/cache)
108
+ local_plugins: [./skills/my-skill] # host plugin dirs → mnt/.local-plugins/marketplaces/local-desktop-app-uploads/<plugin>
109
+ # (the marketplace segment is that fixed synthetic name; ≥1.14271.0 —
110
+ # older baselines use mnt/.local-plugins/cache)
102
111
  remote_plugins: []
103
112
  skills:
104
113
  local: [] # extra host skill dirs
@@ -210,6 +219,7 @@ passes only if every key passes. Keep one concern per item unless you mean conju
210
219
  | `file_exists: <path>` | the path exists under the run's `work/` (anchored at `mnt/`, e.g. `outputs/x.md`). For a user-facing deliverable prefer `user_visible_artifact` — with a connected folder the file lands in `mnt/<folder>` (= `{{workspaceFolder}}`), not `mnt/outputs`, so `file_exists: outputs/x.md` misses it |
211
220
  | `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 |
212
221
  | `no_delete_in_outputs: true` | no delete op touched `mnt/outputs` — **only `true` is valid**; `false` is rejected (omit to allow deletes) |
222
+ | `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 |
213
223
  | `self_heal_ran: <bool>` | a plugin-root self-heal script was (not) invoked |
214
224
  | `tool_called: <Tool>` | the agent invoked the tool (actually ran it) |
215
225
  | `tool_not_called: <Tool>` | the agent never invoked it |
@@ -219,7 +229,7 @@ passes only if every key passes. Keep one concern per item unless you mean conju
219
229
  | `subagent_tool_absent: <Tool>` | no sub-agent used the tool |
220
230
  | `subagent_dispatched: <regex>` | a sub-agent whose `agentType` **or dispatch description** matches |
221
231
  | `subagent_declared_but_unused: <Tool>` | a sub-agent declared the tool but never used **that** tool (even if it used others) |
222
- | `dispatch_count_max: <N>` | at most N sub-agents dispatched (records only — does NOT enforce a cap) |
232
+ | `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) |
223
233
  | `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 |
224
234
  | `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 |
225
235
  | `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` |
@@ -234,7 +244,7 @@ passes only if every key passes. Keep one concern per item unless you mean conju
234
244
  | `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 |
235
245
  | `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 |
236
246
  | `allow_stall: true` | verdict modifier — suppresses the `stalled` default-fail when a run ends on a question having done no productive tool work after its last gate (the agent asked for input and stopped — incl. re-asking in plain text after answering an `AskUserQuestion`); assert only when ending on a question is intended, else script the answer (`answer:` / `--answer` / a decider) |
237
- | `transcript_no_host_path: true` | no host path (`/Users/`, `/opt/cowork/`, `/home/`, `/root/`) leaked into model-visible text — **incompatible with `hostloop`**: its native file tools legitimately expose real host paths, so this fails BY DESIGN there (the harness warns at run start if asserted anyway); use `container`/`microvm` for this check |
247
+ | `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 |
238
248
  | `egress_denied: <host>` | the host was blocked by the egress proxy |
239
249
  | `egress_allowed: <host>` | the host was allowed through |
240
250
  | `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) |
@@ -293,7 +303,9 @@ small-file JSON `body` inlined; a hash-only entry still satisfies `file_exists`.
293
303
  resolves a `/sessions/…/mnt/…`-shaped link directly against the manifest, and a host-shaped (hostloop) link
294
304
  by first normalizing it to a mount-relative path (recorded connected-folder prefixes + the outputs/uploads
295
305
  mounts) — replay has no live filesystem to check a host path against directly (that only happens on a live
296
- `run`/`verify-run`). Without a manifest (older cassettes) all four are skipped. A green replay re-confirms
306
+ `run`/`verify-run`). Without a manifest (older cassettes) all five are skipped; `no_unexpected_files` also
307
+ needs `preRunPaths` (≥0.24 recordings) — without it the key is excluded with a loud warning (live/verify-run
308
+ hard-fails evidence-unavailable instead). A green replay re-confirms
297
309
  *record-time* artifacts, not that the current skill still produces them — `replay --strict` fails when the
298
310
  staleness `fingerprint` shows ANY skill/baseline drift, or `replay --fail-on-skill-drift` only on
299
311
  skill-source drift; every replay result also reports it class-tagged in `staleness[]` for a JSON gate.
@@ -372,7 +384,8 @@ at the top of this file.
372
384
  the stochastic path flags the run `nonDeterministic`. The LLM decider is one mechanism, two
373
385
  spellings: `on_unanswered: llm` (YAML) and `--decider-llm` (CLI). The bare `--on-unanswered llm`
374
386
  is rejected (use `--decider-llm`). `agent` is **retired** — `on_unanswered: agent` is rejected by
375
- the schema. (`src/types.ts:276`; `src/cli.ts:652`.)
387
+ the schema. (`src/types.ts:365` — the `on_unanswered` enum; `src/cli.ts:899` — the CLI-side
388
+ `--on-unanswered` value check.)
376
389
 
377
390
  4. **`--on-unanswered first` is non-deterministic too** — it picks option 1 and is flagged
378
391
  `nonDeterministic`; not a deterministic substitute for scripted answers.
@@ -400,9 +413,11 @@ at the top of this file.
400
413
  11. **`subagent_declared_but_unused` fires on declared-but-didn't-use-THAT-tool**, even if the
401
414
  sub-agent used other tools.
402
415
 
403
- 12. **`dispatch_count_max` asserts but does NOT enforce.** It records the count; it does not
404
- reproduce Cowork's skip-on-cap (`{perTask:1, global:3}`, deferred). Passing means "dispatched ≤N
405
- this run," not "the harness capped it."
416
+ 12. **`dispatch_count_max` is an author-chosen budget, not a production cap.** It records the count
417
+ and asserts on it; passing means "dispatched ≤N this run," not "the harness capped it." Cowork
418
+ imposes no in-conversation Task-dispatch cap to reproduce — gate `1648655587`'s
419
+ `{perTask:1, global:3}` is the scheduled/cron-task session limiter, a different mechanism
420
+ (binary-verified; SPEC §10).
406
421
 
407
422
  13. **`protocol` is rejected (not silently passed) if the scenario asserts egress** — boundary
408
423
  assertions need `container`+. Fails loud by design.
@@ -0,0 +1,108 @@
1
+ # Task recipes — end-to-end paths for the four jobs consumers actually reverse-engineer
2
+
3
+ Each recipe composes facts that live scattered across SKILL.md and the other references into one
4
+ decision path. Every one answers a question a real fleet owner had to work out the hard way. Facts track the harness version in SKILL.md's
5
+ front-matter; the `resolved-tier`/`unverifiable-tier` staleness classes (Recipe 2) and
6
+ `init-redact` (Recipe 3) are new in the release AFTER 0.23.0.
7
+
8
+ ## Recipe 1 — Evolve the `assert:` block of an existing cassette (usually NO re-record)
9
+
10
+ You added or changed assertion keys in a scenario whose cassette is already committed. Do you need
11
+ a paid live re-record? Walk this tree — the answer is usually no:
12
+
13
+ 1. **Content / always-replay keys** (`transcript_*`, `tool_called`/`tool_not_called`,
14
+ `tool_result_*`, `subagent_*`, `dispatch_count_max`, `skill_triggered`/`no_skill_triggered`,
15
+ `result`, `max_turns`, `tool_calls_max`, `max_cost_usd`, `max_tokens`) →
16
+ `cowork-harness replay <cassette> --assert-from <scenario.yaml>`. Token-free, no re-record.
17
+ If the recording genuinely lacks the telemetry a key needs (very old cassettes), the key fails
18
+ **loud** as `evidence-unavailable` — that is correct behavior, not a bug; only then re-record.
19
+ 2. **Gate keys** (`question_asked`, `questions_count_max`, `gate_answers_delivered`) on a cassette
20
+ **with `controlOut`** (any modern recording) → same token-free `--assert-from` path.
21
+ 3. **Gate keys** on a **pre-`controlOut`** cassette → one re-record unlocks gate asserts for that
22
+ cassette permanently.
23
+ 4. **Filesystem / egress keys** (`file_exists` without an artifact manifest, `egress_allowed`,
24
+ `egress_denied`, `no_delete_in_outputs`) → live lane **by design**; replay skips them with a
25
+ loud `::warning::`. Keep them in the scenario, run them on the nightly live gate.
26
+
27
+ **The one hard caveat:** `--assert-from` **hard-fails on recording-shaping drift** — if the
28
+ scenario's `prompt:`, `answers:`, baseline, or skill content differ from what shaped the
29
+ recording, re-asserting under them is meaningless and the harness refuses. The trap you'll
30
+ actually hit is `answers:`: sequence your edits — land assert-only changes (token-free), and land
31
+ answer changes separately (those DO force a re-record).
32
+
33
+ ## Recipe 2 — Audit a fleet for tier drift
34
+
35
+ A `fidelity: cowork` scenario records at whatever tier the baseline's host-loop gate resolves to
36
+ at record time. When Cowork flips that gate, your committed cassettes silently stop representing
37
+ what production would do. Two layers of defense:
38
+
39
+ - **Automated (preferred):** `cowork-harness verify-cassettes <dir>` raises a
40
+ **`resolved-tier`** staleness finding when a `fidelity: cowork` cassette's recorded
41
+ `effectiveFidelity` no longer matches what the current baseline resolves to, and
42
+ **`unverifiable-tier`** when a `fidelity: cowork` cassette predates `effectiveFidelity`
43
+ entirely (can't verify → re-record). Explicit-tier cassettes (`hostloop`, `container`, …) are
44
+ statically knowable and only produce a non-failing informational note.
45
+ - **Manual (one-liner):** `grep -h '"effectiveFidelity"' cassettes/*.cassette.json | sort | uniq -c`
46
+ shows the tier distribution of the fleet at a glance.
47
+
48
+ ### Cassette anatomy (what you're looking at when you open one)
49
+
50
+ Top-level fields of a `*.cassette.json` (schema `schema/cassette.v7.json`):
51
+
52
+ | Field | What it is |
53
+ |---|---|
54
+ | `$schema`, `generator`, `cassetteVersion` | Provenance: schema URL, producing tool, format version (current: 7) |
55
+ | `scenario` | The embedded scenario snapshot at record time |
56
+ | `events` | The recorded agent event stream (the replay source) |
57
+ | `controlOut` | Driver→agent control responses — presence unlocks gate asserts on replay |
58
+ | `effectiveFidelity` | The tier the live record actually resolved to (the drift-audit key above) |
59
+ | `artifacts` | Output-file manifest (paths + hashes + small inlined bodies) — unlocks `file_exists`/`artifact_json` on replay |
60
+ | `fingerprint` | Skill/baseline staleness tripwire |
61
+ | `userVisibleRoots` | The user-visible mount roots captured at record time |
62
+ | `preRunPaths` | Pre-run file-path baseline for `no_unexpected_files` (workRoot-relative; co-present with `userVisibleRoots`) |
63
+ | `scenarioSource` | Relative path to the authored YAML this was recorded from |
64
+ | `authoring` | Present iff a live decider answered ≥1 gate during recording (`nonDeterministic: true`) |
65
+
66
+ ## Recipe 3 — Set up redaction BEFORE your first hostloop/protocol record
67
+
68
+ `hostloop` and `protocol` runs execute on the host, so real host paths (`/Users/you/…`) land in
69
+ the transcript and would be committed inside the cassette. Set the policy up **before** the first
70
+ record — retrofitting means re-recording:
71
+
72
+ 1. `cowork-harness init-redact` — copies the reference `.cowork-redact.json` (host-path +
73
+ email patterns) into the current directory. Version-control it.
74
+ 2. **Search set:** record looks for `.cowork-redact.json` in the **cwd, the scenario's directory,
75
+ and the cassette's output directory** — every distinct file found is MERGED (plus
76
+ `COWORK_HARNESS_REDACT_PATTERNS`/`_KEYS` from the env). Repo root (= cwd in CI) is the
77
+ conventional home.
78
+ 3. **Preflight backstop:** `record` on a host-path-bearing tier with NO effective policy emits a
79
+ loud pre-spawn `::warning::` (once per batch). If you see it, stop and do step 1.
80
+ 4. **Scanner relationship:** redaction (record-time, rewrites content) and the
81
+ `verify-cassettes` privacy scanner (CI-time, read-only tripwire over 4 classes: `domain`,
82
+ `email`, `path`, `machine-inventory`) are independent layers. The scanner catches what
83
+ redaction missed.
84
+ 5. **Scoping idioms for scanner findings you've reviewed and accepted:** prefer the class-scoped
85
+ flags — `--allow-domain 'api\.example\.com'`, `--allow-email '.*@yourco\.com'`,
86
+ `--allow-path '/opt/ci-runner/.*'` — over bare `--allow` (which applies to every class and can
87
+ let a domain allow swallow an email leak). For a version-controlled allowlist use
88
+ `--allow-file <path>`: bare all-class patterns, one regex per line, `#` comments and blank
89
+ lines ignored.
90
+
91
+ ## Recipe 4 — Budget assertions without a two-pass record
92
+
93
+ `max_turns` / `tool_calls_max` / `questions_count_max` / `dispatch_count_max` need observed
94
+ values, but you don't need a second paid run to get them — derive them from evidence you already
95
+ have:
96
+
97
+ 1. **From the run you already did:** `run` always keeps its run dir. Read observed turns from
98
+ `result.json` (`usage.turns`), tool-call and question counts from
99
+ `cowork-harness trace <run-id> --view tools` / `--view questions`, and the dispatch count from
100
+ `--view dispatches`.
101
+ 2. **From history:** `cowork-harness stats <scenario> --metric turns` gives p50/p95 turns across
102
+ indexed runs — better than a single observation.
103
+ 3. **Set the budget with headroom:** observed × 1.5, rounded up (agentic run lengths vary run to
104
+ run; a budget at the observed value flakes).
105
+ 4. **Replay-class note:** `max_turns` / `tool_calls_max` / `dispatch_count_max` re-evaluate
106
+ token-free on replay; `questions_count_max` needs `controlOut` (Recipe 1's tree applies).
107
+ `max_cost_usd` / `max_tokens` on replay assert the FROZEN recording's spend — near-zero signal
108
+ as a regression gate; if you need a cost gate, put it on the live lane via `stats`.
@@ -17,6 +17,7 @@
17
17
  "max_turns",
18
18
  "no_delete_in_outputs",
19
19
  "no_skill_triggered",
20
+ "no_unexpected_files",
20
21
  "question_asked",
21
22
  "questions_count_max",
22
23
  "replay_protocol_fidelity",