cowork-harness 4.13.1 → 5.0.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 (197) hide show
  1. package/.claude/skills/cowork-harness/SKILL.md +9 -8
  2. package/.claude/skills/cowork-harness/references/assertion-catalog-agents-skills-budgets.md +2 -2
  3. package/.claude/skills/cowork-harness/references/assertion-catalog-gates-hooks-modifiers.md +9 -9
  4. package/.claude/skills/cowork-harness/references/assertion-catalog-outcome-files-tools.md +1 -1
  5. package/.claude/skills/cowork-harness/references/assertion-catalog.md +3 -4
  6. package/.claude/skills/cowork-harness/references/assertions-guide.md +6 -6
  7. package/.claude/skills/cowork-harness/references/authoring.md +8 -7
  8. package/.claude/skills/cowork-harness/references/ci-recipe.md +29 -37
  9. package/.claude/skills/cowork-harness/references/critique.md +1 -1
  10. package/.claude/skills/cowork-harness/references/debugging.md +5 -2
  11. package/.claude/skills/cowork-harness/references/eval.md +25 -14
  12. package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +30 -11
  13. package/.claude/skills/cowork-harness/references/gotchas.md +13 -8
  14. package/.claude/skills/cowork-harness/references/hillclimb-recipe.md +9 -8
  15. package/.claude/skills/cowork-harness/references/hillclimb.md +22 -20
  16. package/.claude/skills/cowork-harness/references/measurement.md +1 -1
  17. package/.claude/skills/cowork-harness/references/run-record-replay.md +15 -16
  18. package/.claude/skills/cowork-harness/references/scenario-schema.md +22 -18
  19. package/.claude/skills/cowork-harness/references/semantic-judging.md +11 -1
  20. package/.claude/skills/cowork-harness/references/task-recipes.md +13 -11
  21. package/.claude/skills/cowork-harness/scripts/assertion-keys.json +25 -2
  22. package/.claude/skills/cowork-harness/scripts/scenario.py +62 -37
  23. package/.env.example +2 -2
  24. package/CHANGELOG.md +456 -0
  25. package/CONTRIBUTING.md +16 -4
  26. package/DESIGN.md +2 -2
  27. package/README.md +11 -7
  28. package/RELEASING.md +30 -7
  29. package/SPEC.md +104 -66
  30. package/baselines/desktop-4.16336.0.json +2056 -0
  31. package/baselines/desktop-4.23118.0.json +2096 -0
  32. package/baselines/prompts/cowork-system-prompt-fingerprints.json +18 -1
  33. package/baselines/prompts/desktop-1.18286.0/system-prompt-append.md +2 -0
  34. package/dist/agent/session.js +3 -2
  35. package/dist/assert.js +57 -12
  36. package/dist/async-pool.js +30 -6
  37. package/dist/baseline.js +35 -17
  38. package/dist/cli.js +295 -120
  39. package/dist/critique/command.js +57 -23
  40. package/dist/critique/limitations.js +4 -4
  41. package/dist/decide/decider.js +6 -1
  42. package/dist/decide/external-channel.js +5 -0
  43. package/dist/decide/judge-call-error.js +56 -0
  44. package/dist/decide/llm-transport.js +208 -7
  45. package/dist/decide/pairwise-judge.js +20 -5
  46. package/dist/decide/semantic-judge.js +10 -1
  47. package/dist/effort-env.js +1 -1
  48. package/dist/eval/classify.js +18 -27
  49. package/dist/eval/command.js +123 -14
  50. package/dist/eval/headroom.js +64 -0
  51. package/dist/eval/job-runner.js +3 -1
  52. package/dist/eval/plan-history.js +27 -5
  53. package/dist/eval/plan.js +3 -2
  54. package/dist/eval/planner.js +16 -2
  55. package/dist/eval/report.js +204 -19
  56. package/dist/eval/runs.js +5 -1
  57. package/dist/eval/stopped.js +39 -0
  58. package/dist/eval/usage.js +18 -5
  59. package/dist/fixture/cli.js +3 -1
  60. package/dist/hillclimb/args.js +21 -0
  61. package/dist/hillclimb/budget.js +130 -0
  62. package/dist/hillclimb/check.js +35 -0
  63. package/dist/hillclimb/cli.js +92 -17
  64. package/dist/hillclimb/command.js +41 -7
  65. package/dist/hillclimb/cost.js +14 -9
  66. package/dist/hillclimb/flow.js +22 -7
  67. package/dist/hillclimb/gate-cmd.js +106 -0
  68. package/dist/hillclimb/gate.js +12 -1
  69. package/dist/hillclimb/job.js +13 -8
  70. package/dist/hillclimb/regrade.js +186 -47
  71. package/dist/hillclimb/rows.js +52 -13
  72. package/dist/hillclimb/run-command.js +90 -16
  73. package/dist/hillclimb/runner.js +400 -163
  74. package/dist/hillclimb/schema-check.js +11 -2
  75. package/dist/hillclimb/seed.js +370 -0
  76. package/dist/hillclimb/served-model.js +44 -20
  77. package/dist/hillclimb/snapshot.js +21 -0
  78. package/dist/hillclimb/trace.js +4 -4
  79. package/dist/hillclimb/usage.js +44 -5
  80. package/dist/host-inputs.js +428 -0
  81. package/dist/hostloop/cowork-handler.js +1 -2
  82. package/dist/hostloop/process-cwd.js +85 -2
  83. package/dist/loop-decision.js +1 -1
  84. package/dist/prompt/subagent-manifest.js +28 -2
  85. package/dist/prompt.js +9 -6
  86. package/dist/refs/cli.js +5 -2
  87. package/dist/removed-env.js +42 -0
  88. package/dist/run/agent-infra-end.js +29 -0
  89. package/dist/run/analyze-artifact-runtime.js +19 -4
  90. package/dist/run/analyze-skill.js +16 -4
  91. package/dist/run/artifacts.js +1 -2
  92. package/dist/run/cassette.js +772 -772
  93. package/dist/run/chat-result.js +6 -14
  94. package/dist/run/chat.js +42 -20
  95. package/dist/run/command-globals.js +2 -0
  96. package/dist/run/doctor.js +113 -60
  97. package/dist/run/envelope.js +11 -4
  98. package/dist/run/execute.js +171 -102
  99. package/dist/run/exit.js +156 -0
  100. package/dist/run/grader-stop.js +24 -0
  101. package/dist/run/hook-events.js +7 -18
  102. package/dist/run/host-action-runners.js +2 -2
  103. package/dist/run/judge-check.js +56 -0
  104. package/dist/run/lint-load.js +29 -2
  105. package/dist/run/model-provenance.js +28 -4
  106. package/dist/run/multi-leg-cassette.js +1 -0
  107. package/dist/run/multi-leg.js +12 -3
  108. package/dist/run/pairwise-prepass.js +31 -3
  109. package/dist/run/regrade.js +31 -4
  110. package/dist/run/renderer.js +3 -1
  111. package/dist/run/run-index.js +1 -1
  112. package/dist/run/run.js +40 -2
  113. package/dist/run/runs-gc.js +42 -29
  114. package/dist/run/scenario-tool.js +11 -5
  115. package/dist/run/skill-flag-surface.js +26 -2
  116. package/dist/run/skill-hash.js +60 -84
  117. package/dist/run/turn-cap.js +30 -0
  118. package/dist/run/turn-layout.js +5 -2
  119. package/dist/run/verdict.js +14 -20
  120. package/dist/run/verify-context.js +2 -0
  121. package/dist/runtime/agent-image.js +1 -1
  122. package/dist/runtime/argv.js +43 -21
  123. package/dist/runtime/container.js +3 -1
  124. package/dist/runtime/host-env.js +31 -8
  125. package/dist/runtime/hostloop-prompt.js +1 -2
  126. package/dist/runtime/hostloop-stage.js +13 -1
  127. package/dist/runtime/hostloop.js +102 -85
  128. package/dist/runtime/image-capabilities.js +1 -1
  129. package/dist/runtime/lima.js +12 -12
  130. package/dist/runtime/microvm.js +8 -6
  131. package/dist/runtime/passthrough.js +68 -0
  132. package/dist/runtime/protocol.js +99 -161
  133. package/dist/runtime/resource-sampler.js +1 -1
  134. package/dist/runtime/session-argv-fields.js +70 -0
  135. package/dist/runtime/stage.js +15 -6
  136. package/dist/runtime/vm-baseline-arg.js +1 -1
  137. package/dist/scan.js +29 -9
  138. package/dist/search-corpus/cli.js +6 -3
  139. package/dist/session.js +243 -81
  140. package/dist/staging/resolve.js +1 -1
  141. package/dist/sync/baseline-diff.js +8 -0
  142. package/dist/sync/cowork-sync.js +432 -28
  143. package/dist/sync/hostloop-fence.js +262 -0
  144. package/dist/sync/observed-spawn-env.js +2 -1
  145. package/dist/sync/spawn-env-post-writes.js +5 -3
  146. package/dist/sync/spawn-env-registry.js +40 -12
  147. package/dist/types.js +50 -11
  148. package/docs/README.md +2 -2
  149. package/docs/boundary.md +4 -2
  150. package/docs/cassette.md +58 -110
  151. package/docs/ci.md +18 -18
  152. package/docs/cli.md +69 -46
  153. package/docs/companion-skill.md +2 -2
  154. package/docs/critique.md +8 -7
  155. package/docs/debugging.md +24 -15
  156. package/docs/discovery.md +9 -6
  157. package/docs/eval.md +71 -22
  158. package/docs/fidelity-gaps.md +86 -20
  159. package/docs/gotchas.md +1 -1
  160. package/docs/hillclimb.md +104 -21
  161. package/docs/invariants.md +2 -1
  162. package/docs/maintenance.md +10 -3
  163. package/docs/plugin-root.md +1 -2
  164. package/docs/scenario.md +34 -37
  165. package/docs/session.md +122 -18
  166. package/docs/subagents.md +3 -7
  167. package/examples/replays/README.md +6 -6
  168. package/examples/replays/example-multiselect-gate.cassette.json +82 -51
  169. package/examples/replays/example-pdf-skill.cassette.json +83 -83
  170. package/examples/replays/hostloop-computer-links.cassette.json +68 -62
  171. package/examples/sessions/default.yaml +2 -2
  172. package/llms.txt +1 -1
  173. package/package.json +6 -1
  174. package/python/README.md +1 -1
  175. package/python/cowork_harness.py +2 -2
  176. package/python/test_run_dir_layout.py +10 -0
  177. package/python/test_scenario_lint.py +71 -10
  178. package/schema/cassette.v19.json +703 -0
  179. package/schema/critique-report.json +2 -2
  180. package/schema/regrade.json +81 -7
  181. package/schema/run-result.json +136 -7
  182. package/schema/scenario.schema.json +10 -10
  183. package/schema/session.schema.json +38 -0
  184. package/scripts/check-versions.ts +12 -5
  185. package/scripts/ci/hang-dump.sh +73 -0
  186. package/scripts/ci/vitest-file-timing.ts +47 -0
  187. package/scripts/classify-manifest-probe.ts +73 -0
  188. package/scripts/gen-schema.ts +3 -0
  189. package/baselines/desktop-1.11847.5.json +0 -80
  190. package/baselines/desktop-1.12603.1.json +0 -142
  191. package/baselines/desktop-1.13576.1.json +0 -222
  192. package/baselines/prompts/desktop-1.12603.1/host-loop-append.md +0 -8
  193. package/baselines/prompts/desktop-1.13576.1/host-loop-append.md +0 -11
  194. package/dist/run/migrate-run-dir.js +0 -838
  195. package/schema/cassette.v10.json +0 -312
  196. package/schema/cassette.v11.json +0 -313
  197. package/schema/cassette.v9.json +0 -184
@@ -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, asserting artifacts, egress, or sub-agent dispatch, measuring how long each tool call took (toolDurations / trace), or debugging a failed run or verdict from its result.json or transcript. 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. Also when iterating or hardening a skill across fixes, or grounding a skill's self-critique against its own run evidence — including a document-analysis skill (cap table, deck, financial model, transcript) that needs an uploaded file attached to be critiqued at all. Also for comparing two versions of a skill before merging an edit — did it make answers worse? (`eval`: paired A/B, pinned models) — or improving one round by round (`hillclimb`, the `/claude-api hillclimb` runner). 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 / critique / stats / eval / hillclimb commands and the session-vs-scenario split.
4
4
  metadata:
5
5
  author: cowork-harness
6
- version: 4.13.1
7
- tracks-harness: cowork-harness 4.13.1 (baseline desktop-4.4212.0)
6
+ version: 5.0.0
7
+ tracks-harness: cowork-harness 5.0.0 (baseline desktop-4.23118.0)
8
8
  ---
9
9
 
10
10
  # cowork-harness
@@ -26,8 +26,8 @@ allowlist). This skill exists mostly to keep you out of those traps — the *Inv
26
26
  full landmine catalog in [`references/gotchas.md`](references/gotchas.md) are the highest-value part.
27
27
  Read them.
28
28
 
29
- > **Version note:** the facts and `file:line` pointers here track `cowork-harness 4.13.1` (baseline
30
- > `desktop-4.4212.0`). If your checkout is newer, prefer the live `--help` and — in a repo checkout —
29
+ > **Version note:** the facts and `file:line` pointers here track `cowork-harness 5.0.0` (baseline
30
+ > `desktop-4.23118.0`). If your checkout is newer, prefer the live `--help` and — in a repo checkout —
31
31
  > `SPEC.md` / `docs/*.md` over this snapshot, and re-run the bundled linter.
32
32
 
33
33
  ## Preflight — make sure the harness can actually run
@@ -43,14 +43,14 @@ Before the first command, confirm the CLI is reachable and **fail loud** (never
43
43
 
44
44
  - **One-shot check.** Run `cowork-harness doctor [--tier <tier>]` first — a read-only prerequisite check that inspects Docker, the staged agent, the token, the baseline and the `device_bridge` image (built, or built on the first bridge run) in one pass. The bullets below explain each thing it checks (and how to fix it).
45
45
  - **Replay-only? Skip `doctor`.** Replaying committed cassettes needs no Docker, no staged agent, and no token — and every tier's `doctor` validates the auth token (the live tiers also Docker + the staged agent), so a ✗ there is expected, not a blocker. Go straight to `cowork-harness replay <cassette>`.
46
- - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 4.13.1**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@^4.13.1" <cmd>` (Node ≥ 22), or install once with `npm i -g "cowork-harness@^4.13.1"`. **Pin `@^4.13.1`, 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.
46
+ - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 5.0.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@^5.0.0" <cmd>` (Node ≥ 22), or install once with `npm i -g "cowork-harness@^5.0.0"`. **Pin `@^5.0.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.
47
47
 
48
48
  This skill documents the CURRENT surface, not release history. If `cowork-harness --version` is
49
49
  OLDER than the floor, the per-release record of what you are missing is [CHANGELOG.md](https://github.com/yaniv-golan/cowork-harness/blob/main/CHANGELOG.md)
50
50
  — upgrade rather than work around it, since this skill's `file:line` pointers and flag names track the floor.
51
51
  - **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.
52
52
  - **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. At `protocol` the plugin under test **is** delivered (`--plugin-dir`), but its hooks then run as native host processes — so a plugin declaring hooks is refused there until you pass `--allow-host-hooks` / `allow_host_hooks: true`.
53
- - **Auth.** `CLAUDE_CODE_OAUTH_TOKEN` (preferred), or `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`, via env or `.env`. Minting an OAuth token needs the **`claude` CLI** (`npm i -g @anthropic-ai/claude-code`, then `claude setup-token`).
53
+ - **Auth.** `CLAUDE_CODE_OAUTH_TOKEN` (preferred) or `ANTHROPIC_API_KEY` (`ANTHROPIC_AUTH_TOKEN`: `protocol` only), via env or `.env` — at every tier, `protocol` included (a Keychain or `/login` session is never used). With the token set, the agent gets it alone at `container`/`microvm`/`hostloop` (at `hostloop` it gets only Desktop's named shell vars plus `agent_env.passthrough`; at `protocol`, with `ANTHROPIC_API_KEY` set, the agent gets the key and not the token, since the harness passes the token there only when no key is set), and the graders (judge, LLM decider, evaluator) get it alone — `COWORK_HARNESS_GRADER_AUTH=inherit` passes every key ([semantic-judging](references/semantic-judging.md)). Minting an OAuth token needs the **`claude` CLI** (`npm i -g @anthropic-ai/claude-code`, then `claude setup-token`).
54
54
  - **Point at another `.env`** with `--dotenv <path>`, and relocate run output with `--run-dir <path>`; both work before or after the subcommand.
55
55
 
56
56
  ## Orient — the three loops
@@ -67,6 +67,7 @@ CI-grade scenario, and the post-hoc debug loop; the rest are narrower tools that
67
67
  no scenario file.
68
68
  - **Repeatable, asserted regression** → author a `scenarios/*.yaml` and run `cowork-harness run`.
69
69
  This is the CI-grade path and most of this skill.
70
+ - **Against the plugins and skills you have installed** → `plugins.from_host` ([schema](references/scenario-schema.md)).
70
71
  - **A run failed — or greened and you don't trust it** (the debugging loop) → **Read
71
72
  [`references/debugging.md`](references/debugging.md) before touching the run.** Its *Triage* table first
72
73
  splits "the skill misbehaved" from "a green you don't trust" — they need different tools — then names the
@@ -104,9 +105,9 @@ CI-grade scenario, and the post-hoc debug loop; the rest are narrower tools that
104
105
  > `node_modules/cowork-harness/docs/<name>.md` before assuming a pointer dangles. A **plugin**
105
106
  > install loads a trimmed source-only cache where those pointers genuinely do dangle.
106
107
 
107
- Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · migrate-run-dir · lint ·
108
+ Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
108
109
  lint-skill · analyze-skill · probe-dispatch ·
109
- verify-run · regrade · fixture · search-pool <build|export|import> · ref <freeze|verify> · trace · inspect · diff · critique · eval · eval report · hillclimb <run|check|state-template|freeze-ref|regrade> · stats · decide · gates · answer · scaffold · assertions --list · sync ·
110
+ verify-run · regrade · fixture · search-pool <build|export|import> · ref <freeze|verify> · trace · inspect · diff · critique · eval · eval report · hillclimb <run|check|state-template|freeze-ref|regrade|seed> · stats · decide · gates · answer · scaffold · assertions --list · sync ·
110
111
  list · boundary-check · status · vm <init|status|delete|prune> · doctor · init-redact`. Always check `cowork-harness <cmd> --help`.
111
112
 
112
113
  ## Invariants — how a green run lies
@@ -1,6 +1,6 @@
1
1
  # Assertion catalog: sub-agents, skills and connectors, budgets, tasks, delivery
2
2
 
3
- Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`). Part 2 of 3 of the per-key table. The conventions every key shares (an item with several
3
+ Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.0`). Part 2 of 3 of the per-key table. The conventions every key shares (an item with several
4
4
  keys is an AND; glob vs regex name matching) and the verdict-signal table are in
5
5
  [`assertion-catalog.md`](./assertion-catalog.md); the other parts are [`assertion-catalog-outcome-files-tools.md`](./assertion-catalog-outcome-files-tools.md), [`assertion-catalog-gates-hooks-modifiers.md`](./assertion-catalog-gates-hooks-modifiers.md).
6
6
 
@@ -8,7 +8,7 @@ keys is an AND; glob vs regex name matching) and the verdict-signal table are in
8
8
  |---|---|
9
9
  | `subagent_tool_used: <glob>` | a sub-agent used a tool matching this glob (same `*`/`?`, anchored, case-sensitive semantics as `tool_called`, including the empty/regex-ish rejection) |
10
10
  | `subagent_tool_absent: <glob>` | no sub-agent used a tool matching this glob (same rejection) |
11
- | `no_vm_path_file_op: true` | **`fidelity: hostloop` only** — NO gated file tool attempted a `/sessions`(-prefixed) path (`RunResult.fileToolAttempts`) — content-class, replay-checkable without `controlOut`; any other tier FAILS "cannot verify" (`/sessions/...` is valid there). **Only `true` is valid** |
11
+ | `no_vm_path_file_op: true` | **`fidelity: hostloop` only** — NO gated file tool attempted a `/sessions`(-prefixed) path (`RunResult.fileToolAttempts`) — content-class, replay-checkable; any other tier FAILS "cannot verify" (`/sessions/...` is valid there). **Only `true` is valid** |
12
12
  | `subagent_file_write: {path?, path_suffix?, tool?}` | a sub-agent-origin write attempt whose raw path equals `path` (exact) or ends with `path_suffix` has a paired non-error tool_result — the causal half of a delivery probe; requires one of `path`/`path_suffix`; `tool` defaults to Write/Edit/MultiEdit; content-class; tier-agnostic |
13
13
  | `subagent_dispatch_healthy: {type?, delivered?, path?, path_suffix?, no_vm_paths?}` | **`fidelity: hostloop` only** — composite: selects dispatch(es) via `type` (same matching as `subagent_dispatched`; omit to require every dispatch) and, for EACH selected dispatch, checks it (not just any sub-agent) delivered a paired non-error write (`delivered`, default true — narrowed by `path`/`path_suffix`, same exact-vs-suffix precedence as `subagent_file_write`) and made no `/sessions` VM-path attempt (`no_vm_paths`, default true) — both scoped to that dispatch's OWN `parentToolUseId`, the per-dispatch correlation `subagent_file_write` (which matches ANY sub-agent write) cannot express; a `type` that matches no dispatch FAILS; content-class (`RunResult.fileToolAttempts` + `RunResult.toolResults`); any non-hostloop tier FAILS "cannot verify" |
14
14
  | `subagent_dispatched: <regex>` | a sub-agent whose `dispatchAgentType`, binary-*resolved* `resolvedAgentType`, **or dispatch description** matches |
@@ -1,6 +1,6 @@
1
1
  # Assertion catalog: gates, hooks, path denial, verdict modifiers, egress, judged keys
2
2
 
3
- Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`). Part 3 of 3 of the per-key table. The conventions every key shares (an item with several
3
+ Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.0`). Part 3 of 3 of the per-key table. The conventions every key shares (an item with several
4
4
  keys is an AND; glob vs regex name matching) and the verdict-signal table are in
5
5
  [`assertion-catalog.md`](./assertion-catalog.md); the other parts are [`assertion-catalog-outcome-files-tools.md`](./assertion-catalog-outcome-files-tools.md), [`assertion-catalog-agents-skills-budgets.md`](./assertion-catalog-agents-skills-budgets.md).
6
6
 
@@ -18,20 +18,20 @@ refused at load: no question reaches the harness to grade.
18
18
  | `gate_answers_delivered: false` | asserts at least one answered gate's answer was **confirmed not delivered** (an observed delivery failure); an unobserved/null delivery does **not** satisfy this — for negative-path delivery tests. Requires a gate, so **mutually exclusive** with `questions_count_max: 0` (refused by `run`/`skill`/`record`) |
19
19
  | `gate_answer_count_min: <N>` | at least N AskUserQuestion gates fired AND were delivered non-error — presence companion to `gate_answers_delivered`'s vacuous-pass. **`: 0` asserts nothing** and does not satisfy that pairing; `>= 1` is **mutually exclusive** with `questions_count_max: 0` (refused by `run`/`skill`/`record`) |
20
20
  | `gates_all_scripted: true \| {include_permissions: true}` | every fired AskUserQuestion gate was answered by a scripted rule (`answers:`/`--answer`), not the LLM decider, `first` or a human; `include_permissions` adds tool permissions (`when_tool`/fixed rules count; a web_fetch deny that no answer source decided fails (`answered by abstain-fallback`), reachable only under `first`, not `on_unanswered: fail`). Replay re-classifies on the frozen `answers:`; zero gates passes, so pair with `gate_answer_count_min: >= 1` or `questions_count_max` (`lint`: `unpaired-gates-all-scripted`); details in the scenario reference (linked above) |
21
- | `hook_blocked: <regex>` | one of the harness's own PreToolUse hook callbacks blocked a tool whose name matches the regex (`RunResult.hookEvents`; a plugin's hook never reaches it: use `hook_event_blocked`) — evidence-unavailable if hook telemetry is absent. Replay: needs a `controlOut` cassette (a custom hook's decision lives only there, not the recorded stream). **Does not see the agent's own refusal**: on `hostloop` against Desktop 2.7032.0+ a relative `Read`/`Write`/`Edit` is denied by the agent's permission rules before any hook runs, so neither this key nor `path_denied` records it — assert it with `tool_result_contains: "denied by your permission settings"` |
22
- | `no_hook_blocked: true` | no tool was blocked by the harness's own hook callbacks (a plugin's hook never reaches it: use `no_hook_event_blocked`) (distinguishes a real tool crash from an intentional hook block) — evidence-unavailable if hook telemetry is absent. Replay: needs a `controlOut` cassette. **Only `true` is valid** |
21
+ | `hook_blocked: <regex>` | one of the harness's own PreToolUse hook callbacks blocked a tool whose name matches the regex (`RunResult.hookEvents`; a plugin's hook never reaches it: use `hook_event_blocked`) — evidence-unavailable if hook telemetry is absent. Replay reads a custom hook's decision from `controlOut` (not the recorded stream). **Does not see the agent's own refusal**: on `hostloop` against Desktop 2.7032.0+ a relative `Read`/`Write`/`Edit` is denied by the agent's permission rules before any hook runs, so neither this key nor `path_denied` records it — assert it with `tool_result_contains: "denied by your permission settings"` |
22
+ | `no_hook_blocked: true` | no tool was blocked by the harness's own hook callbacks (a plugin's hook never reaches it: use `no_hook_event_blocked`) (distinguishes a real tool crash from an intentional hook block) — evidence-unavailable if hook telemetry is absent. **Only `true` is valid** |
23
23
  | `hook_event_fired: <HookEvent>` | a **command hook** for this event (a plugin's `hooks/hooks.json` or manifest hook — `Stop`, `SessionStart`, `PostToolUse`, …) ran: a `hook_response` system frame with that `hook_event` was recorded (`RunResult.contextEvents`). Any outcome counts. The harness passes `--include-hook-events` whenever a staged plugin declares hooks — that is what puts events other than SessionStart/Setup on the stream — so a recording made without it reports "never fired". Content-class, grades on replay. Recorded end-to-end for `Stop` ([stop-hook-probe.scenario.yaml](https://github.com/yaniv-golan/cowork-harness/blob/main/examples/probes/stop-hook-probe.scenario.yaml)) and for `PreToolUse` at `container` (deciding by JSON and by exit 2); the other names match the same frame but have not each been recorded |
24
24
  | `hook_event_blocked: <HookEvent> \| {event, tool?, via?, min?, max?}` | a command hook for `event` blocked: the bare form counts exit-2 frames only; the object form also counts a JSON deny on an `outcome: success` frame (`via: exit2`/`json`/`any`, default any), `tool` = the tool that fired (`Bash` at container, `mcp__workspace__bash` at hostloop), `min`/`max` a range (`{event: Stop, max: 0}` is the negative). No frame in scope fails "never fired"; an unreadable frame (incl. a failed one on an `onFailure: "block"` hook) is evidence-unavailable when it could change the verdict. Content-class; details in the scenario reference (linked above) |
25
- | `no_hook_event_blocked: true \| {event, tool?}` | no command hook blocked: every `hook_response` frame (or every frame for `event`) neither exited 2 nor denied by JSON, the same rule as `hook_event_blocked`'s object form. **Never vacuous**: no frame in scope is evidence-unavailable, and `true` also needs a frame of an event other than SessionStart/Setup, because those stream even when hook events were not requested (they stream only when a staged plugin declares hooks). A frame whose outcome cannot be read (no exit code, a hook that started and never answered, stdout a redaction policy rewrote or the agent truncated, output the agent did not apply as a verdict, or a failed or timed-out frame on an event with an `onFailure: "block"` hook — see `hook_decision`) is evidence-unavailable. Hooks on PreCompact, PostCompact, ConfigChange, DirectoryAdded, Elicitation, ElicitationResult, InstructionsLoaded, Notification, SessionEnd, StopFailure, WorktreeCreate, WorktreeRemove, CwdChanged and FileChanged stream no frame at all, so a block there never shows: `true` is evidence-unavailable when one of those events has an `onFailure: "block"` hook, or when the run's hook sources could not all be read. Frames carry no plugin id, so a second staged plugin's hook, or a host plugin's at `protocol`, counts as a frame here (the run warns). Distinct from `no_hook_blocked`, which reads the harness's **own** PreToolUse callbacks from `controlOut`. Content-class |
25
+ | `no_hook_event_blocked: true \| {event, tool?}` | no command hook blocked: every `hook_response` frame (or every frame for `event`) neither exited 2 nor denied by JSON, the same rule as `hook_event_blocked`'s object form. **Never vacuous**: no frame in scope is evidence-unavailable, and `true` also needs a frame of an event other than SessionStart/Setup, because those stream even when hook events were not requested (they stream only when a staged plugin declares hooks). A frame whose outcome cannot be read (no exit code, a hook that started and never answered, stdout a redaction policy rewrote or the agent truncated, output the agent did not apply as a verdict, or a failed or timed-out frame on an event with an `onFailure: "block"` hook — see `hook_decision`) is evidence-unavailable. Hooks on PreCompact, PostCompact, ConfigChange, DirectoryAdded, Elicitation, ElicitationResult, InstructionsLoaded, Notification, SessionEnd, StopFailure, WorktreeCreate, WorktreeRemove, CwdChanged and FileChanged stream no frame at all, so a block there never shows: `true` is evidence-unavailable when one of those events has an `onFailure: "block"` hook, or when the run's hook sources could not all be read. Frames carry no plugin id, so a second staged plugin's hook counts as a frame here (the run warns). Distinct from `no_hook_blocked`, which reads the harness's **own** PreToolUse callbacks from `controlOut`. Content-class |
26
26
  | `hook_decision: {event, decision, tool?, min?, max?}` | counts `hook_response` frames for `event` whose decision (stdout JSON on an `outcome: success` frame, or exit 2 = deny) is `decision`: `allow`, `deny`, `ask`, `defer`, or the aliases `block` (deny), `approve` (allow); `tool`, `min`/`max` as for `hook_event_blocked` (default: at least one). Empty/non-JSON stdout is no decision; no frame fails "never fired"; an unreadable frame (incl. a failed one on an `onFailure: "block"` hook) is evidence-unavailable when it could change the verdict. Content-class, grades on replay; details in the scenario reference (linked above) |
27
- | `hook_output_contains: {event, stream?, text \| matches}` / `hook_output_not_contains: {…}` | a command hook **printed** (or never printed) a text on `stdout` / `stderr` / either (`stream`, default `any`) — some (or no) `hook_response` frame for `event` carries `text` (literal, case-sensitive) or `matches` (regex, case-insensitive, no multiline flag — `^`/`$` anchor the whole stream). For a hook that fails open on stderr while exiting 0. No frame for the event FAILS both. On a redacted stream, a literal miss and any `matches` result are evidence-unavailable for either key, while a literal hit outside a token counts; a miss is also evidence-unavailable on a missing field, a hook that started without a response, or output the agent truncated. Frames carry no plugin id (a second plugin, or at `protocol` without a sealed config dir a host-installed one, can answer). Content-class. Recorded end-to-end for `Stop` |
28
- | `vm_path_denied: true` | **`fidelity: hostloop` only** — at least one recorded path denial (`RunResult.pathDenials`, any source) targeted a `/sessions` VM path — evidence-unavailable if path-denial telemetry is absent. Replay: needs a `controlOut` cassette. Any other tier FAILS "cannot verify". **Only `true` is valid** |
29
- | `path_denied: {tool?, path_matches?, source?, agent_scope?}` | **`fidelity: hostloop` only** — a path denial matching ALL given matchers (`tool` glob, `path_matches` regex, `source` ∈ pretooluse/can_use_tool/permission_denied, `agent_scope` ∈ main/subagent/any) was recorded. Replay: needs a `controlOut` cassette. Any other tier FAILS "cannot verify" |
30
- | `no_path_denied: true` | **`fidelity: hostloop` only** — NO path denial was recorded at all (the channel is already path-scoped, unlike `no_hook_blocked`'s indiscriminate reject). Replay: needs a `controlOut` cassette. Any other tier FAILS "cannot verify". **Only `true` is valid** |
27
+ | `hook_output_contains: {event, stream?, text \| matches}` / `hook_output_not_contains: {…}` | a command hook **printed** (or never printed) a text on `stdout` / `stderr` / either (`stream`, default `any`) — some (or no) `hook_response` frame for `event` carries `text` (literal, case-sensitive) or `matches` (regex, case-insensitive, no multiline flag — `^`/`$` anchor the whole stream). For a hook that fails open on stderr while exiting 0. No frame for the event FAILS both. On a redacted stream, a literal miss and any `matches` result are evidence-unavailable for either key, while a literal hit outside a token counts; a miss is also evidence-unavailable on a missing field, a hook that started without a response, or output the agent truncated. Frames carry no plugin id (a second staged plugin can answer). Content-class. Recorded end-to-end for `Stop` |
28
+ | `vm_path_denied: true` | **`fidelity: hostloop` only** — at least one recorded path denial (`RunResult.pathDenials`, any source) targeted a `/sessions` VM path — evidence-unavailable if path-denial telemetry is absent. Any other tier FAILS "cannot verify". **Only `true` is valid** |
29
+ | `path_denied: {tool?, path_matches?, source?, agent_scope?}` | **`fidelity: hostloop` only** — a path denial matching ALL given matchers (`tool` glob, `path_matches` regex, `source` ∈ pretooluse/can_use_tool/permission_denied, `agent_scope` ∈ main/subagent/any) was recorded. Any other tier FAILS "cannot verify" |
30
+ | `no_path_denied: true` | **`fidelity: hostloop` only** — NO path denial was recorded at all (the channel is already path-scoped, unlike `no_hook_blocked`'s indiscriminate reject). Any other tier FAILS "cannot verify". **Only `true` is valid** |
31
31
  | `allow_permissive_auto_allow: true` | verdict modifier — suppresses the default-fail when the run recorded a cowork-parity permissive auto-allow; for tests that deliberately assert Cowork's permissive behavior |
32
32
  | `allow_missing_capability: true` | verdict modifier — suppresses the default-fail when the (partial "core") agent image omits a capability the skill used but real Cowork ships (OCR/LibreOffice/markitdown/opencv/PDF-tables). Assert only when the skill's fallback is genuinely equivalent; otherwise rebuild full parity (`--build-arg COWORK_FULL_PARITY=1`). Also opts out of the `requires_capabilities` declared-need check. Live tiers only |
33
- | `allow_l0_host_config_contamination: true` | verdict modifier — accept a contaminated L0 environment: suppresses the default-fail when `protocol` runs against your REAL config dir, where your installed plugins/skills/MCP servers are visible and may answer instead of the thing under test. Live tiers only |
34
33
  | `allow_stall: true` | verdict modifier — suppresses the `stalled` default-fail when a run ends on a question or (after an `AskUserQuestion` gate) a closing request for input having done no productive tool work after its last gate (the agent asked for input and stopped — incl. re-asking in plain text after answering an `AskUserQuestion`); assert only when ending on a question is intended, else script the answer (`answer:` / `--answer` / a decider). On an open-ended `skill` / `probe-dispatch` run (no `assert:` block) pass `--allow-stall` instead. A helpful closing offer ("want me to run this through a structured pass?") also fails `stalled` — read the final message before believing it |
34
+ | `allow_max_turns: true` | verdict modifier — a run stopped by the session's `agent_max_turns` cap completes instead of failing (`eval`: `turn_cap_allowed`). `result` stays `error`: never beside `result: success` |
35
35
  | `allow_undelivered_deliverables: true` | verdict modifier — suppresses the `undelivered_deliverables` WARN. Working in the scratchpad is Cowork's designed pattern, so a skill that legitimately leaves intermediates, caches or downloaded inputs behind can say so instead of carrying permanent noise. The signal is warn-only and never fails a run on its own; reach for this when the scratch activity is intentional, not to silence a real delivery gap |
36
36
  | `allow_outputs_delete: true` | verdict modifier — accepts a detected outputs delete instead of failing the run, for a skill whose deletion is intended. Has an effect only on a baseline recording outputs `rw` (Desktop before 2.16120.0), where omitting `no_delete_in_outputs` does **not** permit deletes; on `rwd` (2.16120.0+) an outputs delete does not fail by default, so it is an accepted no-op (no warning), unless `no_delete_in_mounts` arms the outputs check, where it waives that check's signal as on `rw`. **Mutually exclusive** with `no_delete_in_outputs`. Silences `outputs_delete`, `outputs_delete_unconfirmed` and `outputs_diff_unavailable`. Waives the harness's post-hoc detection; it does not model Cowork's `allow_cowork_file_delete` approval handshake |
37
37
  | `allow_delete_in: [<mount>…]` | verdict modifier — accepts detected deletes in the named mounts (the per-mount analogue of `allow_outputs_delete`, mirroring production's `fileDeleteApprovedMounts`). Waives the verdict only; detection still runs and the hits stay in `result.json`. Listing `outputs` conflicts with `no_delete_in_outputs` |
@@ -1,6 +1,6 @@
1
1
  # Assertion catalog: outcome, transcript, files and artifacts, tools, tool results
2
2
 
3
- Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`). Part 1 of 3 of the per-key table. The conventions every key shares (an item with several
3
+ Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.0`). Part 1 of 3 of the per-key table. The conventions every key shares (an item with several
4
4
  keys is an AND; glob vs regex name matching) and the verdict-signal table are in
5
5
  [`assertion-catalog.md`](./assertion-catalog.md); the other parts are [`assertion-catalog-agents-skills-budgets.md`](./assertion-catalog-agents-skills-budgets.md), [`assertion-catalog-gates-hooks-modifiers.md`](./assertion-catalog-gates-hooks-modifiers.md).
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Assertion catalog
2
2
 
3
- Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`). Every `assert:` key with its semantics, and the
3
+ Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.0`). Every `assert:` key with its semantics, and the
4
4
  verdict-signal table. Which keys survive `replay` is in [`scenario-schema.md`](./scenario-schema.md#replay-class);
5
5
  the scenario and session YAML fields are there too.
6
6
 
@@ -50,9 +50,9 @@ paraphrases (that re-records red). Structured JSON → assert it in YAML with **
50
50
  `path` + operator, or `schema:` for a whole value's shape); use the pytest lane (`assert_artifact_json`) only for
51
51
  predicates neither can express.
52
52
 
53
- **VerdictSignals in `result.verdict.signals`:** `computeVerdict` pushes signals into `result.verdict.signals`; twelve
53
+ **VerdictSignals in `result.verdict.signals`:** `computeVerdict` pushes signals into `result.verdict.signals`; eleven
54
54
  are **fail**-severity (they flip the run's pass/exit code even though `result.result` itself stays
55
- `"success"`) and eighteen are **warn**-severity (informational, never flip pass/fail). All thirty signal
55
+ `"success"`) and eighteen are **warn**-severity (informational, never flip pass/fail). All twenty-nine signal
56
56
  codes (`VerdictSignal["code"]` in `src/run/verdict.ts`):
57
57
 
58
58
  | Code | Severity | Meaning |
@@ -70,7 +70,6 @@ codes (`VerdictSignal["code"]` in `src/run/verdict.ts`):
70
70
  | `device_delete_unconfirmed` | warn | `device_bridge`, on a filesystem that can give a new file a deleted file's identity (`rename-reads-as-delete`): files are gone from a connected folder deletion was never allowed in, either deleted past the device's refusal or renamed and then edited, which that filesystem cannot tell apart |
71
71
  | `device_audit_unreadable` | warn | `device_bridge`: the shim's delete log was replaced by the device shell (a link, a FIFO), so the number of refused deletes, and deletes of files the session made after a grant, are unknown (`deviceBridge.auditUnreadable`; `refusedDeletes` is left out). The before-and-after comparison of the folders still ran |
72
72
  | `host_path_leak` | fail | A host path leaked into model-visible text (opt out: author `transcript_no_host_path`). A path copied verbatim from the scenario's own uploads, connected folders, prompt, or declared plugins' or local skills' files is not a leak |
73
- | `l0_host_config_contamination` | fail | `protocol` ran against the operator's REAL config dir, so host-installed plugins/skills/MCP servers may have answered instead of the thing under test — the run did not necessarily measure it (opt out: `allow_l0_host_config_contamination`). Name predates the meaning: delivery at L0 works, contamination is what this reports |
74
73
  | `missing_capability` | fail | A `requires_capabilities` need was unmet, or the skill used a capability the image omits (opt out: `allow_missing_capability`, or `skill --allow-missing-capability` on an open-ended run) |
75
74
  | `infra_error` | fail | A supervising process died mid-run (the VM/egress sidecar, or the `device_bridge` container) — the run's evidence is contaminated, not author-suppressible |
76
75
  | `stalled` | fail | The run ended asking for input with no tool work after the last gate. "Asking" = the final turn's closing sentence ends in `?`; or, once an `AskUserQuestion` gate has fired, it ends in `?` after trailing bold/quotes/`)`/emoji, is a `?` followed only by a `For example: …`/parenthetical aside, or is a request that says the input comes back to the agent (`Please share/provide/upload… so I can…`/`…here`, `Let me know which… you'd like me to use`, `Once you share… I'll…`, `I need X to proceed`, `Once I have the file, I'll…` after a sentence asking for it). Polite closers and hand-offs (`Let me know if…`, `Feel free…`, `with your`/`to your`, `before sending`, `If you…`) and a closing code block or blockquote never count. Hand-offs to a named third party (`with the team`, `to the founders`, `with the CFO`) never count either; `here` is a cue only after share/paste/upload/drop/reply/type/send or as the last word. English-only. Opt out with `allow_stall` (on `run`, `replay` and `eval`); without it, under `eval` a stalled rep is `errored_agent` and fails every row |
@@ -1,6 +1,6 @@
1
1
  # Assertions guide
2
2
 
3
- Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`). Read it when choosing assertion keys: the two orthogonal axes and the goal → key map. The full catalog starts at `assertion-catalog.md`, which indexes the per-family files that hold every key's row.
3
+ Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.0`). Read it when choosing assertion keys: the two orthogonal axes and the goal → key map. The full catalog starts at `assertion-catalog.md`, which indexes the per-family files that hold every key's row.
4
4
 
5
5
  ### Assertions: two orthogonal axes
6
6
 
@@ -50,15 +50,15 @@ them by what you're trying to prove:
50
50
  | the user was **shown** the right choices, in order | `question_options: {when_question, equals: [..]}` — the option SET/ORDER a gate offered (`question_asked` matches the text only); order is compared by default |
51
51
  | the user was **told something specific** at a gate | `question_context: {when_question, matches}` — a regex over the question label + option labels + option **descriptions**. Reach for this when the wording may land in an option's `description`, which `question_asked` and `question_options` cannot see |
52
52
  | every gate offered exactly one (or at most N) options of a kind | `question_option_count: {matches, exactly}` — counts the option LABELS matching a regex on EVERY sub-question asked (`when_question` narrows); zero sub-questions asked fails |
53
- | every gate was answered by a scripted rule (an unattended run that still asks) | `gates_all_scripted: true` with `gate_answer_count_min: 1` — fails naming any gate the LLM decider, `first`, or an external or human decider answered; `{include_permissions: true}` also fails cowork parity's permissive auto-allow (replay needs a `controlOut` cassette) |
54
- | a **plugin's** command hook (any event: PreToolUse, Stop, …) blocked, didn't block, or decided | `hook_event_blocked: <event>` (exit 2) or `{event, tool?, via?, max: 0}`, `no_hook_event_blocked: true`, `hook_decision: {event, decision}` — stream content, no `controlOut` needed. A failed or timed-out frame on an event with an `onFailure: "block"` hook is evidence-unavailable, and so is unscoped `no_hook_event_blocked: true` when such a hook sits on an event that streams no frame |
55
- | the **harness's own** hook callbacks (the built-in Task hook, a custom hook bundle) blocked / didn't block a tool | `hook_blocked: <regex>`, `no_hook_blocked: true` (replay needs a `controlOut` cassette) — a plugin's hook never reaches this list, so `no_hook_blocked` passes over a plugin block |
53
+ | every gate was answered by a scripted rule (an unattended run that still asks) | `gates_all_scripted: true` with `gate_answer_count_min: 1` — fails naming any gate the LLM decider, `first`, or an external or human decider answered; `{include_permissions: true}` also fails cowork parity's permissive auto-allow |
54
+ | a **plugin's** command hook (any event: PreToolUse, Stop, …) blocked, didn't block, or decided | `hook_event_blocked: <event>` (exit 2) or `{event, tool?, via?, max: 0}`, `no_hook_event_blocked: true`, `hook_decision: {event, decision}` — stream content. A failed or timed-out frame on an event with an `onFailure: "block"` hook is evidence-unavailable, and so is unscoped `no_hook_event_blocked: true` when such a hook sits on an event that streams no frame |
55
+ | the **harness's own** hook callbacks (the built-in Task hook, a custom hook bundle) blocked / didn't block a tool | `hook_blocked: <regex>`, `no_hook_blocked: true` — a plugin's hook never reaches this list, so `no_hook_blocked` passes over a plugin block |
56
56
  | every MCP round-trip succeeded | `no_mcp_error: true` (**live-only**) |
57
57
  | a context compaction happened | `compaction_occurred: true` |
58
58
  | THIS run wrote the file (not just that it is there) | `file_exists: {path, authored: true}` (also `user_visible_artifact`, and `authored: true` on `artifact_text`/`artifact_json`) — an untouched pre-run file fails; needs the pre-run manifest, which `authored` arms |
59
59
 
60
- Every one of these still obeys the two axes above — several are live-only or need a `controlOut`
61
- cassette on replay, so check the catalog's replay class before putting one on a PR gate.
60
+ Every one of these still obeys the two axes above — several are live-only or need an artifact
61
+ manifest on replay, so check the catalog's replay class before putting one on a PR gate.
62
62
  `cowork-harness assertions --list` prints the full, always-current key set with one-line semantics
63
63
  straight from the schema — treat it (and the catalog) as the source of truth; this map is a
64
64
  goal-oriented index into it, not a second catalog.
@@ -1,6 +1,6 @@
1
1
  # Authoring a scenario
2
2
 
3
- Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`). Read it when composing a `scenarios/*.yaml`: session vs scenario, discovery, the fidelity tier, the answer path, `web_fetch`, and scaffold + lint.
3
+ Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.0`). Read it when composing a `scenarios/*.yaml`: session vs scenario, discovery, the fidelity tier, the answer path, `web_fetch`, and scaffold + lint.
4
4
 
5
5
  ## Part I — AUTHOR a scenario
6
6
 
@@ -45,7 +45,7 @@ the folder basename (collision-resolved); there is no `to:` override. See `refer
45
45
 
46
46
  | Tier | What it gives you | Use when |
47
47
  |---|---|---|
48
- | `protocol` | Fastest; no sandbox, no egress | Pure protocol/answer-shape tests. **Rejected** if the scenario asserts egress. |
48
+ | `protocol` | Fastest; no sandbox, no egress; needs a credential in env/.env (no Keychain/login) | Pure protocol/answer-shape tests. **Rejected** if the scenario asserts egress. |
49
49
  | `container` | Real sandbox + real default-deny egress (the pre-4.0 default; every scenario now names its tier). Models the **VM loop**: keeps the built-in `Bash`, but `WebFetch` is replaced by `mcp__workspace__web_fetch` — assert on that name, not `WebFetch`. (`run`/`record` only; `chat --fidelity container` still offers the built-in.) **The name is tier-specific**: `microvm` never offers it and `protocol` serves the operator's own host registry. A `tool_not_called`/`subagent_tool_absent` naming a tool its tier does not serve is **REFUSED at scenario load** (the message names what to write instead), so moving a scenario between tiers now errors rather than silently voiding the assertion — except at `protocol`, which is never judged | Most functional + boundary tests. |
50
50
  | `microvm` | VM-grade escape **isolation** (macOS arm64). Egress transport is the *same allowlist proxy as `container`* — not better network fidelity. Unlike `container`, still offers the built-in `WebFetch` | Testing untrusted code escape, not network behavior. |
51
51
  | `hostloop` / `cowork` | Production split-exec: the agent loop is a **native process on the host** (no container around the file tools — matching production), with **both** native `Bash` and `WebFetch` disabled and routed host-side via the workspace SDK-MCP server into a Docker VM sidecar (the VM loop replaces web_fetch only) | Highest-fidelity / parity runs. A writable connected folder needs `allow_host_writes: true` (see scenario-schema.md). |
@@ -288,12 +288,12 @@ cowork-harness lint scenarios/*.yaml
288
288
  | `capabilities-on-protocol` | ERROR | non-empty `requires_capabilities` on `protocol` without `allow_missing_capability` — the probe cannot run there, so the run fails as unverifiable |
289
289
  | `cassette-evidence-skipped` | INFO | with `--cassette-dir`: a cassette (or the directory) could not be read, so it cannot quiet replay-evidence advice |
290
290
  | `container-only-key-off-container` | ERROR | `no_scratchpad_leak` off `container` — hostloop's `present_files` never promotes, so there is nothing to leak (WARN on `cowork`, whose tier resolves per the baseline gate) |
291
+ | `contradictory-turn-cap` | WARN | `allow_max_turns: true` beside `result: success` (a capped run is `error`) |
291
292
  | `egress-on-protocol` | ERROR | an egress assertion (`egress_*` / `expect_denied`) on `protocol`, which enforces no egress |
292
293
  | `enum-value-invalid` | ERROR | a field value outside its allowed set |
293
294
  | `artifact-json-match` | ERROR | an `artifact_json` glob `artifact` (with `*` or `?`) with no `match` or ending in `/`, or a literal one that sets `match` |
294
295
  | `fidelity-missing` | ERROR | no `fidelity:` (required since 4.0.0) |
295
296
  | `file-absent-contradiction` | ERROR | one path under both `file_exists` and `file_absent` |
296
- | `gate-needs-controlout` | INFO | gate assertions, which evaluate on replay only when the cassette has `controlOut` |
297
297
  | `hook-output-control-char` | ERROR | a `hook_output_contains` / `hook_output_not_contains` `text` or `matches` holding a control character (a double-quoted YAML `\b` is a backspace) — the harness refuses it at load |
298
298
  | `host-path-assert-cowork` | WARN | `transcript_no_host_path` on `cowork` — it fails by design if the tier resolves to hostloop |
299
299
  | `host-path-assert-tier` | ERROR | `transcript_no_host_path` on `hostloop` / `protocol`, where it fails by design |
@@ -304,6 +304,7 @@ cowork-harness lint scenarios/*.yaml
304
304
  | `manifest-needs-snapshot` | INFO | manifest-backed keys, which evaluate on replay only when the cassette carries an `artifacts` manifest (not reported on `lane: remote` for `user_visible_artifact` / `artifact_json` / `artifact_text`, which cannot pass there) |
305
305
  | `mixed-assert-item` | WARN | one assert item mixing replay-checkable and live-only keys (replay drops the live-only half) |
306
306
  | `no-scenarios` | ERROR | a linted directory with no `*.yaml` / `*.yml` |
307
+ | `not-a-scenario` | INFO | a session file (only session keys); not checked |
307
308
  | `not-found` | ERROR | a named file that does not exist |
308
309
  | `parse` | ERROR | a file that is not YAML, or not a mapping |
309
310
  | `positional-choose-order` | INFO | an answer rule with a positional `choose` (first / index), which option re-ordering can move |
@@ -369,13 +370,13 @@ a reserved value — is ✗ ERROR `scenario-invalid` (exit 1, with or without `-
369
370
  naming no baseline this installed CLI ships is ✗ ERROR `baseline-unknown` (`latest` always resolves). It
370
371
  does not check what depends on the machine the run happens on (the session file and its mounts, an
371
372
  absolute `baseline:` path that does not exist here — one that exists is checked (4.1.1 and later) —
372
- environment variables). A session or matrix YAML in a linted directory is not
373
- a scenario and is reported as one that does not load — keep those out of the linted set. `python3
373
+ environment variables). A session file in a linted directory is one INFO `not-a-scenario`; a matrix YAML is
374
+ reported as one that does not load — keep those out of the linted set. `python3
374
375
  scenario.py lint` run directly stays offline and lenient: there an unknown key is only a ⚠ WARN (exit 0).
375
376
  `cowork-harness record <file.yaml> --dry-run` also runs the loader and adds the pre-spend refusals (exit 2
376
377
  on a schema error; a directory reports each `✗ broken:` file and exits 1). **Read the exit code, not just
377
378
  its sign:** `record <file>` — with or without
378
- `--dry-run` — answers `2` for "did not load" and `1` for "loaded fine, but this record is refused" (a
379
+ `--dry-run` — answers `2` for "did not load" or an unknown baseline name and `1` for "loaded fine, but this record is refused" (a
379
380
  pre-spend policy refusal, including `--max-budget-usd`, which exited 2 before 4.0.0; `skill`/`run` still
380
381
  exit 2 on it). Treating any non-zero
381
382
  as "scenario broken" mis-reports every refused-but-valid scenario. Corollary: **the loader** fails LOUD on an unknown key (never silently).
@@ -384,6 +385,6 @@ decided when the cassette is recorded. A key that changes what a verdict means (
384
385
  the cassette's version stamp, so the older `replay` / `verify-cassettes` refuses it as too new instead of
385
386
  evaluating it (`replay --best-effort-future-cassette` overrides that and names the key). For `lane: remote`
386
387
  that holds for a cassette recorded on ≥ 1.16.0 (stamped v11); one recorded by 1.14.0 or 1.15.0 is stamped
387
- v10, a pre-`lane` CLI ignores the key there, and `rehash` re-stamps it. A meaning-neutral key leaves the
388
+ v10, and a pre-`lane` CLI ignores the key there (5.0 reads v12+ only). A meaning-neutral key leaves the
388
389
  stamp alone and is ignored by design. Full split:
389
390
  [docs/scenario.md](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/scenario.md#unknown-keys-the-loader-is-strict-lint-is-lenient).
@@ -1,23 +1,23 @@
1
1
  # CI recipe — replay vs live lanes
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`).
3
+ Self-contained reference. Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.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):
7
7
 
8
8
  ```yaml
9
- - uses: yaniv-golan/cowork-harness@v4
9
+ - uses: yaniv-golan/cowork-harness@v5
10
10
  with:
11
11
  command: replay
12
12
  path: cassettes/
13
- version: "^4" # hold the major; see below
13
+ version: "^5" # hold the major; see below
14
14
  ```
15
15
 
16
- **These recipes pin `version: "^4"`.** The Action's `version` input *defaults* to `latest`, which means a
16
+ **These recipes pin `version: "^5"`.** The Action's `version` input *defaults* to `latest`, which means a
17
17
  CLI major reaches your workflow the moment it is promoted even though your `uses:` ref never changed — so a
18
- copy-pasted recipe that omits the input takes a major bump with no say in it. `^4` holds the major, needs no
18
+ copy-pasted recipe that omits the input takes a major bump with no say in it. `^5` holds the major, needs no
19
19
  patch number to remember, and only wants a human decision at the next major. Pin an exact version
20
- (e.g. `version: "4.13.1"`) instead when you want byte-reproducible CI.
20
+ (e.g. `version: "5.0.0"`) instead when you want byte-reproducible CI.
21
21
 
22
22
  Reach for the manual multi-step form below only when you need per-step control the Action's inputs don't
23
23
  cover (a custom flag combination, a different runner matrix per step, or `lint`/`verify-cassettes` gated
@@ -36,7 +36,7 @@ jobs:
36
36
  - uses: actions/checkout@v4
37
37
  - name: Stage the agent binary (official channel, sha256-verified against the pinned baseline)
38
38
  run: |
39
- V=2.1.309 # match your scenario's pinned baseline's agentVersion
39
+ V=2.1.317 # match your scenario's pinned baseline's agentVersion
40
40
  # The release channel is NOT always the stable one. Desktop also stages release CANDIDATES,
41
41
  # served from .../claude-code-releases/rc/<commit>/. For some versions the stable path 404s
42
42
  # (2.1.255); for others it returns 200 and serves a DIFFERENT BUILD UNDER THE SAME VERSION
@@ -57,11 +57,11 @@ jobs:
57
57
  echo "COWORK_AGENT_BINARY=$RUNNER_TEMP/claude-$V" >> "$GITHUB_ENV"
58
58
  # Background on the provenance chain: the "Agent-binary provenance" section of
59
59
  # https://github.com/yaniv-golan/cowork-harness/blob/main/docs/maintenance.md
60
- - uses: yaniv-golan/cowork-harness@v4
60
+ - uses: yaniv-golan/cowork-harness@v5
61
61
  with:
62
62
  command: run
63
63
  path: scenarios/
64
- version: "^4"
64
+ version: "^5"
65
65
  anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
66
66
  model: claude-sonnet-5 # used only where a scenario's session sets no `model:`
67
67
  ```
@@ -82,7 +82,7 @@ sha256-*checked* but not hard-blocking on mismatch — it's advisory for an inte
82
82
  GitHub-hosted runners, no token/Docker/agent:
83
83
 
84
84
  ```yaml
85
- - run: npm i -g "cowork-harness@^4.13.1"
85
+ - run: npm i -g "cowork-harness@^5.0.0"
86
86
  - run: cowork-harness lint scenarios/*.yaml --strict --min-severity WARN
87
87
  # no silent false-greens. WITHOUT --strict this
88
88
  # step cannot fail on a WARN-class rule (e.g.
@@ -157,21 +157,21 @@ Action has no input for, and it creates a coupling nothing checks:
157
157
  you have a reason:
158
158
 
159
159
  ```yaml
160
- - uses: yaniv-golan/cowork-harness@v4
160
+ - uses: yaniv-golan/cowork-harness@v5
161
161
  with:
162
162
  command: lint
163
163
  path: scenarios/
164
- version: "^4" # holds the major
164
+ version: "^5" # holds the major
165
165
  strict: true # without it, only ERROR fails the step
166
- extra-args: --min-severity WARN # needs a CLI >= 1.11.0; any 4.x satisfies that
166
+ extra-args: --min-severity WARN # needs a CLI >= 1.11.0; any 5.x satisfies that
167
167
  ```
168
168
 
169
169
  **If a flag you pass in `extra-args` landed in a specific release, bound the range — don't write a bare
170
170
  floor.** `>=1.11.0` reads as "at least 1.11.0" and silently means "and every future major too", so a
171
171
  recipe written that way hands a copy-paster the next major with no say in it. Anchor it at the current
172
- major instead — `version: "^4"`, which is what the steps above use — keeping the floor's intent while
172
+ major instead — `version: "^5"`, which is what the steps above use — keeping the floor's intent while
173
173
  stopping at the major boundary. An exact
174
- pin (`version: "4.0.0"`) is the right choice when you want byte-reproducible CI, at the cost of rotting the
174
+ pin (`version: "5.0.0"`) is the right choice when you want byte-reproducible CI, at the cost of rotting the
175
175
  moment a recipe adopts a newer flag.
176
176
 
177
177
  Without a satisfied floor, an older CLI fails the step with `unrecognized arguments: --min-severity WARN`
@@ -186,9 +186,8 @@ The split is not just about tokens — it decides **where each lane can run**:
186
186
  Docker, no agent binary** — runs on a stock GitHub Actions runner. Evaluates **content** assertions —
187
187
  `transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`, `skill_triggered`, `no_skill_triggered`,
188
188
  `max_cost_usd`, `max_tokens`, `tool_calls_max`, `result`, and the verdict modifiers
189
- `allow_permissive_auto_allow` / `allow_missing_capability` / `allow_l0_host_config_contamination` /
190
- `allow_stall` (no-op passes); plus the gate keys `question_asked` / `question_options` /
191
- `question_context` / `question_option_count` / `questions_count_max` / `gate_answers_delivered` **if** the cassette has `controlOut`, and the manifest keys
189
+ `allow_permissive_auto_allow` / `allow_missing_capability` / `allow_stall` (no-op passes); plus the gate keys `question_asked` / `question_options` /
190
+ `question_context` / `question_option_count` / `questions_count_max` / `gate_answers_delivered`, and the manifest keys
192
191
  (`file_exists` / `user_visible_artifact` / `artifact_json` / `artifact_text`) **if** it carries an artifact
193
192
  manifest. `file_absent` is in neither class — it is live/verify-run only.
194
193
  **That list is illustrative, not the authoritative set** — more keys are replay-checkable than fit a
@@ -199,7 +198,7 @@ The split is not just about tokens — it decides **where each lane can run**:
199
198
  ```
200
199
 
201
200
  It emits `{key, description}` — there is no structured replay-class field to filter on; the
202
- live-only / manifest / `controlOut` preconditions are stated in each key's `description` prose and,
201
+ live-only / manifest preconditions are stated in each key's `description` prose and,
203
202
  in full, in the catalog's replay-class tables in
204
203
  [`references/scenario-schema.md`](./scenario-schema.md).
205
204
 
@@ -252,8 +251,7 @@ cowork-harness replay cassettes/my-test.cassette.json # token-free re-evaluatio
252
251
  cowork-harness replay cassettes/ # replay every *.cassette.json in the committed dir
253
252
  ```
254
253
 
255
- Re-record whenever the protocol or your scenario's expected content changes. An old cassette without
256
- `controlOut` excludes the gate keys (with a loud warning) — re-record to enable them. `record` **refuses
254
+ Re-record whenever the protocol or your scenario's expected content changes. `record` **refuses
257
255
  to freeze a failing live run** into a cassette (pass `--allow-failing` to override) — a committed red
258
256
  cassette is a latent false-signal. An `--allow-failing` recording of a red run exits 0 with `ok: true`:
259
257
  `record`'s `ok` means a cassette was written, and the run's own verdict is `results[0].verdict.pass`
@@ -290,6 +288,7 @@ dollar figures). In a skill repo these cassettes get **committed**. So:
290
288
  still refuses the write and quarantines it. **You do not need to audit the session by hand to pass this
291
289
  flag**; that precondition was never decidable by the operator, and the scan is the real gate. Writing a
292
290
  recording the scan DID flag is the separate `--allow-host-inventory-findings`.
291
+ A `plugins.from_host` session is host-inheriting at **every** tier: its cassette names your installed set.
293
292
 
294
293
  Two details that matter for a re-record loop. The pre-spend check **warns rather than refuses when the
295
294
  cassette already exists**, deliberately: refusing there would fire on every `--rerecord-stale` pass and
@@ -359,18 +358,11 @@ A typical skill repo runs four stages, fastest/cheapest first:
359
358
 
360
359
  This is the shape a CI step wants: **silent on success (no output, exit 0), loud and specific on
361
360
  failure** — `--quiet` suppresses the readiness preview but never the `✗ broken:` lines, which name the
362
- offending file *and* the rejected key, one line per file, and the step still exits 1. It exits 0,
363
- though, on an input the real record would refuse (a `session:` file that cannot be read — 4.1.1 and
364
- later — a missing path, an unknown baseline name, a tier-vacuous negative tool check): that prints a `⚠ input error:` line and lands in `inputErrors[]`. To
365
- gate on those too, use the JSON form (4.1.0 and later):
366
-
367
- ```bash
368
- cowork-harness record scenarios/ --dry-run --output-format json | jq -e '.ok and (.inputErrors == [])'
369
- ```
370
-
371
- Point `lint`
372
- at scenarios only: a session or matrix YAML in the linted set is reported as a file that does not
373
- load.
361
+ offending file *and* the rejected key, one line per file, and the step still exits 1. An input the real record would refuse
362
+ (an unreadable `session:` file, a missing path, a tier-vacuous negative tool check) exits 1 too, with a `✗ refused:`
363
+ line and an entry in `refusals[]`; an unknown baseline name exits 2. The exit code is the gate. Point `lint`
364
+ at scenarios only: a session file in the linted set is one INFO, and a matrix YAML is reported as a file
365
+ that does not load.
374
366
 
375
367
  **If the repo pays for `critique`, gate the evidence corpus here first, for free:**
376
368
 
@@ -391,7 +383,7 @@ A typical skill repo runs four stages, fastest/cheapest first:
391
383
 
392
384
  ## GitHub Actions sketch
393
385
 
394
- The PR gate below is the manual, step-by-step version of what `uses: yaniv-golan/cowork-harness@v4` does
386
+ The PR gate below is the manual, step-by-step version of what `uses: yaniv-golan/cowork-harness@v5` does
395
387
  in one step (see the top of this doc) — reach for this form when you need independent per-command
396
388
  gating/annotations rather than one action run per command. The nightly live job has no packaged-Action
397
389
  equivalent yet (the Action's `command: run` mode needs a self-hosted runner with Docker + the agent binary
@@ -411,7 +403,7 @@ jobs:
411
403
  with: { node-version: '24' }
412
404
  - uses: actions/setup-python@v5
413
405
  with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
414
- - run: npm i -g "cowork-harness@^4.13.1"
406
+ - run: npm i -g "cowork-harness@^5.0.0"
415
407
  - run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
416
408
  - run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
417
409
  - run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
@@ -440,7 +432,7 @@ jobs:
440
432
  echo "live=true" >> "$GITHUB_OUTPUT"
441
433
  fi
442
434
  - if: steps.guard.outputs.live == 'true'
443
- run: npm i -g "cowork-harness@^4.13.1"
435
+ run: npm i -g "cowork-harness@^5.0.0"
444
436
  - if: steps.guard.outputs.live == 'true'
445
437
  run: cowork-harness run scenarios/ --output-format json
446
438
  env:
@@ -560,7 +552,7 @@ moved — and `unverifiable-fixture`, a `workspace_fixture` whose signature cann
560
552
  | `skill` / `shared-root` | the skill source the assertions validate drifted | **high** (assertions may validate dead code) |
561
553
  | `format` | the git/raw file-set mode or agent-scope differs from the recording | re-record under the same setting (waivable) |
562
554
  | `unverifiable-baseline` | the latest baseline couldn't be loaded | couldn't verify (env, not skill) |
563
- | `unverifiable-skill` | skill dirs unresolvable, **or** the cassette predates the hash-format epoch (v12) so its digest is not comparable | couldn't verify the skill. **Fails a bare `replay`.** For the epoch case try `rehash` first — it migrates without a re-record where it can prove the content unchanged (`rehash <file> --session <s.yaml>` if the cassette moved) |
555
+ | `unverifiable-skill` | skill dirs unresolvable | couldn't verify the skill. **Fails a bare `replay`.** Point at a moved cassette's session with `--session <s.yaml>` |
564
556
  | `resolved-tier` | a `fidelity: cowork` cassette's recorded `effectiveFidelity` no longer matches what the baseline resolves to today (the host-loop gate flipped) | **high** (the recording exercises the wrong tier) |
565
557
  | `unverifiable-tier` | tier check couldn't run for a `fidelity: cowork` cassette (no recorded `effectiveFidelity`, or its pinned baseline failed to load) | couldn't verify the tier — re-record |
566
558
  | `prompt-assets` | the baseline's committed prompt assets changed since record under the same appVersion | re-record (warns; `--strict` fails) |
@@ -1,6 +1,6 @@
1
1
  # Critique — the facts a plugin install can't otherwise reach
2
2
 
3
- Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`). This is **not** a trim of the full
3
+ Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.0`). This is **not** a trim of the full
4
4
  [`docs/critique.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/critique.md) (repo-only —
5
5
  flags, cost, reading several critiques, known limitations all live there). This file covers exactly what a
6
6
  plugin install cannot otherwise discover: the run-dir artifact a harvester actually reads, the report's
@@ -1,6 +1,6 @@
1
1
  # Debugging a run
2
2
 
3
- Tracks `cowork-harness 4.13.1` (baseline `desktop-4.4212.0`). Read it when a run misbehaved or a green looks wrong: triage, the observability output, and `chat`.
3
+ Tracks `cowork-harness 5.0.0` (baseline `desktop-4.23118.0`). Read it when a run misbehaved or a green looks wrong: triage, the observability output, and `chat`.
4
4
 
5
5
  ## Part III — Debug
6
6
 
@@ -28,6 +28,7 @@ Docker, no re-record.
28
28
  |---|---|---|
29
29
  | **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 are the questions view, as there is no gates view; the sub-agent tree is the dispatches view)? · `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 |
30
30
  | **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 · `replay --mutate` — perturbs a CAPPED SAMPLE of recorded JSON values (10/file, 50 total) and reports which perturbations NOTHING caught; the report names the sample size, so read it as a sample not a total (reporting only; never moves the verdict/exit code) · `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` / `skill --repeat N` — did it pass, or pass once? · `stats` — flaky or expensive over time |
31
+ | **A judge or grader call failed** | an assert reads `judge call FAILED (API 401, …)`, or `eval` stops at its credential check; often a stale key, such as an old one in `.env` | `cowork-harness doctor` — which credential the graders and the agent each use · the graders get `CLAUDE_CODE_OAUTH_TOKEN` alone when it is set, so a stale token fails even with a good API key · `COWORK_HARNESS_GRADER_AUTH=inherit` passes every key |
31
32
 
32
33
  A failed run also records `errorSource` (where the failure originated) and `stderrLogPath` (the captured
33
34
  agent stderr) — read those before re-running (`answer_channel_violation` means a run under `answer_channel: none`
@@ -141,7 +142,7 @@ decide which assertions from *Assertions: two orthogonal axes* in `assertions-gu
141
142
  `modelUsage[].costUSD`, which is what `trace --view usage` reports, so the two can differ),
142
143
  `usage` (`input_tokens`/`output_tokens`/`turns`), `toolDurations` (with `toolDurationsBasis`), `models`,
143
144
  `toolCalls` (every tool call in stream order: `name`, top-level `input` fields each capped at 10 KB, and
144
- `origin` `main`/`subagent`/`unknown` — what the object form of `tool_called`/`tool_not_called` reads), `toolErrors`,
145
+ `origin` `main`/`subagent`/`unknown` — what the object form of `tool_called`/`tool_not_called` reads; `denied: true` means the agent received a deny answer to that call's permission request, which a hook block never sets; the footer's `denied: N` counts those answers), `toolErrors`,
145
146
  `redundantToolCalls`, `modelUsage`, `thinking`, `skillActivity`, `subagents[]` (prompt/`dispatchModel`/
146
147
  `resolvedModel`/output/`attributedSkillId`, `outputTruncated`, `referencesRead`, `reasoning`/`reasoningElided`),
147
148
  `context` (tools/mcpServers/availableSkills), `tasks`,
@@ -184,6 +185,8 @@ decide which assertions from *Assertions: two orthogonal axes* in `assertions-gu
184
185
  UNAVAILABLE marker (`workspaceFilesRecorded: false` in JSON, and no phantom "removed" diff rows) and
185
186
  `inspect` prints `artifacts: UNAVAILABLE` (`artifactsRecorded: false`) instead of `artifacts (0):`.
186
187
 
188
+ To grade `assertions[]` by hand, see [docs/debugging.md → Reading a run's assertions by hand](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/debugging.md#reading-a-runs-assertions-by-hand).
189
+
187
190
  ### Debugging with `chat`
188
191
 
189
192
  `cowork-harness chat` opens an interactive multi-turn REPL against a live Cowork session. It is