cowork-harness 0.26.0 → 0.28.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 (104) hide show
  1. package/.claude/skills/cowork-harness/SKILL.md +241 -82
  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 +114 -29
  5. package/.claude/skills/cowork-harness/references/task-recipes.md +62 -7
  6. package/.claude/skills/cowork-harness/scripts/assertion-keys.json +25 -1
  7. package/.claude/skills/cowork-harness/scripts/scenario.py +231 -4
  8. package/.env.example +7 -1
  9. package/CHANGELOG.md +427 -0
  10. package/CONTRIBUTING.md +12 -8
  11. package/DESIGN.md +4 -4
  12. package/README.md +190 -86
  13. package/RELEASING.md +123 -0
  14. package/SPEC.md +45 -26
  15. package/baselines/desktop-1.18286.2.json +296 -0
  16. package/baselines/desktop-1.19367.0.json +373 -0
  17. package/baselines/prompts/cowork-system-prompt-fingerprints.json +22 -0
  18. package/dist/agent/session.js +119 -19
  19. package/dist/agent/timeline.js +141 -0
  20. package/dist/assert.js +570 -73
  21. package/dist/boundary-paths.js +7 -6
  22. package/dist/cli.js +225 -108
  23. package/dist/critique/evaluator.js +199 -0
  24. package/dist/critique/evidence.js +60 -0
  25. package/dist/critique/package-evidence.js +141 -0
  26. package/dist/decide/external-channel.js +8 -6
  27. package/dist/decide/semantic-judge.js +146 -0
  28. package/dist/egress/proxy.js +23 -8
  29. package/dist/egress/sidecar.js +46 -8
  30. package/dist/errors.js +1 -1
  31. package/dist/hostloop/cowork-handler.js +224 -0
  32. package/dist/hostloop/workspace-handler.js +1 -2
  33. package/dist/io.js +1 -1
  34. package/dist/redact.js +2 -3
  35. package/dist/run/artifacts.js +218 -10
  36. package/dist/run/assemble-run-result.js +17 -0
  37. package/dist/run/cassette.js +1130 -228
  38. package/dist/run/chat-result.js +122 -0
  39. package/dist/run/chat.js +40 -7
  40. package/dist/run/computer-links.js +97 -28
  41. package/dist/run/diff.js +5 -5
  42. package/dist/run/display-translate.js +3 -3
  43. package/dist/run/doctor.js +23 -6
  44. package/dist/run/envelope.js +30 -11
  45. package/dist/run/execute.js +401 -47
  46. package/dist/run/matrix.js +21 -12
  47. package/dist/run/pre-run-manifest.js +157 -15
  48. package/dist/run/renderer.js +10 -3
  49. package/dist/run/repeat.js +10 -6
  50. package/dist/run/run-index.js +21 -3
  51. package/dist/run/run-status.js +43 -15
  52. package/dist/run/run.js +466 -34
  53. package/dist/run/runs-gc.js +48 -3
  54. package/dist/run/skill-files.js +7 -2
  55. package/dist/run/skill-hash.js +2 -2
  56. package/dist/run/skill-metadata.js +82 -0
  57. package/dist/run/timeline-fold.js +85 -0
  58. package/dist/run/trace-view.js +248 -23
  59. package/dist/run/verdict.js +15 -3
  60. package/dist/run/vm-path-ctx-file.js +2 -2
  61. package/dist/runtime/argv.js +35 -31
  62. package/dist/runtime/container.js +23 -6
  63. package/dist/runtime/hostloop-stage.js +15 -2
  64. package/dist/runtime/hostloop.js +44 -37
  65. package/dist/runtime/microvm.js +2 -4
  66. package/dist/runtime/protocol.js +15 -0
  67. package/dist/runtime/resource-sampler.js +252 -0
  68. package/dist/runtime/stage.js +7 -0
  69. package/dist/scan.js +6 -1
  70. package/dist/session.js +241 -47
  71. package/dist/sync/baseline-diff.js +1 -1
  72. package/dist/sync/cowork-sync.js +208 -27
  73. package/dist/types.js +200 -18
  74. package/docs/README.md +15 -11
  75. package/docs/boundary.md +1 -1
  76. package/docs/cassette.md +102 -23
  77. package/docs/cowork-spawn-contract-1.12603.1.md +2 -2
  78. package/docs/debugging.md +20 -5
  79. package/docs/decisions/2026-07-07-verification-strictness-and-fidelity.md +74 -0
  80. package/docs/discovery.md +6 -4
  81. package/docs/gotchas.md +19 -0
  82. package/docs/invariants.md +10 -7
  83. package/docs/maintenance.md +17 -8
  84. package/docs/plugin-root.md +78 -0
  85. package/docs/protocol.md +4 -0
  86. package/docs/run-status.md +5 -1
  87. package/docs/scenario.md +100 -23
  88. package/docs/session.md +25 -7
  89. package/docs/stats.md +13 -7
  90. package/examples/README.md +18 -4
  91. package/examples/replays/README.md +34 -4
  92. package/examples/replays/example-multiselect-gate.cassette.json +111 -53
  93. package/examples/replays/example-pdf-skill.cassette.json +184 -31
  94. package/examples/replays/hostloop-computer-links.cassette.json +97 -23
  95. package/llms.txt +2 -2
  96. package/package.json +11 -2
  97. package/python/README.md +7 -3
  98. package/python/cowork_harness.py +40 -1
  99. package/schema/cassette.v10.json +328 -0
  100. package/schema/cassette.v9.json +182 -0
  101. package/schema/run-result.json +866 -73
  102. package/schema/scenario.schema.json +220 -14
  103. package/schema/session.schema.json +48 -35
  104. package/schema/verify-cassettes.json +17 -5
@@ -3,8 +3,8 @@ name: cowork-harness
3
3
  description: Test or debug a Claude Code skill/plugin under Claude Cowork's runtime — sandboxed agent, default-deny egress, the can_use_tool permission/question protocol — using the cowork-harness CLI. Use when validating or regression-testing a skill, authoring or debugging a scenario YAML (prompt + scripted answers + assert:), choosing a fidelity tier, scripting AskUserQuestion / tool-permission answers, or asserting artifacts, egress, or sub-agent dispatch. Especially when a harness run no-ops an assertion, fails on an unanswered gate, false-greens, a steered answer never reaches the model, or a web_fetch is unexpectedly denied or gated. NOT for generic unit testing (pytest/vitest of your own scripts) or non-Cowork CI. Covers the skill / run / chat / record / replay / trace / decide / assertions / scaffold commands and the session-vs-scenario split.
4
4
  metadata:
5
5
  author: cowork-harness
6
- version: 0.26.0
7
- tracks-harness: cowork-harness 0.26.0 (baseline desktop-1.18286.0)
6
+ version: 0.28.0
7
+ tracks-harness: cowork-harness 0.28.0 (baseline desktop-1.19367.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.26.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.28.0` (baseline
26
+ > `desktop-1.19367.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,31 +38,54 @@ 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.26.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.26.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.26.0"`. **Pin `@>=0.26.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.26.0 is what gates the commands/assertions this skill teaches: `assertions --list`, `scaffold <run-id>`, `trace --view dispatches`, `artifact_json` incl. the `in:` operator, `verify-cassettes`, batch `record <dir>`/`--rerecord-stale`, `record --concurrency <N>`, record-time redaction, multiSelect/`answer:`, `verify-run` answer-coverage, `record --max-artifact-bytes`, live record-time deciders, `verify-cassettes --allow-domain`/`--allow-email`/`--allow-path`/`--allow-patterns-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0; `--allow-patterns-file <path>` is a FILE of patterns, one regex per line — not a path to allow, unlike `--allow <regex>`), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field, and `computer_links_resolve` (new in 0.22.0).)
41
+ - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.28.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.28.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.28.0"`. **Pin `@>=0.28.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.28.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
+ - **`--dotenv` is a GLOBAL flag — put it BEFORE the subcommand.** `cowork-harness --dotenv .env record …`, never `cowork-harness record … --dotenv .env`. Every *other* flag is subcommand-level, so muscle memory fights this one; the harness rejects the misplaced form with an exact-fix error, but placing it first avoids the round-trip.
45
46
 
46
- ## 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:
47
53
 
48
54
  - **"Is it even alive?"** (inner loop) → `cowork-harness skill <folder> "<prompt>"`. Fastest; no
49
55
  scenario file.
50
56
  - **Repeatable, asserted regression** → author a `scenarios/*.yaml` and run `cowork-harness run`.
51
57
  This is the CI-grade path and most of this skill.
52
- - **Multi-turn debugging** → `cowork-harness chat` (interactive; gates answered at the TTY, **not** an
53
- asserted test — see *Debugging with `chat`* in `docs/scenario.md`, or `docs/debugging.md` for the full
54
- 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.28.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**).
55
71
 
56
72
  Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
57
73
  verify-run · trace · inspect · diff · stats · decide · gates · answer · scaffold · assertions --list · sync ·
58
- list · boundary-check · status · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
74
+ list · boundary-check · status · vm <init|status|delete|prune> · doctor · init-redact`. Always check `cowork-harness <cmd> --help`.
59
75
 
60
76
  **Two different `scaffold` tools — don't confuse them.** The native `cowork-harness scaffold <run-id>`
61
77
  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).
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
83
+
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.
64
87
 
65
- ## 2. Two files: session vs scenario
88
+ ### Two files: session vs scenario
66
89
 
67
90
  - **`sessions/*.yaml`** — pre-prompt setup: `model`, mounts (`folders`), and discovery
68
91
  (marketplaces / plugins / skills / mcp). One session is reused by many scenarios. A scenario that
@@ -73,7 +96,7 @@ This split matters: release ground truth (`baseline:` / `baselines/`, produced b
73
96
  **separate** from authored setup (`session:` / `sessions/`). "profile" is retired vocabulary — do
74
97
  not use it. See `references/scenario-schema.md` for every field.
75
98
 
76
- ## 3. Discovery: how the skill-under-test gets mounted
99
+ ### Discovery: how the skill-under-test gets mounted
77
100
 
78
101
  The skill is **copied fresh into the sandbox each run**. Wire it via `plugins.local_plugins` +
79
102
  `plugins.enabled: [<plugin>@local]` in the session (or `--marketplace` / `--plugin` flags on
@@ -83,13 +106,17 @@ the folder basename (collision-resolved); there is no `to:` override. See `refer
83
106
 
84
107
  > **`git add` a brand-new skill before testing it.** Inside a git repo the harness stages the
85
108
  > **git-tracked** files (the fidelity boundary — real Cowork installs from a repo and sees only committed
86
- > 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
87
114
  > installed" then did the work itself — a green-looking run where the skill never loaded. That now
88
115
  > **hard-fails** (`BoundaryError`, exit 3) naming the dir, and a partially-tracked folder emits a loud
89
116
  > `::notice:: [stage]` listing the excluded files. Fix: `git add` the skill, or `COWORK_HARNESS_GITSET=0`
90
117
  > to copy untracked files (won't reflect what ships). A folder **outside** any repo is copied raw (no guard).
91
118
 
92
- ## 4. Choose a fidelity tier
119
+ ### Choose a fidelity tier
93
120
 
94
121
  | Tier | What it gives you | Use when |
95
122
  |---|---|---|
@@ -102,7 +129,7 @@ Set the tier in the **scenario's `fidelity:` field**, not a flag — `run` rejec
102
129
  (it's a `skill`/`chat` flag; `run` takes fidelity only from the scenario). See
103
130
  `references/fidelity-and-answers.md`.
104
131
 
105
- ## 5. Choose an answer path (gates: AskUserQuestion + tool-permission)
132
+ ### Choose an answer path (gates: AskUserQuestion + tool-permission)
106
133
 
107
134
  Default to **deterministic**: scripted `answers:` + `on_unanswered: fail`. Anything that brings a
108
135
  live model into answering flags the run `nonDeterministic` — keep those out of deterministic
@@ -131,7 +158,7 @@ anchor. (When label *order* is stable but the text drifts, a positional `choose`
131
158
  linter flags positional `choose` as order-dependent, so use it deliberately.) The caution stands: `first`
132
159
  *masks* an unanswered gate, so don't use it for a gate you actually need answered a specific way.
133
160
 
134
- ### External deciders and the "first" shorthand
161
+ #### External deciders and the "first" shorthand
135
162
 
136
163
  When using `--decider-cmd` or `--decider-dir`, the helper's output is passed through
137
164
  `coerceLabel` **with the "first" shorthand disabled**. This means a helper that returns the literal
@@ -142,22 +169,7 @@ The `"first"` shorthand remains active only for the built-in `--on-unanswered fi
142
169
  write an external helper, return a label name or option index — never the bare word `"first"` unless
143
170
  your gate actually has a label called `"first"`.
144
171
 
145
- ## 5a. Checking whether a background run is alive
146
-
147
- Never use `ps aux` to check on a `cowork-harness` run you launched in the background — it only sees
148
- processes in your OWN PID namespace, which is frequently NOT the harness process's namespace (e.g. when
149
- you're a sandboxed subagent). An empty `ps aux` match tells you nothing about whether the run is still
150
- going.
151
-
152
- Use **`cowork-harness status <dir> [--follow]`** instead — reads `<outDir>/status.json`, a file the
153
- harness writes/updates throughout the run's lifecycle (including a crash-safety net for a thrown
154
- error/`SIGTERM`, AND staleness detection for a hard `SIGKILL`/OOM-kill that no exit handler can catch —
155
- either way you get `"error"`/`stale` instead of a permanently-trusted `"running"`), so liveness is
156
- checkable regardless of PID namespace. The harness prints `[status] <outDir>` to stderr as soon as the
157
- run starts, so capture stderr to get the exact directory. `--follow` fails loud on a timeout/staleness
158
- rather than hanging forever. See `docs/run-status.md` for the full recipe.
159
-
160
- ## 6. Assertions: two orthogonal axes
172
+ ### Assertions: two orthogonal axes
161
173
 
162
174
  Conflating these is the **biggest landmine**. An assertion key has two independent properties:
163
175
 
@@ -173,11 +185,40 @@ Conflating these is the **biggest landmine**. An assertion key has two independe
173
185
 
174
186
  Getting Axis B wrong means a check that **does nothing in CI** — the harness warns loudly when it skips
175
187
  (an `::warning::` annotation, not a silent no-op — see the Axis B bullet above), and the bundled linter
176
- 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).
177
189
 
178
190
  See `references/scenario-schema.md` for the full assertion catalog with each key's replay class.
179
191
 
180
- ## 7. web_fetch (fail-closed, two-path)
192
+ #### Which assertion for which question (goal → key)
193
+
194
+ Beyond the outcome/content keys most scenarios reach for first (`result`, `transcript_*`,
195
+ `file_exists`/`user_visible_artifact`, `artifact_json`), the harness surfaces the agent's *behavior*
196
+ — tool health, sub-agent work, panels, skill attribution, resources — as assertable keys. Reach for
197
+ them by what you're trying to prove:
198
+
199
+ | You want to check that… | Reach for |
200
+ |---|---|
201
+ | the skill didn't error out of a tool | `tool_no_error: <regex>`, `max_tool_errors: <N>` |
202
+ | it didn't waste repeated identical calls | `max_redundant_tool_calls: <N>` |
203
+ | a deliverable reached the user | `user_visible_artifact: <path>` (+ `no_scratchpad_leak: true` if it delivers via `present_files` — **`container` only**) |
204
+ | a to-do workflow finished | `all_tasks_completed: true`, `task_status: {match, status}` |
205
+ | a skill / connector / tool was **offered** | `skill_available`, `connector_available`, `tool_available` (all `<regex>`) |
206
+ | a skill actually **ran** (or must NOT) | `skill_triggered: <regex>`, `no_skill_triggered: <regex>` |
207
+ | a tool ran **inside** a skill's scope | `skill_tool_used: {skill, tool}` |
208
+ | a sub-agent did the work | `subagent_output_contains: {contains}`, `subagent_dispatched: <regex>`, `dispatch_count_max: <N>` |
209
+ | a pre-existing input wasn't mutated | `input_unmodified: [<glob>]` (live/verify-run; not microvm) |
210
+ | a resource ceiling held | `max_peak_rss_bytes: <N>` (**live-only**) |
211
+ | a hook blocked / didn't block a tool | `hook_blocked: <regex>`, `no_hook_blocked: true` (replay needs a `controlOut` cassette) |
212
+ | every MCP round-trip succeeded | `no_mcp_error: true` (**live-only**) |
213
+ | a context compaction happened | `compaction_occurred: true` |
214
+
215
+ Every one of these still obeys the two axes above — several are live-only or need a `controlOut`
216
+ cassette on replay, so check the catalog's replay class before putting one on a PR gate.
217
+ `cowork-harness assertions --list` prints the full, always-current key set with one-line semantics
218
+ straight from the schema — treat it (and the catalog) as the source of truth; this map is a
219
+ goal-oriented index into it, not a second catalog.
220
+
221
+ ### web_fetch (fail-closed, two-path)
181
222
 
182
223
  `web_fetch` behaves unlike `curl`. A URL is gated by **provenance**, not the egress allowlist:
183
224
 
@@ -193,7 +234,47 @@ See `references/scenario-schema.md` for the full assertion catalog with each key
193
234
  Surprise to remember: adding a host to `egress.extra_allow` is a **no-op** for a provenanced fetch.
194
235
  Full model in `references/scenario-schema.md`.
195
236
 
196
- ## 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
197
278
 
198
279
  Read the verdict and the inline failing transcript. To pin a flaky-because-stochastic gate, paste
199
280
  the echoed `--answer "<q>=<choice>"` footer lines back into the scenario's `answers:` for a
@@ -218,7 +299,7 @@ launched from a repo root never drops sensitive skill inputs/outputs into it. Pa
218
299
  `COWORK_HARNESS_RUNS_DIR`) to relocate; in CI point it at a workspace path so an artifact-upload step can
219
300
  collect the runs.
220
301
 
221
- ### Validate a skill against real documents (not a cassette)
302
+ #### Validate a skill against real documents (not a cassette)
222
303
 
223
304
  The loops above build **deterministic regressions**. A different job — drive a skill against *real* input
224
305
  documents to judge whether it actually does the work (extraction, analysis), with no intent to record a
@@ -254,7 +335,7 @@ cassette — has its own recipe:
254
335
  proceeds. Rebuild with `--build-arg COWORK_FULL_PARITY=1` and point `COWORK_AGENT_IMAGE` at it for those
255
336
  skills.
256
337
 
257
- ### Interpreting verdict signals
338
+ #### Interpreting verdict signals
258
339
 
259
340
  The run verdict may include `WARN`-severity signals in addition to pass/fail. One to watch for:
260
341
 
@@ -263,51 +344,111 @@ The run verdict may include `WARN`-severity signals in addition to pass/fail. On
263
344
  the run can still green. If you see it, fix the asset path — a green with a missing asset is
264
345
  not a valid pass.
265
346
 
266
- ## 9. Scaffold a valid scenario, lint before you push, then place in CI
267
-
268
- Don't hand-write the YAML from memory — that's how invented keys (`assertions:` vs `assert:`,
269
- `json_file`, `answer_policy`) creep in. Start from the bundled generator, which emits the
270
- known-good skeleton (right tier, scripted `answers:` + `on_unanswered: fail`, content assertions
271
- separated from live-only ones, one concern per item) and **self-lints its own output**:
347
+ ### Checking whether a background run is alive
272
348
 
273
- ```bash
274
- S="${CLAUDE_PLUGIN_ROOT}/scripts/scenario.py"
275
- # Working from a repo checkout instead of an installed plugin?
276
- # S=".claude/skills/cowork-harness/scripts/scenario.py"
277
- python3 "$S" scaffold --name report-check --skill ./skills/report-gen \
278
- --prompt "Generate the weekly report to outputs/report.md." \
279
- --content 'weekly report' --artifact outputs/report.md \
280
- --egress-allowed api.weather.example.com --out scenarios/report-check.yaml
281
- ```
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.
282
353
 
283
- Then lint every scenario — it encodes the no-silent-false-green invariants. Use the CLI wrapper
284
- `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.)
285
362
 
286
- ```bash
287
- cowork-harness lint scenarios/*.yaml
288
- ```
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"`.)
289
368
 
290
- `lint` flags: filesystem/egress-only assertions on a `replay` gate (silent no-op), bad regex
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.
369
+ ### Place assertions in the right CI lane
299
370
 
300
371
  CI placement: a **token-free `replay` PR gate** (content/structure only) + a **nightly live `run`**
301
372
  (filesystem/egress). Fastest setup: `uses: yaniv-golan/cowork-harness@v1` (a packaged GitHub Action with a
302
373
  PR job-summary reporter). See `references/ci-recipe.md` for the Action, the manual step-by-step form, and
303
374
  the four-stage pipeline.
304
375
 
305
- ## 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`), `context` (tools/mcpServers/availableSkills), `tasks`, `workspaceFiles`,
425
+ `presentedFiles`, `hookEvents`, `mcpErrors`, `contextEvents`, `resources`. (Full per-field semantics:
426
+ the README's "Observability fields" section — repo-only; `schema/run-result.json` is the machine
427
+ source.)
428
+ - **Opaque failure?** A failed run also records **`errorSource`** (where the failure originated) and
429
+ **`stderrLogPath`** (the captured agent stderr) — read those and `trace <run-dir>` *before* re-running;
430
+ a re-record rarely tells you more than the captured stderr already does.
431
+ - **Attributing cost to sub-agent work.** `subagents[]` gives the dispatch tree — each sub-agent's
432
+ `model`, `toolsUsed`, `prompt`/`output`, and `attributedSkillId` — but **not** its own token/cost;
433
+ aggregate cost is per-**model** in `modelUsage` (and `trace --view usage`), not per-sub-agent. So a
434
+ cost spike from fan-out reads as `trace --view dispatches` (how many, which agent) against that model's
435
+ per-model usage — the harness doesn't line-item each sub-agent's tokens.
436
+ - **Debugging a wrong Cowork UI panel.** Each panel is reconstructed in `result.json`: **Progress** =
437
+ `tasks[]`, **Working folder** = `workspaceFiles[]` (classified output/mount/input, with a
438
+ `trace --view files` diff), **Context / Connectors** = `context` (tools / mcpServers / availableSkills),
439
+ **Scratch-pad → outputs** = `presentedFiles[]`. If a panel looks wrong in a run, read its field.
440
+
441
+ ### Debugging with `chat`
306
442
 
307
443
  `cowork-harness chat` opens an interactive multi-turn REPL against a live Cowork session. It is
308
444
  **not** an asserted test — no `assert:` block, no cassette. Use it to explore behavior, reproduce a
309
445
  bug interactively, or test a prompt before committing it to a scenario.
310
446
 
447
+ Each session still writes an informational `result.json` (`mode: "chat"`, no `assertions`) plus a
448
+ trace and index row under its run dir — the same telemetry (tool durations, model usage, resources,
449
+ etc.) that `run`/`skill` produce — so `cowork-harness trace <chat-run-dir>` / `stats` work on a chat
450
+ session too, even though it never yields a verdict.
451
+
311
452
  **`--plugin <dir>` flag (repeatable).** Load additional skill folders alongside the primary session
312
453
  plugin. Each `--plugin <dir>` appends the folder to `local_plugins`. Useful when the skill-under-test
313
454
  depends on a sibling plugin:
@@ -331,8 +472,8 @@ The startup banner now reads `type your message (/help for commands)` as a remin
331
472
 
332
473
  ## Gotchas — the "✓ passed ≠ correct" landmines
333
474
 
334
- Stated as *symptom → why → fix*. The full catalog (with `file:line`) is in the references; these
335
- are the ones that bite hardest.
475
+ Stated as *symptom → why → fix*. **This is the full landmine catalog;** `references/scenario-schema.md`
476
+ repeats the assertion/replay-relevant ones alongside the schema (a scoped subset, not a fuller list).
336
477
 
337
478
  1. **An assertion passed but tested nothing on the PR gate.** *Why:* on a manifest-less cassette
338
479
  `replay` skips filesystem/egress keys (`file_exists`, `user_visible_artifact`, `artifact_json`,
@@ -369,9 +510,9 @@ are the ones that bite hardest.
369
510
  6. **`dispatch_count_max` is an author-chosen budget, not a production cap.** It's a post-hoc count
370
511
  assertion: passing means "happened to dispatch ≤N this run," nothing more. Cowork imposes **no**
371
512
  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.
513
+ separate scheduled/cron-task session scheduler, not the `Task` tool (binary-verified; details in
514
+ `SPEC.md` §10 — repo-only). So there is no "skip-on-cap" for the harness to reproduce; use this key
515
+ only to catch a fan-out you don't want.
375
516
 
376
517
  7. **`protocol` is rejected (not silently passed) if the scenario asserts egress** — boundary
377
518
  assertions need a sandboxed tier (`container`+). Good: this one fails loud by design.
@@ -394,13 +535,14 @@ are the ones that bite hardest.
394
535
  content assertion and gate on `file_exists` on the live lane instead.
395
536
 
396
537
  11. **An external decider returning `"first"` does not select option 1.** The `"first"` keyword
397
- shorthand is disabled for `--decider-cmd` / `--decider-dir` helpers (see §5). If your helper
538
+ shorthand is disabled for `--decider-cmd` / `--decider-dir` helpers (see *Choose an answer path*
539
+ → External deciders). If your helper
398
540
  accidentally emits `"first"` and no label named `"first"` exists, the gate fails — it does
399
541
  **not** silently pick the first option. This is intentional: a helper bug should fail loud, not
400
542
  green wrong. *Fix:* have helpers return a label name or numeric index.
401
543
 
402
544
  12. **`prompt_asset_missing` is a WARN, not a hard failure — greens can hide it.** The
403
- `prompt_asset_missing` verdict signal (see §8) does not block a green verdict. Scan the verdict
545
+ `prompt_asset_missing` verdict signal (see *Interpreting verdict signals*) does not block a green verdict. Scan the verdict
404
546
  signals section after every run; a run that greened with this signal ran against an incomplete
405
547
  prompt. *Fix:* treat `prompt_asset_missing` as a blocking error in CI by checking the signals
406
548
  array.
@@ -444,6 +586,11 @@ are the ones that bite hardest.
444
586
  any failure) — a failing scenario does NOT abort the batch. So a single `cowork-harness record cassettes/
445
587
  --rerecord-stale` surfaces ALL stale anchors in one pass (add `--concurrency <N>` to parallelize); a shell
446
588
  wrapper that loops one cassette at a time with `set -e` defeats this and rediscovers stale anchors serially.
589
+ Two durability properties make the batch safe to trust: each cassette is written **atomically** (a
590
+ same-directory temp file + rename), so an interrupted or OOM-killed batch never leaves a partial/corrupt
591
+ cassette — a failed scenario simply produces none; and under `--concurrency <N>` each scenario runs **fully
592
+ isolated** (its own egress sidecar network + proxy, its own per-session run dir), so parallel records don't
593
+ cross-talk — the concurrency bound exists only for the Docker address pool + API rate limits, not correctness.
447
594
 
448
595
  17. **Editing `scenarios/*.yaml` `assert:` does NOT change a plain `replay`.** *Why:* `replay` evaluates the
449
596
  assertions **frozen in the cassette** by default — it is byte-deterministic and ignores the working tree (so
@@ -479,8 +626,17 @@ are the ones that bite hardest.
479
626
  `RunResult.artifacts`). A `mode: rw`/`rwd` folder's contents are captured with a full body, same as
480
627
  `outputs/`.
481
628
 
482
- For the complete gotcha list, the assertion catalog, the YAML schema, the fidelity/answer tables,
483
- and the CI recipe, read the files in `references/`.
629
+ 21. **A `fidelity: cowork` cassette can go stale in a way `skill`/`format` drift won't catch.** Its recorded
630
+ `effectiveFidelity` field pins which concrete tier (`hostloop` or `container`) the baseline resolved to
631
+ AT RECORD TIME. If a later Desktop baseline flips that resolution, `verify-cassettes` reports it as a
632
+ `resolved-tier` finding (re-record — the recording now exercises the wrong tier); a cassette with no
633
+ `effectiveFidelity` at all, or an unloadable pinned `baseline:`, reports `unverifiable-tier` instead
634
+ (couldn't check — also re-record). Both are `fidelity: cowork`-only; an explicit-tier scenario never
635
+ produces them. (Details: `docs/cassette.md` § tier staleness — repo-only.)
636
+
637
+ For the assertion catalog, the YAML schema, the fidelity/answer tables, and the CI recipe, read the
638
+ files in `references/` (the gotchas above are the full list; the references repeat only the
639
+ assertion/replay-relevant ones).
484
640
 
485
641
  ## References
486
642
 
@@ -489,10 +645,13 @@ and the CI recipe, read the files in `references/`.
489
645
  redaction before the first hostloop/protocol record, derive budget assertions without a
490
646
  two-pass record. Start here when the question is "how do I do X", not "what does flag Y mean".
491
647
  - `references/scenario-schema.md` — scenario/session YAML schema, full assertion catalog (with each
492
- key's replay class), the web_fetch model, and the complete gotcha list.
648
+ key's replay class), the web_fetch model, and an assertion/replay-scoped gotcha subset (the full
649
+ landmine catalog lives in this SKILL's Gotchas section above).
493
650
  - `references/fidelity-and-answers.md` — fidelity tiers, answer paths, the determinism contract.
494
651
  - `references/ci-recipe.md` — the packaged GitHub Action, replay-vs-live lane split, and the four-stage
495
652
  GitHub Actions pipeline.
496
653
  - `scripts/scenario.py` — `scaffold` a valid scenario skeleton and `lint` scenarios for the
497
654
  no-silent-false-green invariants (both usable as CI steps).
498
- - `../../../docs/run-status.md` — checking a background run's status without `ps aux`.
655
+ - Checking a background run's status without `ps aux` — covered in *Checking whether a background run is
656
+ alive* (Part II) above; the fuller recipe is in `docs/run-status.md` (repo-only, not shipped with the
657
+ installed skill).
@@ -1,6 +1,6 @@
1
1
  # CI recipe — replay vs live lanes
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.26.0` (baseline `desktop-1.18286.0`).
3
+ Self-contained reference. Tracks `cowork-harness 0.28.0` (baseline `desktop-1.19367.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.26.0"
57
+ - run: npm i -g "cowork-harness@>=0.28.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.26.0"
191
+ - run: npm i -g "cowork-harness@>=0.28.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.26.0"
220
+ run: npm i -g "cowork-harness@>=0.28.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.26.0` (baseline `desktop-1.18286.0`).
3
+ Self-contained reference. Tracks `cowork-harness 0.28.0` (baseline `desktop-1.19367.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