cowork-harness 4.14.0 → 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.
- package/.claude/skills/cowork-harness/SKILL.md +9 -8
- package/.claude/skills/cowork-harness/references/assertion-catalog-agents-skills-budgets.md +2 -2
- package/.claude/skills/cowork-harness/references/assertion-catalog-gates-hooks-modifiers.md +9 -9
- package/.claude/skills/cowork-harness/references/assertion-catalog-outcome-files-tools.md +1 -1
- package/.claude/skills/cowork-harness/references/assertion-catalog.md +3 -4
- package/.claude/skills/cowork-harness/references/assertions-guide.md +6 -6
- package/.claude/skills/cowork-harness/references/authoring.md +8 -7
- package/.claude/skills/cowork-harness/references/ci-recipe.md +28 -36
- package/.claude/skills/cowork-harness/references/critique.md +1 -1
- package/.claude/skills/cowork-harness/references/debugging.md +5 -2
- package/.claude/skills/cowork-harness/references/eval.md +25 -14
- package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +21 -13
- package/.claude/skills/cowork-harness/references/gotchas.md +7 -9
- package/.claude/skills/cowork-harness/references/hillclimb-recipe.md +9 -8
- package/.claude/skills/cowork-harness/references/hillclimb.md +22 -20
- package/.claude/skills/cowork-harness/references/measurement.md +1 -1
- package/.claude/skills/cowork-harness/references/run-record-replay.md +15 -16
- package/.claude/skills/cowork-harness/references/scenario-schema.md +22 -18
- package/.claude/skills/cowork-harness/references/semantic-judging.md +11 -1
- package/.claude/skills/cowork-harness/references/task-recipes.md +13 -11
- package/.claude/skills/cowork-harness/scripts/assertion-keys.json +25 -2
- package/.claude/skills/cowork-harness/scripts/scenario.py +62 -37
- package/.env.example +2 -2
- package/CHANGELOG.md +402 -0
- package/CONTRIBUTING.md +7 -3
- package/DESIGN.md +2 -2
- package/README.md +6 -6
- package/RELEASING.md +30 -7
- package/SPEC.md +104 -66
- package/baselines/desktop-4.23118.0.json +2096 -0
- package/baselines/prompts/cowork-system-prompt-fingerprints.json +9 -0
- package/baselines/prompts/desktop-1.18286.0/system-prompt-append.md +2 -0
- package/dist/agent/session.js +3 -2
- package/dist/assert.js +53 -10
- package/dist/async-pool.js +30 -6
- package/dist/baseline.js +31 -17
- package/dist/cli.js +280 -118
- package/dist/critique/command.js +57 -23
- package/dist/critique/limitations.js +4 -4
- package/dist/decide/decider.js +6 -1
- package/dist/decide/external-channel.js +5 -0
- package/dist/decide/judge-call-error.js +56 -0
- package/dist/decide/llm-transport.js +208 -7
- package/dist/decide/pairwise-judge.js +20 -5
- package/dist/decide/semantic-judge.js +10 -1
- package/dist/effort-env.js +1 -1
- package/dist/eval/classify.js +18 -27
- package/dist/eval/command.js +123 -14
- package/dist/eval/headroom.js +64 -0
- package/dist/eval/job-runner.js +3 -1
- package/dist/eval/plan-history.js +27 -5
- package/dist/eval/plan.js +3 -2
- package/dist/eval/planner.js +16 -2
- package/dist/eval/report.js +204 -19
- package/dist/eval/runs.js +5 -1
- package/dist/eval/stopped.js +39 -0
- package/dist/eval/usage.js +18 -5
- package/dist/fixture/cli.js +3 -1
- package/dist/hillclimb/args.js +21 -0
- package/dist/hillclimb/budget.js +130 -0
- package/dist/hillclimb/check.js +35 -0
- package/dist/hillclimb/cli.js +92 -17
- package/dist/hillclimb/command.js +41 -7
- package/dist/hillclimb/cost.js +14 -9
- package/dist/hillclimb/flow.js +22 -7
- package/dist/hillclimb/gate-cmd.js +106 -0
- package/dist/hillclimb/gate.js +12 -1
- package/dist/hillclimb/job.js +13 -8
- package/dist/hillclimb/regrade.js +186 -47
- package/dist/hillclimb/rows.js +52 -13
- package/dist/hillclimb/run-command.js +90 -16
- package/dist/hillclimb/runner.js +400 -163
- package/dist/hillclimb/schema-check.js +11 -2
- package/dist/hillclimb/seed.js +370 -0
- package/dist/hillclimb/served-model.js +44 -20
- package/dist/hillclimb/snapshot.js +21 -0
- package/dist/hillclimb/trace.js +4 -4
- package/dist/hillclimb/usage.js +44 -5
- package/dist/host-inputs.js +428 -0
- package/dist/hostloop/cowork-handler.js +1 -2
- package/dist/loop-decision.js +1 -1
- package/dist/prompt.js +2 -2
- package/dist/refs/cli.js +5 -2
- package/dist/removed-env.js +42 -0
- package/dist/run/agent-infra-end.js +29 -0
- package/dist/run/analyze-artifact-runtime.js +19 -4
- package/dist/run/analyze-skill.js +16 -4
- package/dist/run/artifacts.js +1 -2
- package/dist/run/cassette.js +763 -771
- package/dist/run/chat-result.js +6 -14
- package/dist/run/chat.js +42 -20
- package/dist/run/command-globals.js +2 -0
- package/dist/run/doctor.js +113 -60
- package/dist/run/envelope.js +11 -4
- package/dist/run/execute.js +169 -102
- package/dist/run/exit.js +156 -0
- package/dist/run/grader-stop.js +24 -0
- package/dist/run/hook-events.js +7 -18
- package/dist/run/host-action-runners.js +2 -2
- package/dist/run/judge-check.js +56 -0
- package/dist/run/lint-load.js +29 -2
- package/dist/run/model-provenance.js +28 -4
- package/dist/run/multi-leg-cassette.js +1 -0
- package/dist/run/multi-leg.js +12 -3
- package/dist/run/pairwise-prepass.js +31 -3
- package/dist/run/regrade.js +31 -4
- package/dist/run/renderer.js +3 -1
- package/dist/run/run-index.js +1 -1
- package/dist/run/run.js +40 -2
- package/dist/run/runs-gc.js +42 -29
- package/dist/run/scenario-tool.js +11 -5
- package/dist/run/skill-flag-surface.js +26 -2
- package/dist/run/skill-hash.js +60 -84
- package/dist/run/turn-cap.js +30 -0
- package/dist/run/turn-layout.js +5 -2
- package/dist/run/verdict.js +14 -20
- package/dist/run/verify-context.js +2 -0
- package/dist/runtime/agent-image.js +1 -1
- package/dist/runtime/argv.js +43 -21
- package/dist/runtime/container.js +3 -1
- package/dist/runtime/host-env.js +31 -8
- package/dist/runtime/hostloop-prompt.js +1 -2
- package/dist/runtime/hostloop-stage.js +13 -1
- package/dist/runtime/hostloop.js +68 -76
- package/dist/runtime/image-capabilities.js +1 -1
- package/dist/runtime/lima.js +12 -12
- package/dist/runtime/microvm.js +8 -6
- package/dist/runtime/passthrough.js +68 -0
- package/dist/runtime/protocol.js +99 -161
- package/dist/runtime/resource-sampler.js +1 -1
- package/dist/runtime/session-argv-fields.js +70 -0
- package/dist/runtime/stage.js +15 -6
- package/dist/runtime/vm-baseline-arg.js +1 -1
- package/dist/scan.js +29 -9
- package/dist/search-corpus/cli.js +6 -3
- package/dist/session.js +243 -81
- package/dist/staging/resolve.js +1 -1
- package/dist/sync/cowork-sync.js +140 -19
- package/dist/sync/observed-spawn-env.js +2 -1
- package/dist/sync/spawn-env-post-writes.js +5 -3
- package/dist/sync/spawn-env-registry.js +16 -10
- package/dist/types.js +47 -11
- package/docs/README.md +2 -2
- package/docs/boundary.md +2 -2
- package/docs/cassette.md +55 -109
- package/docs/ci.md +17 -17
- package/docs/cli.md +69 -46
- package/docs/companion-skill.md +2 -2
- package/docs/critique.md +8 -7
- package/docs/debugging.md +24 -15
- package/docs/discovery.md +9 -6
- package/docs/eval.md +71 -22
- package/docs/fidelity-gaps.md +14 -16
- package/docs/gotchas.md +1 -1
- package/docs/hillclimb.md +104 -21
- package/docs/invariants.md +2 -1
- package/docs/maintenance.md +3 -1
- package/docs/plugin-root.md +1 -2
- package/docs/scenario.md +34 -37
- package/docs/session.md +122 -18
- package/docs/subagents.md +3 -7
- package/examples/replays/README.md +6 -6
- package/examples/replays/example-multiselect-gate.cassette.json +82 -51
- package/examples/replays/example-pdf-skill.cassette.json +83 -83
- package/examples/replays/hostloop-computer-links.cassette.json +59 -59
- package/examples/sessions/default.yaml +2 -2
- package/llms.txt +1 -1
- package/package.json +6 -1
- package/python/README.md +1 -1
- package/python/cowork_harness.py +2 -2
- package/python/test_run_dir_layout.py +10 -0
- package/python/test_scenario_lint.py +71 -10
- package/schema/cassette.v19.json +703 -0
- package/schema/critique-report.json +2 -2
- package/schema/regrade.json +81 -7
- package/schema/run-result.json +136 -7
- package/schema/scenario.schema.json +10 -10
- package/schema/session.schema.json +38 -0
- package/scripts/check-versions.ts +12 -5
- package/scripts/ci/vitest-file-timing.ts +47 -0
- package/scripts/classify-manifest-probe.ts +73 -0
- package/scripts/gen-schema.ts +3 -0
- package/baselines/desktop-1.11847.5.json +0 -80
- package/baselines/desktop-1.12603.1.json +0 -142
- package/baselines/desktop-1.13576.1.json +0 -222
- package/baselines/prompts/desktop-1.12603.1/host-loop-append.md +0 -8
- package/baselines/prompts/desktop-1.13576.1/host-loop-append.md +0 -11
- package/dist/run/migrate-run-dir.js +0 -838
- package/schema/cassette.v10.json +0 -312
- package/schema/cassette.v11.json +0 -313
- 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:
|
|
7
|
-
tracks-harness: cowork-harness
|
|
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
|
|
30
|
-
> `desktop-4.
|
|
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 **≥
|
|
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)
|
|
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 ·
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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).
|
|
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
|
|
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
|
|
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`;
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
61
|
-
|
|
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
|
|
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
|
|
373
|
-
|
|
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
|
|
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
|
|
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@
|
|
9
|
+
- uses: yaniv-golan/cowork-harness@v5
|
|
10
10
|
with:
|
|
11
11
|
command: replay
|
|
12
12
|
path: cassettes/
|
|
13
|
-
version: "^
|
|
13
|
+
version: "^5" # hold the major; see below
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
**These recipes pin `version: "^
|
|
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. `^
|
|
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: "
|
|
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
|
|
@@ -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@
|
|
60
|
+
- uses: yaniv-golan/cowork-harness@v5
|
|
61
61
|
with:
|
|
62
62
|
command: run
|
|
63
63
|
path: scenarios/
|
|
64
|
-
version: "^
|
|
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@^
|
|
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@
|
|
160
|
+
- uses: yaniv-golan/cowork-harness@v5
|
|
161
161
|
with:
|
|
162
162
|
command: lint
|
|
163
163
|
path: scenarios/
|
|
164
|
-
version: "^
|
|
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
|
|
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: "^
|
|
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: "
|
|
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` / `
|
|
190
|
-
`
|
|
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
|
|
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.
|
|
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.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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@
|
|
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@^
|
|
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@^
|
|
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
|
|
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
|
|
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
|
|
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
|