cowork-harness 0.27.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 (81) hide show
  1. package/.claude/skills/cowork-harness/SKILL.md +198 -108
  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 +73 -22
  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 +196 -2
  8. package/CHANGELOG.md +254 -0
  9. package/CONTRIBUTING.md +12 -5
  10. package/DESIGN.md +4 -4
  11. package/README.md +48 -38
  12. package/RELEASING.md +123 -0
  13. package/SPEC.md +20 -12
  14. package/baselines/desktop-1.18286.2.json +296 -0
  15. package/baselines/desktop-1.19367.0.json +373 -0
  16. package/baselines/prompts/cowork-system-prompt-fingerprints.json +22 -0
  17. package/dist/agent/session.js +14 -12
  18. package/dist/agent/timeline.js +6 -2
  19. package/dist/assert.js +245 -42
  20. package/dist/cli.js +83 -63
  21. package/dist/critique/evaluator.js +199 -0
  22. package/dist/critique/evidence.js +60 -0
  23. package/dist/critique/package-evidence.js +141 -0
  24. package/dist/decide/external-channel.js +8 -6
  25. package/dist/decide/semantic-judge.js +146 -0
  26. package/dist/egress/sidecar.js +3 -1
  27. package/dist/hostloop/workspace-handler.js +1 -2
  28. package/dist/redact.js +2 -3
  29. package/dist/run/artifacts.js +184 -17
  30. package/dist/run/cassette.js +441 -155
  31. package/dist/run/chat-result.js +12 -1
  32. package/dist/run/computer-links.js +54 -3
  33. package/dist/run/diff.js +1 -1
  34. package/dist/run/doctor.js +22 -5
  35. package/dist/run/execute.js +167 -31
  36. package/dist/run/pre-run-manifest.js +74 -33
  37. package/dist/run/run-status.js +15 -2
  38. package/dist/run/run.js +51 -11
  39. package/dist/run/runs-gc.js +48 -3
  40. package/dist/run/skill-files.js +7 -2
  41. package/dist/run/skill-hash.js +2 -2
  42. package/dist/run/trace-view.js +171 -15
  43. package/dist/runtime/argv.js +28 -28
  44. package/dist/runtime/container.js +2 -4
  45. package/dist/runtime/hostloop-stage.js +15 -2
  46. package/dist/runtime/hostloop.js +33 -24
  47. package/dist/runtime/microvm.js +2 -4
  48. package/dist/runtime/protocol.js +15 -0
  49. package/dist/runtime/resource-sampler.js +7 -1
  50. package/dist/runtime/stage.js +7 -0
  51. package/dist/scan.js +6 -1
  52. package/dist/session.js +171 -40
  53. package/dist/sync/cowork-sync.js +205 -24
  54. package/dist/types.js +84 -10
  55. package/docs/README.md +15 -11
  56. package/docs/boundary.md +1 -1
  57. package/docs/cassette.md +40 -9
  58. package/docs/cowork-spawn-contract-1.12603.1.md +2 -2
  59. package/docs/debugging.md +20 -5
  60. package/docs/discovery.md +6 -4
  61. package/docs/gotchas.md +12 -3
  62. package/docs/invariants.md +10 -7
  63. package/docs/maintenance.md +16 -7
  64. package/docs/plugin-root.md +78 -0
  65. package/docs/protocol.md +4 -0
  66. package/docs/scenario.md +63 -15
  67. package/docs/session.md +23 -7
  68. package/docs/stats.md +13 -7
  69. package/examples/README.md +1 -1
  70. package/examples/replays/README.md +7 -4
  71. package/examples/replays/example-multiselect-gate.cassette.json +111 -53
  72. package/examples/replays/example-pdf-skill.cassette.json +184 -31
  73. package/examples/replays/hostloop-computer-links.cassette.json +50 -36
  74. package/llms.txt +1 -1
  75. package/package.json +11 -2
  76. package/python/README.md +3 -2
  77. package/python/cowork_harness.py +40 -1
  78. package/schema/cassette.v10.json +328 -0
  79. package/schema/run-result.json +764 -125
  80. package/schema/scenario.schema.json +58 -7
  81. 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.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.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.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,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.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
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.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**).
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
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.
65
87
 
66
- ## 2. Two files: session vs scenario
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*
@@ -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,101 @@ 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
 
347
+ ### Checking whether a background run is alive
348
+
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.
353
+
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.)
362
+
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"`.)
368
+
369
+ ### Place assertions in the right CI lane
370
+
371
+ CI placement: a **token-free `replay` PR gate** (content/structure only) + a **nightly live `run`**
372
+ (filesystem/egress). Fastest setup: `uses: yaniv-golan/cowork-harness@v1` (a packaged GitHub Action with a
373
+ PR job-summary reporter). See `references/ci-recipe.md` for the Action, the manual step-by-step form, and
374
+ the four-stage pipeline.
375
+
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
+
302
408
  ### Inspecting a run's observability output
303
409
 
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:
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):
307
415
 
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.
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.
311
420
  - **`cowork-harness stats [--metric <m>]`** — aggregate across the run index: `cost`, `duration`,
312
421
  `tokens`, `cache-tokens`, `model-cost`, `turns`, `pass-rate`.
313
422
  - **`result.json` carries the raw fields** the assertions read: `toolDurations`, `models`, `toolErrors`,
314
423
  `redundantToolCalls`, `modelUsage`, `thinking`, `skillActivity`, `subagents[]` (prompt/model/output/
315
424
  `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.
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.)
318
428
  - **Opaque failure?** A failed run also records **`errorSource`** (where the failure originated) and
319
429
  **`stderrLogPath`** (the captured agent stderr) — read those and `trace <run-dir>` *before* re-running;
320
430
  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
- ```
338
-
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`):
341
-
342
- ```bash
343
- cowork-harness lint scenarios/*.yaml
344
- ```
345
-
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.
355
-
356
- CI placement: a **token-free `replay` PR gate** (content/structure only) + a **nightly live `run`**
357
- (filesystem/egress). Fastest setup: `uses: yaniv-golan/cowork-harness@v1` (a packaged GitHub Action with a
358
- PR job-summary reporter). See `references/ci-recipe.md` for the Action, the manual step-by-step form, and
359
- the four-stage pipeline.
360
-
361
- ## 10. Debugging with `chat`
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`
362
442
 
363
443
  `cowork-harness chat` opens an interactive multi-turn REPL against a live Cowork session. It is
364
444
  **not** an asserted test — no `assert:` block, no cassette. Use it to explore behavior, reproduce a
@@ -392,8 +472,8 @@ The startup banner now reads `type your message (/help for commands)` as a remin
392
472
 
393
473
  ## Gotchas — the "✓ passed ≠ correct" landmines
394
474
 
395
- Stated as *symptom → why → fix*. The full catalog (with `file:line`) is in the references; these
396
- 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).
397
477
 
398
478
  1. **An assertion passed but tested nothing on the PR gate.** *Why:* on a manifest-less cassette
399
479
  `replay` skips filesystem/egress keys (`file_exists`, `user_visible_artifact`, `artifact_json`,
@@ -430,9 +510,9 @@ are the ones that bite hardest.
430
510
  6. **`dispatch_count_max` is an author-chosen budget, not a production cap.** It's a post-hoc count
431
511
  assertion: passing means "happened to dispatch ≤N this run," nothing more. Cowork imposes **no**
432
512
  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.
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.
436
516
 
437
517
  7. **`protocol` is rejected (not silently passed) if the scenario asserts egress** — boundary
438
518
  assertions need a sandboxed tier (`container`+). Good: this one fails loud by design.
@@ -455,13 +535,14 @@ are the ones that bite hardest.
455
535
  content assertion and gate on `file_exists` on the live lane instead.
456
536
 
457
537
  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
538
+ shorthand is disabled for `--decider-cmd` / `--decider-dir` helpers (see *Choose an answer path*
539
+ → External deciders). If your helper
459
540
  accidentally emits `"first"` and no label named `"first"` exists, the gate fails — it does
460
541
  **not** silently pick the first option. This is intentional: a helper bug should fail loud, not
461
542
  green wrong. *Fix:* have helpers return a label name or numeric index.
462
543
 
463
544
  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
545
+ `prompt_asset_missing` verdict signal (see *Interpreting verdict signals*) does not block a green verdict. Scan the verdict
465
546
  signals section after every run; a run that greened with this signal ran against an incomplete
466
547
  prompt. *Fix:* treat `prompt_asset_missing` as a blocking error in CI by checking the signals
467
548
  array.
@@ -505,6 +586,11 @@ are the ones that bite hardest.
505
586
  any failure) — a failing scenario does NOT abort the batch. So a single `cowork-harness record cassettes/
506
587
  --rerecord-stale` surfaces ALL stale anchors in one pass (add `--concurrency <N>` to parallelize); a shell
507
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.
508
594
 
509
595
  17. **Editing `scenarios/*.yaml` `assert:` does NOT change a plain `replay`.** *Why:* `replay` evaluates the
510
596
  assertions **frozen in the cassette** by default — it is byte-deterministic and ignores the working tree (so
@@ -546,10 +632,11 @@ are the ones that bite hardest.
546
632
  `resolved-tier` finding (re-record — the recording now exercises the wrong tier); a cassette with no
547
633
  `effectiveFidelity` at all, or an unloadable pinned `baseline:`, reports `unverifiable-tier` instead
548
634
  (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.
635
+ produces them. (Details: `docs/cassette.md` § tier staleness — repo-only.)
550
636
 
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/`.
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).
553
640
 
554
641
  ## References
555
642
 
@@ -558,10 +645,13 @@ and the CI recipe, read the files in `references/`.
558
645
  redaction before the first hostloop/protocol record, derive budget assertions without a
559
646
  two-pass record. Start here when the question is "how do I do X", not "what does flag Y mean".
560
647
  - `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.
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).
562
650
  - `references/fidelity-and-answers.md` — fidelity tiers, answer paths, the determinism contract.
563
651
  - `references/ci-recipe.md` — the packaged GitHub Action, replay-vs-live lane split, and the four-stage
564
652
  GitHub Actions pipeline.
565
653
  - `scripts/scenario.py` — `scaffold` a valid scenario skeleton and `lint` scenarios for the
566
654
  no-silent-false-green invariants (both usable as CI steps).
567
- - `../../../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.27.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.27.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.27.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.27.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.27.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