cowork-harness 0.27.0 → 0.29.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 (95) hide show
  1. package/.claude/skills/cowork-harness/SKILL.md +210 -110
  2. package/.claude/skills/cowork-harness/references/ci-recipe.md +7 -6
  3. package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +2 -2
  4. package/.claude/skills/cowork-harness/references/scenario-schema.md +81 -29
  5. package/.claude/skills/cowork-harness/references/task-recipes.md +60 -7
  6. package/.claude/skills/cowork-harness/scripts/assertion-keys.json +3 -0
  7. package/.claude/skills/cowork-harness/scripts/scenario.py +241 -3
  8. package/AGENTS.md +5 -4
  9. package/CHANGELOG.md +392 -0
  10. package/CONTRIBUTING.md +12 -5
  11. package/DESIGN.md +4 -4
  12. package/README.md +73 -46
  13. package/RELEASING.md +123 -0
  14. package/SPEC.md +32 -17
  15. package/baselines/desktop-1.18286.2.json +296 -0
  16. package/baselines/desktop-1.19367.0.json +373 -0
  17. package/baselines/desktop-1.20186.0.json +373 -0
  18. package/baselines/prompts/cowork-system-prompt-fingerprints.json +38 -0
  19. package/dist/agent/session.js +125 -29
  20. package/dist/agent/timeline.js +18 -10
  21. package/dist/assert.js +325 -57
  22. package/dist/baseline.js +1 -1
  23. package/dist/cli.js +109 -69
  24. package/dist/critique/evaluator.js +321 -0
  25. package/dist/critique/evidence.js +175 -0
  26. package/dist/critique/package-evidence.js +198 -0
  27. package/dist/decide/external-channel.js +8 -6
  28. package/dist/decide/llm-transport.js +26 -3
  29. package/dist/decide/semantic-judge.js +156 -0
  30. package/dist/egress/sidecar.js +9 -8
  31. package/dist/hostloop/workspace-handler.js +1 -2
  32. package/dist/prompt.js +16 -0
  33. package/dist/redact.js +2 -3
  34. package/dist/run/artifacts.js +341 -35
  35. package/dist/run/cassette.js +544 -169
  36. package/dist/run/chat-result.js +24 -3
  37. package/dist/run/chat.js +21 -1
  38. package/dist/run/computer-links.js +54 -3
  39. package/dist/run/diff.js +1 -1
  40. package/dist/run/doctor.js +22 -5
  41. package/dist/run/execute.js +200 -35
  42. package/dist/run/matrix.js +1 -1
  43. package/dist/run/pre-run-manifest.js +132 -32
  44. package/dist/run/renderer.js +9 -1
  45. package/dist/run/run-index.js +10 -1
  46. package/dist/run/run-status.js +64 -8
  47. package/dist/run/run.js +124 -19
  48. package/dist/run/runs-gc.js +48 -3
  49. package/dist/run/scenario-tool.js +4 -8
  50. package/dist/run/skill-files.js +7 -2
  51. package/dist/run/skill-hash.js +2 -2
  52. package/dist/run/trace-view.js +180 -16
  53. package/dist/run/verdict.js +10 -1
  54. package/dist/runtime/argv.js +30 -28
  55. package/dist/runtime/container.js +2 -4
  56. package/dist/runtime/hostloop-stage.js +15 -2
  57. package/dist/runtime/hostloop.js +33 -24
  58. package/dist/runtime/microvm.js +2 -4
  59. package/dist/runtime/protocol.js +15 -0
  60. package/dist/runtime/resource-sampler.js +84 -21
  61. package/dist/runtime/stage.js +7 -0
  62. package/dist/scan.js +6 -1
  63. package/dist/session.js +171 -40
  64. package/dist/sync/cowork-sync.js +410 -34
  65. package/dist/types.js +93 -13
  66. package/dist/usage-limit.js +40 -0
  67. package/docs/README.md +15 -11
  68. package/docs/boundary.md +6 -1
  69. package/docs/cassette.md +75 -18
  70. package/docs/chat.md +8 -0
  71. package/docs/cowork-spawn-contract-1.12603.1.md +2 -2
  72. package/docs/debugging.md +20 -5
  73. package/docs/discovery.md +6 -4
  74. package/docs/gotchas.md +12 -3
  75. package/docs/invariants.md +14 -7
  76. package/docs/maintenance.md +27 -7
  77. package/docs/plugin-root.md +78 -0
  78. package/docs/protocol.md +28 -7
  79. package/docs/run-status.md +8 -2
  80. package/docs/scenario.md +74 -24
  81. package/docs/session.md +23 -7
  82. package/docs/stats.md +20 -7
  83. package/examples/README.md +1 -1
  84. package/examples/replays/README.md +7 -4
  85. package/examples/replays/example-multiselect-gate.cassette.json +111 -53
  86. package/examples/replays/example-pdf-skill.cassette.json +184 -31
  87. package/examples/replays/hostloop-computer-links.cassette.json +50 -36
  88. package/llms.txt +1 -1
  89. package/package.json +11 -2
  90. package/python/README.md +3 -2
  91. package/python/cowork_harness.py +40 -1
  92. package/schema/cassette.v10.json +328 -0
  93. package/schema/run-result.json +793 -127
  94. package/schema/scenario.schema.json +81 -18
  95. package/schema/session.schema.json +45 -37
@@ -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.27.0
7
- tracks-harness: cowork-harness 0.27.0 (baseline desktop-1.18286.0)
6
+ version: 0.29.0
7
+ tracks-harness: cowork-harness 0.29.0 (baseline desktop-1.20186.0)
8
8
  ---
9
9
 
10
10
  # cowork-harness
@@ -22,11 +22,11 @@ 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.27.0` (baseline
26
- > `desktop-1.18286.0`). If your checkout is newer, prefer the live `--help`, `SPEC.md`, and
27
- > `docs/*.md` over this snapshot, and re-run the bundled linter.
25
+ > **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.29.0` (baseline
26
+ > `desktop-1.20186.0`). If your checkout is newer, prefer the live `--help` and — in a repo checkout —
27
+ > `SPEC.md` / `docs/*.md` over this snapshot, and re-run the bundled linter.
28
28
 
29
- ## 0. Preflight — make sure the harness can actually run
29
+ ## Preflight — make sure the harness can actually run
30
30
 
31
31
  The 10-second inner loop, once the CLI is on PATH:
32
32
 
@@ -38,21 +38,36 @@ 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.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).)
41
+ - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.29.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.29.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.29.0"`. **Pin `@>=0.29.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.29.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, `computer_links_resolve` (new in 0.22.0), and `semantic_matches` (an LLM judge grades a fixed `rubric` of claims against the run's answer — **live-only**, so it is evidence-unavailable / skipped-loud on replay, never a vacuous pass; new in 0.27.0), and **glob-matched** `tool_called`/`tool_not_called`/`subagent_tool_used`/`subagent_tool_absent` (a pattern like `mcp__workspace__*` matches any tool in the family; exact names match exactly; new in 0.28.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
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.
46
46
 
47
- ## 1. Pick the loop
47
+ ## Orient — the three loops
48
+
49
+ Everything you do with the harness is one of **three loops**, and the rest of this skill is organized
50
+ into three Parts to match: **author** a scenario (Part I), **run / record / lock** it into a
51
+ reproducible regression (Part II), and **debug** a run that misbehaved or greened when it shouldn't
52
+ (Part III — reachable straight from here in one hop). Pick the loop you're in:
48
53
 
49
54
  - **"Is it even alive?"** (inner loop) → `cowork-harness skill <folder> "<prompt>"`. Fastest; no
50
55
  scenario file.
51
56
  - **Repeatable, asserted regression** → author a `scenarios/*.yaml` and run `cowork-harness run`.
52
57
  This is the CI-grade path and most of this skill.
53
- - **Multi-turn debugging** → `cowork-harness chat` (interactive; gates answered at the TTY, **not** an
54
- asserted test — see *Debugging with `chat`* in `docs/scenario.md`, or `docs/debugging.md` for the full
55
- debugging map).
58
+ - **Regression-test your skill's ANSWER quality** (not just its behavior — does its guidance still lead to
59
+ correct answers after you edit it?) → author `semantic_matches` scenarios and gate on the per-claim
60
+ profile. See **Recipe 5** in `references/task-recipes.md` (validity, N≥3, discrimination — the traps).
61
+ - **A run failed — or greened and you don't trust it** (the debugging loop) → don't re-run and hope.
62
+ The run already wrote its evidence to a **kept run dir** (`~/.cowork-harness/runs/…`; `--keep` prints
63
+ the path, `trace <run-id>` finds it). **Localize the failure post-hoc** from that evidence:
64
+ `cowork-harness trace <run-dir>`'s views + the emitted `result.json` to see what the run actually did,
65
+ then `verify-run` to re-check a suspect assertion — all token-free, no Docker, no re-record. This is
66
+ the loop 0.29.0's observability is built for; the *Triage* and *Inspecting a run's observability
67
+ output* sections in **Part III — Debug** are the detail (the fuller human-facing map lives in
68
+ `docs/debugging.md` — repo-only, not shipped with the installed skill).
69
+ - **Multi-turn / interactive reproduction** → `cowork-harness chat` (interactive; gates answered at the
70
+ TTY, **not** an asserted test — see *Debugging with `chat`* in **Part III — Debug**).
56
71
 
57
72
  Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
58
73
  verify-run · trace · inspect · diff · stats · decide · gates · answer · scaffold · assertions --list · sync ·
@@ -60,10 +75,17 @@ list · boundary-check · status · vm <init|status|delete|prune> · doctor · i
60
75
 
61
76
  **Two different `scaffold` tools — don't confuse them.** The native `cowork-harness scaffold <run-id>`
62
77
  above turns an already-*recorded* run into a scenario (needs a run to exist first). The bundled
63
- `scripts/scenario.py scaffold --name … --skill …` in §9 builds a scenario from flags alone, no run
64
- required. Passing §9's flag set to the native command fails with `unknown flag: --name` (exit 2).
78
+ `scripts/scenario.py scaffold --name … --skill …` — see *Scaffold a valid scenario, then lint before
79
+ you push* in **Part I** — builds a scenario from flags alone, no run required. Passing that section's
80
+ flag set to the native command fails with `unknown flag: --name` (exit 2).
81
+
82
+ ## Part I — AUTHOR a scenario
65
83
 
66
- ## 2. Two files: session vs scenario
84
+ Everything below composes one deterministic, asserted `scenarios/*.yaml`: the session/scenario split,
85
+ how the skill mounts, the fidelity tier, the answer path, the two assertion axes, `web_fetch`
86
+ provenance, and the scaffold/lint tools that keep the YAML honest.
87
+
88
+ ### Two files: session vs scenario
67
89
 
68
90
  - **`sessions/*.yaml`** — pre-prompt setup: `model`, mounts (`folders`), and discovery
69
91
  (marketplaces / plugins / skills / mcp). One session is reused by many scenarios. A scenario that
@@ -74,7 +96,7 @@ This split matters: release ground truth (`baseline:` / `baselines/`, produced b
74
96
  **separate** from authored setup (`session:` / `sessions/`). "profile" is retired vocabulary — do
75
97
  not use it. See `references/scenario-schema.md` for every field.
76
98
 
77
- ## 3. Discovery: how the skill-under-test gets mounted
99
+ ### Discovery: how the skill-under-test gets mounted
78
100
 
79
101
  The skill is **copied fresh into the sandbox each run**. Wire it via `plugins.local_plugins` +
80
102
  `plugins.enabled: [<plugin>@local]` in the session (or `--marketplace` / `--plugin` flags on
@@ -84,13 +106,17 @@ the folder basename (collision-resolved); there is no `to:` override. See `refer
84
106
 
85
107
  > **`git add` a brand-new skill before testing it.** Inside a git repo the harness stages the
86
108
  > **git-tracked** files (the fidelity boundary — real Cowork installs from a repo and sees only committed
87
- > files). An **all-untracked** skill folder used to mount *empty* and the agent reported "the skill isn't
109
+ > files). *Tracked* means **in the git index** (committed **or** `git add`-staged); the **content** staged
110
+ > is your **working tree**, so an uncommitted edit to an already-tracked file *is* tested — you needn't
111
+ > commit to iterate. Only brand-new (untracked) files must be `git add`-ed to appear. Commit before you
112
+ > record the **locking cassette**, though: real Cowork ships the *committed* tree, so a green on
113
+ > uncommitted edits isn't yet a green on what installs. An **all-untracked** skill folder used to mount *empty* and the agent reported "the skill isn't
88
114
  > installed" then did the work itself — a green-looking run where the skill never loaded. That now
89
115
  > **hard-fails** (`BoundaryError`, exit 3) naming the dir, and a partially-tracked folder emits a loud
90
116
  > `::notice:: [stage]` listing the excluded files. Fix: `git add` the skill, or `COWORK_HARNESS_GITSET=0`
91
117
  > to copy untracked files (won't reflect what ships). A folder **outside** any repo is copied raw (no guard).
92
118
 
93
- ## 4. Choose a fidelity tier
119
+ ### Choose a fidelity tier
94
120
 
95
121
  | Tier | What it gives you | Use when |
96
122
  |---|---|---|
@@ -103,7 +129,7 @@ Set the tier in the **scenario's `fidelity:` field**, not a flag — `run` rejec
103
129
  (it's a `skill`/`chat` flag; `run` takes fidelity only from the scenario). See
104
130
  `references/fidelity-and-answers.md`.
105
131
 
106
- ## 5. Choose an answer path (gates: AskUserQuestion + tool-permission)
132
+ ### Choose an answer path (gates: AskUserQuestion + tool-permission)
107
133
 
108
134
  Default to **deterministic**: scripted `answers:` + `on_unanswered: fail`. Anything that brings a
109
135
  live model into answering flags the run `nonDeterministic` — keep those out of deterministic
@@ -132,7 +158,7 @@ anchor. (When label *order* is stable but the text drifts, a positional `choose`
132
158
  linter flags positional `choose` as order-dependent, so use it deliberately.) The caution stands: `first`
133
159
  *masks* an unanswered gate, so don't use it for a gate you actually need answered a specific way.
134
160
 
135
- ### External deciders and the "first" shorthand
161
+ #### External deciders and the "first" shorthand
136
162
 
137
163
  When using `--decider-cmd` or `--decider-dir`, the helper's output is passed through
138
164
  `coerceLabel` **with the "first" shorthand disabled**. This means a helper that returns the literal
@@ -143,28 +169,7 @@ The `"first"` shorthand remains active only for the built-in `--on-unanswered fi
143
169
  write an external helper, return a label name or option index — never the bare word `"first"` unless
144
170
  your gate actually has a label called `"first"`.
145
171
 
146
- ## 5a. Checking whether a background run is alive
147
-
148
- Never use `ps aux` to check on a `cowork-harness` run you launched in the background — it only sees
149
- processes in your OWN PID namespace, which is frequently NOT the harness process's namespace (e.g. when
150
- you're a sandboxed subagent). An empty `ps aux` match tells you nothing about whether the run is still
151
- going.
152
-
153
- Use **`cowork-harness status <dir> [--follow]`** instead — reads `<outDir>/status.json`, a file the
154
- harness writes/updates throughout the run's lifecycle (including a crash-safety net for a thrown
155
- error/`SIGTERM`, AND staleness detection for a hard `SIGKILL`/OOM-kill that no exit handler can catch —
156
- either way you get `"error"`/`stale` instead of a permanently-trusted `"running"`), so liveness is
157
- checkable regardless of PID namespace. The harness prints `[status] <outDir>` to stderr as soon as the
158
- run starts, so capture stderr to get the exact directory. `--follow` fails loud on a timeout/staleness
159
- rather than hanging forever. See `docs/run-status.md` for the full recipe.
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
-
167
- ## 6. Assertions: two orthogonal axes
172
+ ### Assertions: two orthogonal axes
168
173
 
169
174
  Conflating these is the **biggest landmine**. An assertion key has two independent properties:
170
175
 
@@ -180,11 +185,11 @@ Conflating these is the **biggest landmine**. An assertion key has two independe
180
185
 
181
186
  Getting Axis B wrong means a check that **does nothing in CI** — the harness warns loudly when it skips
182
187
  (an `::warning::` annotation, not a silent no-op — see the Axis B bullet above), and the bundled linter
183
- catches it before you push — run it (§9).
188
+ catches it before you push — run it (see *Scaffold a valid scenario, then lint before you push* below).
184
189
 
185
190
  See `references/scenario-schema.md` for the full assertion catalog with each key's replay class.
186
191
 
187
- ### Which assertion for which question (goal → key)
192
+ #### Which assertion for which question (goal → key)
188
193
 
189
194
  Beyond the outcome/content keys most scenarios reach for first (`result`, `transcript_*`,
190
195
  `file_exists`/`user_visible_artifact`, `artifact_json`), the harness surfaces the agent's *behavior*
@@ -201,7 +206,7 @@ them by what you're trying to prove:
201
206
  | a skill actually **ran** (or must NOT) | `skill_triggered: <regex>`, `no_skill_triggered: <regex>` |
202
207
  | a tool ran **inside** a skill's scope | `skill_tool_used: {skill, tool}` |
203
208
  | 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) |
209
+ | a pre-existing input wasn't mutated (incl. `uploads/**`) | `input_unmodified: <glob>` or `[<glob>, …]` (live/verify-run; not microvm) |
205
210
  | a resource ceiling held | `max_peak_rss_bytes: <N>` (**live-only**) |
206
211
  | a hook blocked / didn't block a tool | `hook_blocked: <regex>`, `no_hook_blocked: true` (replay needs a `controlOut` cassette) |
207
212
  | every MCP round-trip succeeded | `no_mcp_error: true` (**live-only**) |
@@ -213,7 +218,7 @@ cassette on replay, so check the catalog's replay class before putting one on a
213
218
  straight from the schema — treat it (and the catalog) as the source of truth; this map is a
214
219
  goal-oriented index into it, not a second catalog.
215
220
 
216
- ## 7. web_fetch (fail-closed, two-path)
221
+ ### web_fetch (fail-closed, two-path)
217
222
 
218
223
  `web_fetch` behaves unlike `curl`. A URL is gated by **provenance**, not the egress allowlist:
219
224
 
@@ -229,7 +234,47 @@ goal-oriented index into it, not a second catalog.
229
234
  Surprise to remember: adding a host to `egress.extra_allow` is a **no-op** for a provenanced fetch.
230
235
  Full model in `references/scenario-schema.md`.
231
236
 
232
- ## 8. Run, then lock determinism
237
+ ### Scaffold a valid scenario, then lint before you push
238
+
239
+ Don't hand-write the YAML from memory — that's how invented keys (`assertions:` vs `assert:`,
240
+ `json_file`, `answer_policy`) creep in. Start from the bundled generator, which emits the
241
+ known-good skeleton (right tier, scripted `answers:` + `on_unanswered: fail`, content assertions
242
+ separated from live-only ones, one concern per item) and **self-lints its own output**:
243
+
244
+ ```bash
245
+ S="${CLAUDE_PLUGIN_ROOT}/scripts/scenario.py"
246
+ # Working from a repo checkout instead of an installed plugin?
247
+ # S=".claude/skills/cowork-harness/scripts/scenario.py"
248
+ python3 "$S" scaffold --name report-check --skill ./skills/report-gen \
249
+ --prompt "Generate the weekly report to outputs/report.md." \
250
+ --content 'weekly report' --artifact outputs/report.md \
251
+ --egress-allowed api.weather.example.com --out scenarios/report-check.yaml
252
+ ```
253
+
254
+ Then lint every scenario — it encodes the no-silent-false-green invariants. Use the CLI wrapper
255
+ `cowork-harness lint` (it runs the same bundled `scenario.py lint`):
256
+
257
+ ```bash
258
+ cowork-harness lint scenarios/*.yaml
259
+ ```
260
+
261
+ `lint` flags: filesystem/egress-only assertions on a `replay` gate (silent no-op), bad regex
262
+ quoting, an egress assert on `protocol` fidelity, `transcript_no_host_path` on `hostloop`/`protocol`
263
+ (ERROR — fails by design at those tiers; WARN on `fidelity: cowork`, whose tier resolves per the
264
+ baseline's host-loop gate), non-empty `requires_capabilities` on `protocol` without
265
+ `allow_missing_capability` (ERROR — the capability probe can't run there, so the run hard-fails as
266
+ unverifiable), a `controlOut`-gated key on a non-`controlOut` replay, mixed-class assertion items,
267
+ and hallucinated schema (`assertions:` vs `assert:`, unknown keys). Exit code is non-zero on errors
268
+ (CI-friendly). `scaffold` auto-upgrades the tier if you ask for egress on `protocol`, so it never
269
+ emits a scenario `lint` would reject.
270
+
271
+ ## Part II — RUN, RECORD & LOCK
272
+
273
+ You have an authored scenario. This Part runs it, reads the verdict, locks it into a
274
+ byte-deterministic cassette, checks a background run's liveness, and places the assertions in the
275
+ right CI lane.
276
+
277
+ ### Run, then lock determinism
233
278
 
234
279
  Read the verdict and the inline failing transcript. To pin a flaky-because-stochastic gate, paste
235
280
  the echoed `--answer "<q>=<choice>"` footer lines back into the scenario's `answers:` for a
@@ -254,7 +299,7 @@ launched from a repo root never drops sensitive skill inputs/outputs into it. Pa
254
299
  `COWORK_HARNESS_RUNS_DIR`) to relocate; in CI point it at a workspace path so an artifact-upload step can
255
300
  collect the runs.
256
301
 
257
- ### Validate a skill against real documents (not a cassette)
302
+ #### Validate a skill against real documents (not a cassette)
258
303
 
259
304
  The loops above build **deterministic regressions**. A different job — drive a skill against *real* input
260
305
  documents to judge whether it actually does the work (extraction, analysis), with no intent to record a
@@ -290,7 +335,7 @@ cassette — has its own recipe:
290
335
  proceeds. Rebuild with `--build-arg COWORK_FULL_PARITY=1` and point `COWORK_AGENT_IMAGE` at it for those
291
336
  skills.
292
337
 
293
- ### Interpreting verdict signals
338
+ #### Interpreting verdict signals
294
339
 
295
340
  The run verdict may include `WARN`-severity signals in addition to pass/fail. One to watch for:
296
341
 
@@ -299,66 +344,111 @@ The run verdict may include `WARN`-severity signals in addition to pass/fail. On
299
344
  the run can still green. If you see it, fix the asset path — a green with a missing asset is
300
345
  not a valid pass.
301
346
 
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:
347
+ ### Checking whether a background run is alive
307
348
 
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
-
322
- ## 9. Scaffold a valid scenario, lint before you push, then place in CI
323
-
324
- Don't hand-write the YAML from memory — that's how invented keys (`assertions:` vs `assert:`,
325
- `json_file`, `answer_policy`) creep in. Start from the bundled generator, which emits the
326
- known-good skeleton (right tier, scripted `answers:` + `on_unanswered: fail`, content assertions
327
- separated from live-only ones, one concern per item) and **self-lints its own output**:
328
-
329
- ```bash
330
- S="${CLAUDE_PLUGIN_ROOT}/scripts/scenario.py"
331
- # Working from a repo checkout instead of an installed plugin?
332
- # S=".claude/skills/cowork-harness/scripts/scenario.py"
333
- python3 "$S" scaffold --name report-check --skill ./skills/report-gen \
334
- --prompt "Generate the weekly report to outputs/report.md." \
335
- --content 'weekly report' --artifact outputs/report.md \
336
- --egress-allowed api.weather.example.com --out scenarios/report-check.yaml
337
- ```
349
+ Never use `ps aux` to check on a `cowork-harness` run you launched in the background — it only sees
350
+ processes in your OWN PID namespace, which is frequently NOT the harness process's namespace (e.g. when
351
+ you're a sandboxed subagent). An empty `ps aux` match tells you nothing about whether the run is still
352
+ going.
338
353
 
339
- Then lint every scenario — it encodes the no-silent-false-green invariants. Use the CLI wrapper
340
- `cowork-harness lint` (it runs the same bundled `scenario.py lint`):
354
+ Use **`cowork-harness status <dir> [--follow]`** instead — reads `<outDir>/status.json`, a file the
355
+ harness writes/updates throughout the run's lifecycle (including a crash-safety net for a thrown
356
+ error/`SIGTERM`, AND staleness detection for a hard `SIGKILL`/OOM-kill that no exit handler can catch —
357
+ either way you get `"error"`/`stale` instead of a permanently-trusted `"running"`), so liveness is
358
+ checkable regardless of PID namespace. The harness prints `[status] <outDir>` to stderr as soon as the
359
+ run starts, so capture stderr to get the exact directory. `--follow` fails loud on a timeout/staleness
360
+ rather than hanging forever. (Fuller recipe in `docs/run-status.md` — repo-only, not in the installed
361
+ payload; `cowork-harness status --help` has the flags.)
341
362
 
342
- ```bash
343
- cowork-harness lint scenarios/*.yaml
344
- ```
363
+ **A multi-minute `record`/`run` outlives a short-lived wrapper.** Don't launch a long record from a
364
+ subagent that returns before it finishes — the returning agent tears down its process tree and kills the
365
+ in-flight run mid-artifact-write. Run it foreground, or detached from any process that will exit first.
366
+ (The `status.json` liveness above is exactly what surfaces such a teardown as `"error"`/`stale` rather
367
+ than a stuck `"running"`.)
345
368
 
346
- `lint` flags: filesystem/egress-only assertions on a `replay` gate (silent no-op), bad regex
347
- quoting, an egress assert on `protocol` fidelity, `transcript_no_host_path` on `hostloop`/`protocol`
348
- (ERROR — fails by design at those tiers; WARN on `fidelity: cowork`, whose tier resolves per the
349
- baseline's host-loop gate), non-empty `requires_capabilities` on `protocol` without
350
- `allow_missing_capability` (ERROR — the capability probe can't run there, so the run hard-fails as
351
- unverifiable), a `controlOut`-gated key on a non-`controlOut` replay, mixed-class assertion items,
352
- and hallucinated schema (`assertions:` vs `assert:`, unknown keys). Exit code is non-zero on errors
353
- (CI-friendly). `scaffold` auto-upgrades the tier if you ask for egress on `protocol`, so it never
354
- emits a scenario `lint` would reject.
369
+ ### Place assertions in the right CI lane
355
370
 
356
371
  CI placement: a **token-free `replay` PR gate** (content/structure only) + a **nightly live `run`**
357
372
  (filesystem/egress). Fastest setup: `uses: yaniv-golan/cowork-harness@v1` (a packaged GitHub Action with a
358
373
  PR job-summary reporter). See `references/ci-recipe.md` for the Action, the manual step-by-step form, and
359
374
  the four-stage pipeline.
360
375
 
361
- ## 10. Debugging with `chat`
376
+ ## Part III — Debug
377
+
378
+ A run misbehaved, or greened when you don't trust it. Debugging is a first-class loop, not an
379
+ afterthought: the run already wrote its evidence, so you **localize the failure post-hoc** rather than
380
+ re-run and hope. Start at the triage below, then use the observability output and, when you need to
381
+ reproduce interactively, `chat`.
382
+
383
+ ### Triage — a run misbehaved, or a green looks wrong
384
+
385
+ <!-- BEGIN triage-canonical -->
386
+ Two situations need different tools — figure out which one you're in first, then reach for the tool
387
+ instead of re-running and hoping. The run already wrote its evidence to a kept run dir (`--keep` prints
388
+ the path; `trace <run-id>` finds it), and every tool below reads that evidence **token-free** — no
389
+ Docker, no re-record.
390
+
391
+ | Situation | Symptom | Reach for (in order) |
392
+ |---|---|---|
393
+ | **The skill misbehaved** | wrong output, an unexpected gate, a denied tool, an opaque crash | `inspect` — what did it produce? · `trace <run-dir> --view <view>` — what did it actually do (tools, gates, sub-agent tree)? · `verify-run` — re-assert cheaply when only an assertion is wrong · `diff <old-run> <new-run>` — what changed since it worked · `chat` — reproduce it by hand |
394
+ | **A green you don't trust** | an assert that may have tested nothing, a stale cassette, an auto-answered or decided gate | `replay --explain` — the evidence trail behind each *passing* assert · `lint` — assertions on the wrong CI lane / mixed-class keys · `verify-cassettes` — privacy + staleness over committed cassettes · the Gotchas landmine catalog — how a check passes vacuously · `run --repeat N` — did it pass, or pass once? · `stats` — flaky or expensive over time |
395
+
396
+ A failed run also records `errorSource` (where the failure originated) and `stderrLogPath` (the captured
397
+ agent stderr) — read those before re-running; a re-record rarely tells you more than the captured stderr
398
+ already does.
399
+ <!-- END triage-canonical -->
400
+
401
+ **Is it your skill's bug, or a known harness gap?** Before deep-debugging a wrong behavior, rule out a
402
+ **deliberate fidelity gap** — the harness intentionally does *not* reproduce a few real-Cowork behaviors,
403
+ so a "bug" you see here that real Cowork also has isn't yours to fix. The tier semantics are in
404
+ `references/fidelity-and-answers.md` (shipped); the specific deltas vs. real Cowork and the sandbox
405
+ boundary model live in `docs/fidelity-gaps.md` / `docs/boundary.md` (repo-only, not in the installed
406
+ payload). If the behavior is on that gap list, it's expected — stop debugging your skill.
407
+
408
+ ### Inspecting a run's observability output
409
+
410
+ A verdict is only the top of what a run records, and the run dir persists after the verdict
411
+ (`~/.cowork-harness/runs/…`). Beyond pass/fail, every `run`/`skill`/`chat` writes a `result.json` and a
412
+ trace you read back without a re-record — the debugging loop is *localize the failure from that
413
+ already-written evidence*, not re-run-and-hope. Use them to diagnose a failure (and, secondarily, to
414
+ decide which assertions from *Assertions: two orthogonal axes* are worth adding):
415
+
416
+ - **`cowork-harness trace <run-dir> --view <view>`** — focuses one of the run's rollups (the per-tool
417
+ call-count/timing table, the sub-agent dispatch tree, the gate lifecycle, the tool/error rollups, …);
418
+ bare `trace` digests the whole run. The view set is actively being extended — run `trace --help` for
419
+ the current list rather than relying on a fixed enumeration here.
420
+ - **`cowork-harness stats [--metric <m>]`** — aggregate across the run index: `cost`, `duration`,
421
+ `tokens`, `cache-tokens`, `model-cost`, `turns`, `pass-rate`.
422
+ - **`result.json` carries the raw fields** the assertions read: `toolDurations`, `models`, `toolErrors`,
423
+ `redundantToolCalls`, `modelUsage`, `thinking`, `skillActivity`, `subagents[]` (prompt/model/output/
424
+ `attributedSkillId`, `outputTruncated`), `context` (tools/mcpServers/availableSkills), `tasks`,
425
+ `workspaceFiles`, `presentedFiles`, `hookEvents`, `mcpErrors`, `contextEvents`, `resources`
426
+ (`probeFailures` distinguishes a failed sample from a tier that was never sampleable). Provenance/
427
+ evidence-health fields: `command` (`run`/`skill`/`record`/`chat`/`replay` — finer than `mode`),
428
+ `gateProvenance` (per-gate `scripted`/`decided(llm|external)`/`first-option`/`prompt` with a
429
+ `bySource` histogram), `evidenceErrors` (dropped/malformed telemetry lines per stream, incl.
430
+ `egressParse`), `fingerprint.frozen` (replay only — marks the shown staleness fingerprint as the
431
+ cassette's record-time value, not a fresh recompute), and `assertTextTruncated` (companion to
432
+ `outputTruncated` on a matched tool result). (Full per-field semantics: the README's "Observability
433
+ fields" section — repo-only; `schema/run-result.json` is the machine source.)
434
+ - **Opaque failure?** A failed run also records **`errorSource`** (where the failure originated) and
435
+ **`stderrLogPath`** (the captured agent stderr) — read those and `trace <run-dir>` *before* re-running;
436
+ a re-record rarely tells you more than the captured stderr already does. Also check
437
+ **`resultErrorKind`** (`"transport" | "agent" | "usage_limit"`) before spending another paid run: a
438
+ `"usage_limit"` failure is a quota exhaustion, not a skill bug — retry after the limit resets rather
439
+ than debugging; `"transport"`/`"agent"` means something actually broke, worth localizing before
440
+ re-running.
441
+ - **Attributing cost to sub-agent work.** `subagents[]` gives the dispatch tree — each sub-agent's
442
+ `model`, `toolsUsed`, `prompt`/`output`, and `attributedSkillId` — but **not** its own token/cost;
443
+ aggregate cost is per-**model** in `modelUsage` (and `trace --view usage`), not per-sub-agent. So a
444
+ cost spike from fan-out reads as `trace --view dispatches` (how many, which agent) against that model's
445
+ per-model usage — the harness doesn't line-item each sub-agent's tokens.
446
+ - **Debugging a wrong Cowork UI panel.** Each panel is reconstructed in `result.json`: **Progress** =
447
+ `tasks[]`, **Working folder** = `workspaceFiles[]` (classified output/mount/input, with a
448
+ `trace --view files` diff), **Context / Connectors** = `context` (tools / mcpServers / availableSkills),
449
+ **Scratch-pad → outputs** = `presentedFiles[]`. If a panel looks wrong in a run, read its field.
450
+
451
+ ### Debugging with `chat`
362
452
 
363
453
  `cowork-harness chat` opens an interactive multi-turn REPL against a live Cowork session. It is
364
454
  **not** an asserted test — no `assert:` block, no cassette. Use it to explore behavior, reproduce a
@@ -392,8 +482,8 @@ The startup banner now reads `type your message (/help for commands)` as a remin
392
482
 
393
483
  ## Gotchas — the "✓ passed ≠ correct" landmines
394
484
 
395
- Stated as *symptom → why → fix*. The full catalog (with `file:line`) is in the references; these
396
- are the ones that bite hardest.
485
+ Stated as *symptom → why → fix*. **This is the full landmine catalog;** `references/scenario-schema.md`
486
+ repeats the assertion/replay-relevant ones alongside the schema (a scoped subset, not a fuller list).
397
487
 
398
488
  1. **An assertion passed but tested nothing on the PR gate.** *Why:* on a manifest-less cassette
399
489
  `replay` skips filesystem/egress keys (`file_exists`, `user_visible_artifact`, `artifact_json`,
@@ -430,9 +520,9 @@ are the ones that bite hardest.
430
520
  6. **`dispatch_count_max` is an author-chosen budget, not a production cap.** It's a post-hoc count
431
521
  assertion: passing means "happened to dispatch ≤N this run," nothing more. Cowork imposes **no**
432
522
  in-conversation `Task`-dispatch cap — gate `1648655587`'s `{perTask:1, global:3}` governs the
433
- separate scheduled/cron-task session scheduler, not the `Task` tool (binary-verified; see SPEC
434
- §10). So there is no "skip-on-cap" for the harness to reproduce; use this key only to catch a
435
- fan-out you don't want.
523
+ separate scheduled/cron-task session scheduler, not the `Task` tool (binary-verified; details in
524
+ `SPEC.md` §10 — repo-only). So there is no "skip-on-cap" for the harness to reproduce; use this key
525
+ only to catch a fan-out you don't want.
436
526
 
437
527
  7. **`protocol` is rejected (not silently passed) if the scenario asserts egress** — boundary
438
528
  assertions need a sandboxed tier (`container`+). Good: this one fails loud by design.
@@ -455,13 +545,14 @@ are the ones that bite hardest.
455
545
  content assertion and gate on `file_exists` on the live lane instead.
456
546
 
457
547
  11. **An external decider returning `"first"` does not select option 1.** The `"first"` keyword
458
- shorthand is disabled for `--decider-cmd` / `--decider-dir` helpers (see §5). If your helper
548
+ shorthand is disabled for `--decider-cmd` / `--decider-dir` helpers (see *Choose an answer path*
549
+ → External deciders). If your helper
459
550
  accidentally emits `"first"` and no label named `"first"` exists, the gate fails — it does
460
551
  **not** silently pick the first option. This is intentional: a helper bug should fail loud, not
461
552
  green wrong. *Fix:* have helpers return a label name or numeric index.
462
553
 
463
554
  12. **`prompt_asset_missing` is a WARN, not a hard failure — greens can hide it.** The
464
- `prompt_asset_missing` verdict signal (see §8) does not block a green verdict. Scan the verdict
555
+ `prompt_asset_missing` verdict signal (see *Interpreting verdict signals*) does not block a green verdict. Scan the verdict
465
556
  signals section after every run; a run that greened with this signal ran against an incomplete
466
557
  prompt. *Fix:* treat `prompt_asset_missing` as a blocking error in CI by checking the signals
467
558
  array.
@@ -505,6 +596,11 @@ are the ones that bite hardest.
505
596
  any failure) — a failing scenario does NOT abort the batch. So a single `cowork-harness record cassettes/
506
597
  --rerecord-stale` surfaces ALL stale anchors in one pass (add `--concurrency <N>` to parallelize); a shell
507
598
  wrapper that loops one cassette at a time with `set -e` defeats this and rediscovers stale anchors serially.
599
+ Two durability properties make the batch safe to trust: each cassette is written **atomically** (a
600
+ same-directory temp file + rename), so an interrupted or OOM-killed batch never leaves a partial/corrupt
601
+ cassette — a failed scenario simply produces none; and under `--concurrency <N>` each scenario runs **fully
602
+ isolated** (its own egress sidecar network + proxy, its own per-session run dir), so parallel records don't
603
+ cross-talk — the concurrency bound exists only for the Docker address pool + API rate limits, not correctness.
508
604
 
509
605
  17. **Editing `scenarios/*.yaml` `assert:` does NOT change a plain `replay`.** *Why:* `replay` evaluates the
510
606
  assertions **frozen in the cassette** by default — it is byte-deterministic and ignores the working tree (so
@@ -546,10 +642,11 @@ are the ones that bite hardest.
546
642
  `resolved-tier` finding (re-record — the recording now exercises the wrong tier); a cassette with no
547
643
  `effectiveFidelity` at all, or an unloadable pinned `baseline:`, reports `unverifiable-tier` instead
548
644
  (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.
645
+ produces them. (Details: `docs/cassette.md` § tier staleness — repo-only.)
550
646
 
551
- For the complete gotcha list, the assertion catalog, the YAML schema, the fidelity/answer tables,
552
- and the CI recipe, read the files in `references/`.
647
+ For the assertion catalog, the YAML schema, the fidelity/answer tables, and the CI recipe, read the
648
+ files in `references/` (the gotchas above are the full list; the references repeat only the
649
+ assertion/replay-relevant ones).
553
650
 
554
651
  ## References
555
652
 
@@ -558,10 +655,13 @@ and the CI recipe, read the files in `references/`.
558
655
  redaction before the first hostloop/protocol record, derive budget assertions without a
559
656
  two-pass record. Start here when the question is "how do I do X", not "what does flag Y mean".
560
657
  - `references/scenario-schema.md` — scenario/session YAML schema, full assertion catalog (with each
561
- key's replay class), the web_fetch model, and the complete gotcha list.
658
+ key's replay class), the web_fetch model, and an assertion/replay-scoped gotcha subset (the full
659
+ landmine catalog lives in this SKILL's Gotchas section above).
562
660
  - `references/fidelity-and-answers.md` — fidelity tiers, answer paths, the determinism contract.
563
661
  - `references/ci-recipe.md` — the packaged GitHub Action, replay-vs-live lane split, and the four-stage
564
662
  GitHub Actions pipeline.
565
663
  - `scripts/scenario.py` — `scaffold` a valid scenario skeleton and `lint` scenarios for the
566
664
  no-silent-false-green invariants (both usable as CI steps).
567
- - `../../../docs/run-status.md` — checking a background run's status without `ps aux`.
665
+ - Checking a background run's status without `ps aux` — covered in *Checking whether a background run is
666
+ alive* (Part II) above; the fuller recipe is in `docs/run-status.md` (repo-only, not shipped with the
667
+ installed skill).
@@ -1,6 +1,6 @@
1
1
  # CI recipe — replay vs live lanes
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.27.0` (baseline `desktop-1.18286.0`).
3
+ Self-contained reference. Tracks `cowork-harness 0.29.0` (baseline `desktop-1.20186.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):
@@ -45,15 +45,16 @@ jobs:
45
45
  Why this is a step *you* write, not an Action input the harness provides for you: pulling Anthropic's
46
46
  binary is a call about your own relationship with their distribution terms — keeping it in your own
47
47
  version-controlled workflow keeps that decision and its execution yours, auditable, and outside any
48
- third-party action's code. See the [agent-binary provenance runbook](../../../../docs/maintenance.md) for
49
- the full recovery/verification story (including why `COWORK_AGENT_BINARY` substitutions are
48
+ third-party action's code. The agent-binary provenance runbook (`docs/maintenance.md` in the repo —
49
+ not shipped with the installed skill) has the full recovery/verification story (including why
50
+ `COWORK_AGENT_BINARY` substitutions are
50
51
  sha256-*checked* but not hard-blocking on mismatch — it's advisory for an intentional substitution).
51
52
 
52
53
  **Minimal token-free PR gate (manual form)** — the smallest thing worth committing; runs on stock
53
54
  GitHub-hosted runners, no token/Docker/agent:
54
55
 
55
56
  ```yaml
56
- - run: npm i -g "cowork-harness@>=0.27.0"
57
+ - run: npm i -g "cowork-harness@>=0.29.0"
57
58
  - run: cowork-harness lint scenarios/*.yaml # no silent false-greens
58
59
  - run: cowork-harness verify-cassettes cassettes/ # privacy + staleness
59
60
  - run: cowork-harness replay cassettes/ # token-free content/structure
@@ -187,7 +188,7 @@ jobs:
187
188
  with: { node-version: '20' }
188
189
  - uses: actions/setup-python@v5
189
190
  with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
190
- - run: npm i -g "cowork-harness@>=0.27.0"
191
+ - run: npm i -g "cowork-harness@>=0.29.0"
191
192
  - run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
192
193
  - run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
193
194
  - run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
@@ -216,7 +217,7 @@ jobs:
216
217
  echo "live=true" >> "$GITHUB_OUTPUT"
217
218
  fi
218
219
  - if: steps.guard.outputs.live == 'true'
219
- run: npm i -g "cowork-harness@>=0.27.0"
220
+ run: npm i -g "cowork-harness@>=0.29.0"
220
221
  - if: steps.guard.outputs.live == 'true'
221
222
  run: cowork-harness run scenarios/ --output-format json
222
223
  env:
@@ -1,6 +1,6 @@
1
1
  # Fidelity tiers & answer paths
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.27.0` (baseline `desktop-1.18286.0`).
3
+ Self-contained reference. Tracks `cowork-harness 0.29.0` (baseline `desktop-1.20186.0`).
4
4
 
5
5
  ## Fidelity tiers (`fidelity:` in the scenario)
6
6
 
@@ -58,7 +58,7 @@ regressions.
58
58
  | **Spawned helper** | `--decider-cmd '<helper>'` | depends on the helper |
59
59
  | **In-band (driving agent)** | `--decider-dir <dir>` (+ a Monitor that writes responses) | depends |
60
60
 
61
- The answer paths are orthogonal — don't mix them on one run. The in-band path's Monitor drives two
61
+ Scripted `answers:` and a terminal (`--decider-llm` / `--decider-cmd` / `--decider-dir` / `on_unanswered`) compose as a normal Chain — scripted rules resolve matched gates, the terminal answers whatever's left. What's mutually exclusive is picking **two terminals** at once (e.g. `--decider-llm` with `--decider-cmd`, or `--decider-llm` with an explicit `--on-unanswered`) — the CLI rejects that combination. The in-band path's Monitor drives two
62
62
  subcommands: `gates <dir>` (stream the pending questions) and `answer <dir> --gate N …` (reply to one).
63
63
 
64
64
  **The `--decider-llm` answer protocol.** The answering model is shown the gate's options **numbered** and