cowork-harness 0.29.0 → 0.31.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 +37 -17
- package/.claude/skills/cowork-harness/references/ci-recipe.md +23 -11
- package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +1 -1
- package/.claude/skills/cowork-harness/references/scenario-schema.md +31 -10
- package/.claude/skills/cowork-harness/references/task-recipes.md +3 -3
- package/.claude/skills/cowork-harness/scripts/assertion-keys.json +7 -1
- package/.claude/skills/cowork-harness/scripts/scenario.py +333 -10
- package/CHANGELOG.md +393 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +22 -34
- package/RELEASING.md +27 -0
- package/SPEC.md +44 -19
- package/baselines/desktop-1.18286.2.json +1 -0
- package/baselines/desktop-1.19367.0.json +1 -0
- package/baselines/desktop-1.20186.0.json +1 -0
- package/baselines/desktop-1.20186.1.json +380 -0
- package/baselines/prompts/cowork-system-prompt-fingerprints.json +4 -0
- package/baselines/prompts/desktop-1.18286.2/subagent-append-hl.md +16 -0
- package/dist/agent/session.js +70 -9
- package/dist/agent/timeline.js +6 -1
- package/dist/assert.js +170 -5
- package/dist/baseline.js +55 -13
- package/dist/cli.js +299 -16
- package/dist/decide/decider.js +1 -1
- package/dist/hostloop/canusetool-gate.js +64 -0
- package/dist/hostloop/pretooluse-path-hook.js +64 -23
- package/dist/prompt.js +33 -10
- package/dist/run/analyze-skill.js +619 -0
- package/dist/run/cassette.js +298 -114
- package/dist/run/chat-result.js +3 -0
- package/dist/run/chat.js +30 -11
- package/dist/run/doctor.js +12 -3
- package/dist/run/envelope.js +8 -4
- package/dist/run/execute.js +904 -681
- package/dist/run/latest-run.js +130 -0
- package/dist/run/probe-dispatch.js +81 -0
- package/dist/run/renderer.js +1 -1
- package/dist/run/run.js +277 -44
- package/dist/run/scenario-tool.js +17 -0
- package/dist/run/subagent-reasoning.js +151 -0
- package/dist/run/trace-view.js +27 -7
- package/dist/run/verdict.js +39 -3
- package/dist/runtime/argv.js +6 -1
- package/dist/runtime/container.js +7 -2
- package/dist/runtime/hostloop-prompt.js +5 -5
- package/dist/runtime/hostloop.js +51 -13
- package/dist/runtime/microvm.js +3 -1
- package/dist/runtime/protocol.js +17 -1
- package/dist/session.js +42 -0
- package/dist/sync/cowork-sync.js +296 -5
- package/dist/types.js +67 -2
- package/dist/vm-paths.js +13 -0
- package/docker/Dockerfile.agent +1 -1
- package/docs/README.md +2 -1
- package/docs/boundary.md +22 -3
- package/docs/cassette.md +56 -17
- package/docs/cowork-spawn-contract-1.12603.1.md +3 -2
- package/docs/fidelity-gaps.md +85 -0
- package/docs/gotchas.md +5 -7
- package/docs/invariants.md +4 -2
- package/docs/maintenance.md +20 -1
- package/docs/plugin-root.md +27 -8
- package/docs/scenario.md +53 -6
- package/docs/session.md +37 -0
- package/docs/subagents.md +428 -0
- package/examples/replays/README.md +1 -1
- package/examples/replays/example-multiselect-gate.cassette.json +1 -1
- package/examples/replays/example-pdf-skill.cassette.json +1 -1
- package/examples/replays/hostloop-computer-links.cassette.json +1 -1
- package/package.json +3 -1
- package/schema/cassette.v10.json +4 -0
- package/schema/protocol.v1.json +1 -0
- package/schema/run-result.json +187 -9
- package/schema/scenario.schema.json +101 -2
- package/schema/session.schema.json +20 -0
- package/schema/verify-cassettes.json +11 -6
- package/scripts/check-versions.ts +8 -15
- package/dist/canary/grants.js +0 -37
- package/dist/critique/evaluator.js +0 -321
- package/dist/critique/evidence.js +0 -175
- package/dist/critique/package-evidence.js +0 -198
- package/docs/assets/banner.png +0 -0
- package/fixtures/subagent-grants.json +0 -5
- package/schema/cassette.v2.json +0 -19
- package/schema/cassette.v3.json +0 -30
- package/schema/cassette.v5.json +0 -107
- package/schema/cassette.v6.json +0 -131
- package/schema/cassette.v7.json +0 -156
- package/schema/cassette.v8.json +0 -166
- package/scripts/boot-rootfs-vz.ts +0 -99
|
@@ -3,8 +3,8 @@ name: cowork-harness
|
|
|
3
3
|
description: Test or debug a Claude Code skill/plugin under Claude Cowork's runtime — sandboxed agent, default-deny egress, the can_use_tool permission/question protocol — using the cowork-harness CLI. Use when validating or regression-testing a skill, authoring or debugging a scenario YAML (prompt + scripted answers + assert:), choosing a fidelity tier, scripting AskUserQuestion / tool-permission answers, or asserting artifacts, egress, or sub-agent dispatch. Especially when a harness run no-ops an assertion, fails on an unanswered gate, false-greens, a steered answer never reaches the model, or a web_fetch is unexpectedly denied or gated. NOT for generic unit testing (pytest/vitest of your own scripts) or non-Cowork CI. Covers the skill / run / chat / record / replay / trace / decide / assertions / scaffold commands and the session-vs-scenario split.
|
|
4
4
|
metadata:
|
|
5
5
|
author: cowork-harness
|
|
6
|
-
version: 0.
|
|
7
|
-
tracks-harness: cowork-harness 0.
|
|
6
|
+
version: 0.31.0
|
|
7
|
+
tracks-harness: cowork-harness 0.31.0 (baseline desktop-1.20186.1)
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# cowork-harness
|
|
@@ -22,8 +22,8 @@ flagged with a loud `::warning::`, not silent — auto-answer a gate, observe an
|
|
|
22
22
|
allowlist). This skill exists mostly to keep you out of those traps — the Gotchas section below is
|
|
23
23
|
the highest-value part. Read it.
|
|
24
24
|
|
|
25
|
-
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.
|
|
26
|
-
> `desktop-1.20186.
|
|
25
|
+
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.31.0` (baseline
|
|
26
|
+
> `desktop-1.20186.1`). If your checkout is newer, prefer the live `--help` and — in a repo checkout —
|
|
27
27
|
> `SPEC.md` / `docs/*.md` over this snapshot, and re-run the bundled linter.
|
|
28
28
|
|
|
29
29
|
## Preflight — make sure the harness can actually run
|
|
@@ -38,7 +38,7 @@ cowork-harness skill ./my-skill "do X" # run the skill once against the sta
|
|
|
38
38
|
Before the first command, confirm the CLI is reachable and **fail loud** (never fake a pass) when a tier's dependencies are missing:
|
|
39
39
|
|
|
40
40
|
- **One-shot check.** Run `cowork-harness doctor [--tier <tier>]` first — a read-only prerequisite check that inspects Docker, the staged agent, the token, and the baseline in one pass. The bullets below explain each thing it checks (and how to fix it).
|
|
41
|
-
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.
|
|
41
|
+
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.31.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.31.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.31.0"`. **Pin `@>=0.31.0`, never `@latest`** — `@latest` can silently fetch an older CLI and the new commands fail as "unknown command", whereas the floor **fails loud** if no compatible version is published. (≥ 0.31.0 is what gates the commands/assertions this skill teaches: `assertions --list`, `scaffold <run-id>`, `trace --view dispatches`, `artifact_json` incl. the `in:` operator (passes when the resolved value deep-equals one of the listed members — value ∈ your list, not the reverse), `verify-cassettes`, batch `record <dir>`/`--rerecord-stale`, `record --concurrency <N>`, record-time redaction, multiSelect/`answer:`, `verify-run` answer-coverage, `record --max-artifact-bytes`, live record-time deciders, `verify-cassettes --allow-domain`/`--allow-email`/`--allow-path`/`--allow-patterns-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0; `--allow-patterns-file <path>` is a FILE of patterns, one regex per line — not a path to allow, unlike `--allow <regex>`), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field, `computer_links_resolve` (new in 0.22.0), `semantic_matches` (an LLM judge grades a fixed `rubric` of claims against the run's answer — **live-only**, so it is evidence-unavailable / skipped-loud on replay, never a vacuous pass; new in 0.27.0), **glob-matched** `tool_called`/`tool_not_called`/`subagent_tool_used`/`subagent_tool_absent` (a pattern like `mcp__workspace__*` matches any tool in the family; exact names match exactly; new in 0.28.0), the five path-gate assertion keys `no_vm_path_file_op`/`vm_path_denied`/`path_denied`/`no_path_denied`/`subagent_file_write`, the session-level `agent_env` knob, and resolved sub-agent identity on dispatch records (`resolvedAgentType`/`resolvedModel`; new in 0.30.0), the `lint-skill` (static host-loop footgun + `subagent_type` resolution linter) / `analyze-skill` (advisory `/sessions`-path static scan, `--strict`, `analyze-skill: ignore` marker) / `probe-dispatch` (single-dispatch mechanics probe) commands, `status --latest-for` (resolve a scenario's newest run dir by run time, not directory mtime), the `subagent_dispatch_healthy` composite assertion, and the persisted `result.json` fields `verdict`, `subagents[].referencesRead`/`subagents[].reasoning`, plus the `toolCounts`/`toolErrors`/`toolDurations` shape distinction (all new in 0.31.0).)
|
|
42
42
|
- **Agent binary (sandboxed live tiers — `container`/`microvm`/`hostloop`/`cowork`).** The staged Claude Code agent is **bind-mounted** from a local Claude Desktop install, or point `COWORK_AGENT_BINARY` at a `claude-code-vm/<ver>/claude` ELF. Nothing is bundled. `protocol` (L0) and `replay` need no staged agent; for the sandboxed tiers, no agent → no run; report that, don't skip silently.
|
|
43
43
|
- **Docker / Lima.** Only `--fidelity protocol` (L0) runs without them. `container` / `microvm` / `hostloop` / `cowork` need Docker (Lima for L2). If they're absent, drop to `--fidelity protocol` and **say so** — a green that never exercised the sandbox is not a sandbox pass.
|
|
44
44
|
- **Auth.** `CLAUDE_CODE_OAUTH_TOKEN` (preferred) or `ANTHROPIC_API_KEY`, via env or `.env`. Minting an OAuth token needs the **`claude` CLI** (`npm i -g @anthropic-ai/claude-code`, then `claude setup-token`).
|
|
@@ -63,13 +63,14 @@ reproducible regression (Part II), and **debug** a run that misbehaved or greene
|
|
|
63
63
|
the path, `trace <run-id>` finds it). **Localize the failure post-hoc** from that evidence:
|
|
64
64
|
`cowork-harness trace <run-dir>`'s views + the emitted `result.json` to see what the run actually did,
|
|
65
65
|
then `verify-run` to re-check a suspect assertion — all token-free, no Docker, no re-record. This is
|
|
66
|
-
the loop 0.
|
|
66
|
+
the loop 0.31.0's observability is built for; the *Triage* and *Inspecting a run's observability
|
|
67
67
|
output* sections in **Part III — Debug** are the detail (the fuller human-facing map lives in
|
|
68
68
|
`docs/debugging.md` — repo-only, not shipped with the installed skill).
|
|
69
69
|
- **Multi-turn / interactive reproduction** → `cowork-harness chat` (interactive; gates answered at the
|
|
70
70
|
TTY, **not** an asserted test — see *Debugging with `chat`* in **Part III — Debug**).
|
|
71
71
|
|
|
72
72
|
Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
|
|
73
|
+
lint-skill · analyze-skill · probe-dispatch ·
|
|
73
74
|
verify-run · trace · inspect · diff · stats · decide · gates · answer · scaffold · assertions --list · sync ·
|
|
74
75
|
list · boundary-check · status · vm <init|status|delete|prune> · doctor · init-redact`. Always check `cowork-harness <cmd> --help`.
|
|
75
76
|
|
|
@@ -292,7 +293,9 @@ scenario's gates or their labels: `--keep` ONE run, then `cowork-harness trace <
|
|
|
292
293
|
your `answers:` against that kept run, then record once. **But the kept run is a snapshot:** if you change the
|
|
293
294
|
skill's gate phrasing afterward, re-`--keep` — verify-run's answer-coverage *refuses* (exit 2, "predates the
|
|
294
295
|
current skill") rather than vouch against stale labels, but the trace/inspect path can't warn you, so re-keep
|
|
295
|
-
deliberately. (
|
|
296
|
+
deliberately. (Same fail-closed family: corrupt gate evidence — unparseable `events.jsonl` lines, or fewer
|
|
297
|
+
gates than `trace.json` recorded questions — and a structurally invalid `result.json` also refuse rather
|
|
298
|
+
than certify.) (A token-free probe of "which gates fire" isn't possible — gates are model-decided per run.)
|
|
296
299
|
|
|
297
300
|
Run artifacts are written to `~/.cowork-harness/runs/…` by default — **outside any working tree**, so a run
|
|
298
301
|
launched from a repo root never drops sensitive skill inputs/outputs into it. Pass `--run-dir <path>` (or set
|
|
@@ -419,9 +422,10 @@ decide which assertions from *Assertions: two orthogonal axes* are worth adding)
|
|
|
419
422
|
the current list rather than relying on a fixed enumeration here.
|
|
420
423
|
- **`cowork-harness stats [--metric <m>]`** — aggregate across the run index: `cost`, `duration`,
|
|
421
424
|
`tokens`, `cache-tokens`, `model-cost`, `turns`, `pass-rate`.
|
|
422
|
-
- **`result.json` carries the raw fields** the assertions read: `toolDurations`, `models`, `toolErrors`,
|
|
423
|
-
`redundantToolCalls`, `modelUsage`, `thinking`, `skillActivity`, `subagents[]` (prompt
|
|
424
|
-
`attributedSkillId`, `outputTruncated`
|
|
425
|
+
- **`result.json` carries the raw fields** the assertions read: `verdict`, `toolDurations`, `models`, `toolErrors`,
|
|
426
|
+
`redundantToolCalls`, `modelUsage`, `thinking`, `skillActivity`, `subagents[]` (prompt/`dispatchModel`/
|
|
427
|
+
`resolvedModel`/output/`attributedSkillId`, `outputTruncated`, `referencesRead`, `reasoning`/`reasoningElided`),
|
|
428
|
+
`context` (tools/mcpServers/availableSkills), `tasks`,
|
|
425
429
|
`workspaceFiles`, `presentedFiles`, `hookEvents`, `mcpErrors`, `contextEvents`, `resources`
|
|
426
430
|
(`probeFailures` distinguishes a failed sample from a tier that was never sampleable). Provenance/
|
|
427
431
|
evidence-health fields: `command` (`run`/`skill`/`record`/`chat`/`replay` — finer than `mode`),
|
|
@@ -429,8 +433,11 @@ decide which assertions from *Assertions: two orthogonal axes* are worth adding)
|
|
|
429
433
|
`bySource` histogram), `evidenceErrors` (dropped/malformed telemetry lines per stream, incl.
|
|
430
434
|
`egressParse`), `fingerprint.frozen` (replay only — marks the shown staleness fingerprint as the
|
|
431
435
|
cassette's record-time value, not a fresh recompute), and `assertTextTruncated` (companion to
|
|
432
|
-
`outputTruncated` on a matched tool result).
|
|
433
|
-
|
|
436
|
+
`outputTruncated` on a matched tool result). Three separately-shaped rollups, easy to conflate in a
|
|
437
|
+
`jq` recipe: `toolCounts` is a flat `{tool: number}` call-count map, `toolErrors` is
|
|
438
|
+
`{tool: {calls, errors}}`, and `toolDurations` is `{tool: {calls, totalMs, maxMs}}`. (Full per-field
|
|
439
|
+
semantics: the README's "Observability fields" section — repo-only; `schema/run-result.json` is the
|
|
440
|
+
machine source.)
|
|
434
441
|
- **Opaque failure?** A failed run also records **`errorSource`** (where the failure originated) and
|
|
435
442
|
**`stderrLogPath`** (the captured agent stderr) — read those and `trace <run-dir>` *before* re-running;
|
|
436
443
|
a re-record rarely tells you more than the captured stderr already does. Also check
|
|
@@ -439,7 +446,7 @@ decide which assertions from *Assertions: two orthogonal axes* are worth adding)
|
|
|
439
446
|
than debugging; `"transport"`/`"agent"` means something actually broke, worth localizing before
|
|
440
447
|
re-running.
|
|
441
448
|
- **Attributing cost to sub-agent work.** `subagents[]` gives the dispatch tree — each sub-agent's
|
|
442
|
-
`
|
|
449
|
+
`dispatchModel`/`resolvedModel`, `toolsUsed`, `prompt`/`output`, and `attributedSkillId` — but **not** its own token/cost;
|
|
443
450
|
aggregate cost is per-**model** in `modelUsage` (and `trace --view usage`), not per-sub-agent. So a
|
|
444
451
|
cost spike from fan-out reads as `trace --view dispatches` (how many, which agent) against that model's
|
|
445
452
|
per-model usage — the harness doesn't line-item each sub-agent's tokens.
|
|
@@ -514,8 +521,20 @@ repeats the assertion/replay-relevant ones alongside the schema (a scoped subset
|
|
|
514
521
|
to prove an attempt; it proves the tool actually ran.
|
|
515
522
|
|
|
516
523
|
5. **`subagent_declared_but_unused` fires on declared-but-didn't-use-THAT-tool**, even if the
|
|
517
|
-
sub-agent used other tools.
|
|
518
|
-
|
|
524
|
+
sub-agent used other tools. `subagent_dispatched` / `subagent_output_contains` match on dispatch
|
|
525
|
+
type (`dispatchAgentType`), the binary-*resolved* type (`resolvedAgentType`), *or* the dispatch
|
|
526
|
+
**description** — so a type-less dispatch that resolved to e.g. `general-purpose` is still
|
|
527
|
+
selectable, by either the resolved type or the description. A `Task` dispatch that carries NO
|
|
528
|
+
`subagent_type` at all falls back to the built-in `general-purpose` agent with a **wildcard tool
|
|
529
|
+
surface** (`tools:["*"]`, including workspace bash) — faithful production behavior, and it fires
|
|
530
|
+
routinely. The harness warns loudly on this fallback and records `subagents[].dispatchTypeOmitted`;
|
|
531
|
+
an *explicit* `subagent_type: "general-purpose"` is a deliberate author choice and does not warn.
|
|
532
|
+
Implication: `subagent_tool_absent` on a type-less dispatch is weaker evidence (wildcard surface) —
|
|
533
|
+
pin `subagent_type` explicitly when you need a tight tool-absence guarantee.
|
|
534
|
+
|
|
535
|
+
**Cross-tier "no shell" caveat.** On `hostloop`, native `Bash` calls route through the
|
|
536
|
+
`mcp__workspace__bash` alias, so a "sub-agent used no shell" check must glob **both** `Bash` and
|
|
537
|
+
`mcp__workspace__*` to hold across every tier.
|
|
519
538
|
|
|
520
539
|
6. **`dispatch_count_max` is an author-chosen budget, not a production cap.** It's a post-hoc count
|
|
521
540
|
assertion: passing means "happened to dispatch ≤N this run," nothing more. Cowork imposes **no**
|
|
@@ -660,8 +679,9 @@ assertion/replay-relevant ones).
|
|
|
660
679
|
- `references/fidelity-and-answers.md` — fidelity tiers, answer paths, the determinism contract.
|
|
661
680
|
- `references/ci-recipe.md` — the packaged GitHub Action, replay-vs-live lane split, and the four-stage
|
|
662
681
|
GitHub Actions pipeline.
|
|
663
|
-
- `scripts/scenario.py` — `scaffold` a valid scenario skeleton
|
|
664
|
-
no-silent-false-green invariants (both usable as CI steps)
|
|
682
|
+
- `scripts/scenario.py` — `scaffold` a valid scenario skeleton, `lint` scenarios for the
|
|
683
|
+
no-silent-false-green invariants (both usable as CI steps), and `resolve-agent-types <plugin-dir>`
|
|
684
|
+
(validates a pinned `subagent_type` against the plugin's own `plugin.json` + `agents/*.md`).
|
|
665
685
|
- Checking a background run's status without `ps aux` — covered in *Checking whether a background run is
|
|
666
686
|
alive* (Part II) above; the fuller recipe is in `docs/run-status.md` (repo-only, not shipped with the
|
|
667
687
|
installed skill).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CI recipe — replay vs live lanes
|
|
2
2
|
|
|
3
|
-
Self-contained reference. Tracks `cowork-harness 0.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 0.31.0` (baseline `desktop-1.20186.1`).
|
|
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):
|
|
@@ -54,7 +54,7 @@ sha256-*checked* but not hard-blocking on mismatch — it's advisory for an inte
|
|
|
54
54
|
GitHub-hosted runners, no token/Docker/agent:
|
|
55
55
|
|
|
56
56
|
```yaml
|
|
57
|
-
- run: npm i -g "cowork-harness@>=0.
|
|
57
|
+
- run: npm i -g "cowork-harness@>=0.31.0"
|
|
58
58
|
- run: cowork-harness lint scenarios/*.yaml # no silent false-greens
|
|
59
59
|
- run: cowork-harness verify-cassettes cassettes/ # privacy + staleness
|
|
60
60
|
- run: cowork-harness replay cassettes/ # token-free content/structure
|
|
@@ -139,13 +139,19 @@ dollar figures). In a skill repo these cassettes get **committed**. So:
|
|
|
139
139
|
universal net (container-tier recordings can trip it too).
|
|
140
140
|
- **Always-on scan gate** — `verify-cassettes` flags email / currency / bare-domain / local-path /
|
|
141
141
|
machine-inventory matches it finds in the committed cassettes and **exits non-zero**, so "no leak" is
|
|
142
|
-
a gate, not discipline.
|
|
142
|
+
a gate, not discipline. Non-zero is not one thing, though: exit `1` means verification RAN and found a
|
|
143
|
+
real finding (a PII match, a genuine staleness drift, or scenario-prompt drift); exit `3` means
|
|
144
|
+
verification could NOT complete (an `unverifiable-*`-class staleness finding, a cassette written by a
|
|
145
|
+
newer harness than this one understands, or a malformed/unreadable cassette). A plain `|| true` or `[
|
|
146
|
+
$? -ne 0 ]` tripwire treats both the same — if you need to tell "the gate caught something" apart from
|
|
147
|
+
"the gate couldn't run", branch on the exit code (or parse `--output-format json`'s per-file
|
|
148
|
+
`findings`/`staleness` vs `unverifiable`/`version`/`error` buckets).
|
|
143
149
|
Suppress synthetic / public reference names (NVCA, Cooley GO, …) with `--allow <regex>`. (Multi-word
|
|
144
150
|
proper names are NOT a default class — too noisy to gate on; add a pattern via config if your corpus
|
|
145
151
|
needs it.)
|
|
146
152
|
|
|
147
153
|
```bash
|
|
148
|
-
cowork-harness verify-cassettes cassettes/ # privacy scan + staleness
|
|
154
|
+
cowork-harness verify-cassettes cassettes/ # privacy scan + staleness — exit 1 = verified & failed, exit 3 = could not verify
|
|
149
155
|
cowork-harness verify-cassettes cassettes/ --allow 'NVCA|Cooley GO|Acme'
|
|
150
156
|
cowork-harness verify-cassettes cassettes/ --skip-privacy # staleness only (skip the privacy scan); both run by default
|
|
151
157
|
```
|
|
@@ -188,7 +194,7 @@ jobs:
|
|
|
188
194
|
with: { node-version: '20' }
|
|
189
195
|
- uses: actions/setup-python@v5
|
|
190
196
|
with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
|
|
191
|
-
- run: npm i -g "cowork-harness@>=0.
|
|
197
|
+
- run: npm i -g "cowork-harness@>=0.31.0"
|
|
192
198
|
- run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
|
|
193
199
|
- run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
|
|
194
200
|
- run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
|
|
@@ -217,7 +223,7 @@ jobs:
|
|
|
217
223
|
echo "live=true" >> "$GITHUB_OUTPUT"
|
|
218
224
|
fi
|
|
219
225
|
- if: steps.guard.outputs.live == 'true'
|
|
220
|
-
run: npm i -g "cowork-harness@>=0.
|
|
226
|
+
run: npm i -g "cowork-harness@>=0.31.0"
|
|
221
227
|
- if: steps.guard.outputs.live == 'true'
|
|
222
228
|
run: cowork-harness run scenarios/ --output-format json
|
|
223
229
|
env:
|
|
@@ -245,9 +251,12 @@ fails or a run errors, so a plain `cowork-harness run scenarios/` is already CI-
|
|
|
245
251
|
JSON.
|
|
246
252
|
|
|
247
253
|
`verify-cassettes` emits its **own** envelope (`{command, ok, coverage, results[]}` with per-file
|
|
248
|
-
`findings`/`staleness`/`notes`/`version`/`error`), published as
|
|
249
|
-
|
|
250
|
-
the
|
|
254
|
+
`findings`/`staleness`/`unverifiable`/`notes`/`version`/`error`), published as
|
|
255
|
+
`schema/verify-cassettes.json` in the npm package. `ok:false` doesn't say *why* — read the buckets, or
|
|
256
|
+
the exit code (`1` = `findings`/`staleness`/`scenarioDrift` populated, a real problem verified & found;
|
|
257
|
+
`3` = `unverifiable`/`version`/`error` populated, verification could not complete; a real finding wins
|
|
258
|
+
`1` if both are present). Both envelope schemas are covered 1.0 contract surfaces (SPEC §12) — parse the
|
|
259
|
+
JSON, not the human-readable text (which is explicitly NOT stable).
|
|
251
260
|
|
|
252
261
|
A run writes to `~/.cowork-harness/runs/<name>/<sessionId>/` by default — outside any working tree. In CI,
|
|
253
262
|
set `COWORK_HARNESS_RUNS_DIR` (or pass `--run-dir`) to a workspace-relative path (e.g. `runs`) so an
|
|
@@ -280,8 +289,11 @@ does **not** imply the recording is still valid. Each replay result carries `sta
|
|
|
280
289
|
|
|
281
290
|
(A pre-`effectiveFidelity` cassette with an **explicit** tier is statically knowable — it passes the tier
|
|
282
291
|
check with a non-failing informational note in the `verify-cassettes` envelope's per-file `notes[]`, a
|
|
283
|
-
`·`-prefixed row in text output. On `verify-cassettes` every staleness *finding* above
|
|
284
|
-
|
|
292
|
+
`·`-prefixed row in text output. On `verify-cassettes` every staleness *finding* above still fails the
|
|
293
|
+
gate (`ok:false`) — but it's no longer class-blind on the EXIT CODE: a `baseline`/`skill`/`shared-root`/
|
|
294
|
+
`format`/`resolved-tier` class lands in the envelope's `staleness[]` (verified & failed — exit `1`),
|
|
295
|
+
while an `unverifiable-*` class lands in `unverifiable[]` (could not verify — exit `3`). Notes never
|
|
296
|
+
fail it either way.)
|
|
285
297
|
|
|
286
298
|
To gate in CI, pick the severity you want:
|
|
287
299
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Fidelity tiers & answer paths
|
|
2
2
|
|
|
3
|
-
Self-contained reference. Tracks `cowork-harness 0.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 0.31.0` (baseline `desktop-1.20186.1`).
|
|
4
4
|
|
|
5
5
|
## Fidelity tiers (`fidelity:` in the scenario)
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Scenario & session schema, assertion catalog, web_fetch, full gotchas
|
|
2
2
|
|
|
3
|
-
Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.
|
|
4
|
-
(baseline `desktop-1.20186.
|
|
3
|
+
Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.31.0`
|
|
4
|
+
(baseline `desktop-1.20186.1`). If your checkout is newer, prefer the live `docs/scenario.md`,
|
|
5
5
|
`docs/session.md`, and `SPEC.md`.
|
|
6
6
|
|
|
7
7
|
**Minimal scenario** — `prompt` is the only required field:
|
|
@@ -105,6 +105,12 @@ agent_max_turns: 500 # optional turn ceiling -> agent --max-turns;
|
|
|
105
105
|
permission_mode: default # default | acceptEdits | plan | bypassPermissions
|
|
106
106
|
permission_parity: cowork # cowork (unscripted tool calls allowed) | strict (deny unscripted)
|
|
107
107
|
|
|
108
|
+
# sub-agent / tool-search env knob (tier-uniform; maps to agent env vars)
|
|
109
|
+
agent_env:
|
|
110
|
+
subagent_model: claude-opus-4-8 # -> CLAUDE_CODE_SUBAGENT_MODEL
|
|
111
|
+
tool_search: auto # auto | off -> ENABLE_TOOL_SEARCH; omit = binary default (ToolSearch ON)
|
|
112
|
+
disable_experimental_betas: false # true -> CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 (also disables ToolSearch)
|
|
113
|
+
|
|
108
114
|
# fenced debug escape hatch (NOT reachable via Cowork's UI)
|
|
109
115
|
debug:
|
|
110
116
|
max_thinking_tokens: 50000 # overrides --max-thinking-tokens directly; bypasses extended_thinking's
|
|
@@ -254,9 +260,12 @@ same set live from the schema.
|
|
|
254
260
|
| `tool_result_not_contains: <str>` | no tool result includes the literal string (content / replay-checkable; fails loud when tool results are absent) |
|
|
255
261
|
| `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) |
|
|
256
262
|
| `subagent_tool_absent: <glob>` | no sub-agent used a tool matching this glob (same rejection) |
|
|
257
|
-
| `
|
|
263
|
+
| `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** |
|
|
264
|
+
| `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 |
|
|
265
|
+
| `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" |
|
|
266
|
+
| `subagent_dispatched: <regex>` | a sub-agent whose `dispatchAgentType`, binary-*resolved* `resolvedAgentType`, **or dispatch description** matches |
|
|
258
267
|
| `subagent_declared_but_unused: <Tool>` | a sub-agent declared the tool but never used **that** tool (even if it used others) |
|
|
259
|
-
| `subagent_output_contains: {match?, contains}` | a dispatched sub-agent's own output contains the substring `contains` — `match` (optional regex over `
|
|
268
|
+
| `subagent_output_contains: {match?, contains}` | a dispatched sub-agent's own output contains the substring `contains` — `match` (optional regex over `dispatchAgentType`/`resolvedAgentType`/`description`) narrows to specific dispatch(es); omitted, checks whether ANY dispatch's output contains it (existence check, not "all"); a miss against an output that was **truncated at the assert cap** reports evidence-unavailable instead of a proven absence — the substring could lie past the cut |
|
|
260
269
|
| `dispatch_count_max: <N>` | at most N sub-agents dispatched — an author-chosen budget (Cowork imposes no in-conversation Task-dispatch cap; records only, enforces nothing — see gotcha 12) |
|
|
261
270
|
| `skill_triggered: <regex>` | a skill matching the regex (invoked id, e.g. `"plugin:skill"`) was invoked via the `Skill` tool — evidence-unavailable (not a normal fail) if the agent's init tools have no `Skill` tool |
|
|
262
271
|
| `no_skill_triggered: <regex>` | no invoked skill id matched — the negative-control / description-collision catcher; evidence-unavailable (never a vacuous pass) if invocation data is absent or the `Skill` tool is unobservable |
|
|
@@ -291,6 +300,9 @@ same set live from the schema.
|
|
|
291
300
|
| `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 |
|
|
292
301
|
| `hook_blocked: <regex>` | a PreToolUse hook blocked a tool whose name matches the regex (`RunResult.hookEvents`) — evidence-unavailable if hook telemetry is absent. Replay: needs a `controlOut` cassette (a custom hook's decision lives only there, not the recorded stream) |
|
|
293
302
|
| `no_hook_blocked: true` | no tool was hook-blocked during the run (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** |
|
|
303
|
+
| `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** |
|
|
304
|
+
| `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" |
|
|
305
|
+
| `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** |
|
|
294
306
|
| `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 |
|
|
295
307
|
| `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 |
|
|
296
308
|
| `allow_l0_plugin_divergence: true` | verdict modifier — opt into L0/protocol plugin divergence: suppresses the default-fail when a plugin behaves differently at `protocol` (L0) fidelity than under a sandboxed tier. Live tiers only |
|
|
@@ -319,7 +331,7 @@ dotted path.
|
|
|
319
331
|
|
|
320
332
|
**VerdictSignals in `result.signals`:** `computeVerdict` pushes signals into `result.signals`; most
|
|
321
333
|
are **fail**-severity (they flip the run's pass/exit code even though `result.result` itself stays
|
|
322
|
-
`"success"`) and only
|
|
334
|
+
`"success"`) and only three are **warn**-severity (informational, never flip pass/fail). Current signal
|
|
323
335
|
codes (`VerdictSignal["code"]` in `src/run/verdict.ts`):
|
|
324
336
|
|
|
325
337
|
| Code | Severity | Meaning |
|
|
@@ -337,10 +349,11 @@ codes (`VerdictSignal["code"]` in `src/run/verdict.ts`):
|
|
|
337
349
|
| `stalled` | fail | The run ended on an unanswered question (opt out: `allow_stall`) |
|
|
338
350
|
| `non_deterministic` | warn | The run was LLM/external/human-decided — not reproducible |
|
|
339
351
|
| `prompt_asset_missing` | warn | The run proceeded with a missing prompt asset (`COWORK_HARNESS_ALLOW_MISSING_PROMPT=1`); fidelity is degraded |
|
|
352
|
+
| `scan_unavailable` | warn | Post-run scan evidence unavailable (`RunResult.scan` undefined) — the host-path and outputs-delete guards did not run this run |
|
|
340
353
|
|
|
341
354
|
A **fail**-severity signal does not change `result.result` (still `"success"`), but it DOES fail the
|
|
342
355
|
overall run verdict and exit code — `assert result: success` alone won't catch it; check
|
|
343
|
-
`result.signals[].severity` or the run's exit code. Only the
|
|
356
|
+
`result.signals[].severity` or the run's exit code. Only the three **warn** codes are truly benign.
|
|
344
357
|
|
|
345
358
|
## Replay class
|
|
346
359
|
|
|
@@ -356,7 +369,8 @@ on recording-shaping drift (`prompt`/`answers`/`baseline`/`skills`) or skill-con
|
|
|
356
369
|
sourcing ≠ evaluation (replay warns when you edit one). `verify-run` is the on-disk-`assert:` path for a kept
|
|
357
370
|
*run dir*; `--assert-from` is the equivalent for a *cassette*.
|
|
358
371
|
|
|
359
|
-
**Evaluated on replay (content):** `transcript_*`, `tool_*`, `subagent_*`, `
|
|
372
|
+
**Evaluated on replay (content):** `transcript_*`, `tool_*`, `subagent_*`, `subagent_file_write`,
|
|
373
|
+
`no_vm_path_file_op`, `dispatch_count_max`,
|
|
360
374
|
`skill_triggered`, `no_skill_triggered`, `skill_available`, `connector_available`, `tool_available`,
|
|
361
375
|
`skill_tool_used`, `max_cost_usd`, `max_tokens`, `tool_calls_max`, `tool_no_error`,
|
|
362
376
|
`max_tool_errors`, `max_redundant_tool_calls`, `max_turns`, `compaction_occurred`, `all_tasks_completed`, `task_status`, `task_count_min`, `no_scratchpad_leak`, `present_files_called`, `result`
|
|
@@ -365,7 +379,9 @@ modifiers `allow_permissive_auto_allow` / `allow_missing_capability` / `allow_l0
|
|
|
365
379
|
`allow_stall` are also kept on replay, evaluated as no-op passes.
|
|
366
380
|
|
|
367
381
|
**Gate keys — replay only with a `controlOut` cassette:** `question_asked`, `questions_count_max`,
|
|
368
|
-
`gate_answers_delivered`, `gate_answer_count_min`, `hook_blocked`, `no_hook_blocked
|
|
382
|
+
`gate_answers_delivered`, `gate_answer_count_min`, `hook_blocked`, `no_hook_blocked`, `vm_path_denied`,
|
|
383
|
+
`path_denied`, `no_path_denied` (the latter three are also `fidelity: hostloop`-only — see the assertion
|
|
384
|
+
table). With `controlOut` present they evaluate; on an old
|
|
369
385
|
cassette without it, a **loud warning** fires and they are **excluded** (not vacuously passed). Re-record to enable them.
|
|
370
386
|
`questions_count_max` counts sub-questions, not gates/tool-calls — see the catalog row above and
|
|
371
387
|
`trace --view questions`. `hook_blocked`/`no_hook_blocked` need `controlOut` for a different reason than
|
|
@@ -498,8 +514,13 @@ reference omits). Neither list is a strict superset of the other — reach for t
|
|
|
498
514
|
`artifact_json` (dotted `path` + operator); use the pytest lane (`assert_artifact_json` with a real
|
|
499
515
|
predicate) only for checks too complex for a dotted path. Find field paths via `--keep`.
|
|
500
516
|
|
|
501
|
-
10. **`subagent_dispatched` matches by `description` too** —
|
|
502
|
-
`subagent_type`
|
|
517
|
+
10. **`subagent_dispatched` matches by `resolvedAgentType` or `description` too** — a `Task` dispatch
|
|
518
|
+
with no `subagent_type` at all falls back to the built-in `general-purpose` agent (a WILDCARD tool
|
|
519
|
+
surface, `tools:["*"]`, incl. workspace bash) rather than leaving the type unresolved; the harness
|
|
520
|
+
warns loudly on this fallback and records `subagents[].dispatchTypeOmitted`, so match on the
|
|
521
|
+
resolved type or the dispatch description. `subagent_tool_absent` on a type-less dispatch is
|
|
522
|
+
correspondingly weaker evidence (wildcard surface) — pin `subagent_type` explicitly when you need a
|
|
523
|
+
tight guarantee.
|
|
503
524
|
|
|
504
525
|
11. **`subagent_declared_but_unused` fires on declared-but-didn't-use-THAT-tool**, even if the
|
|
505
526
|
sub-agent used other tools.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Each recipe composes facts that live scattered across SKILL.md and the other references into one
|
|
4
4
|
decision path. Every one answers a question a real fleet owner had to work out the hard way. Facts track the harness version in SKILL.md's
|
|
5
|
-
front-matter (currently 0.
|
|
5
|
+
front-matter (currently 0.31.0). Recipe 2's `resolved-tier`/`unverifiable-tier` staleness classes and
|
|
6
6
|
Recipe 3's `init-redact` shipped in 0.24.0 and are part of the current feature set — no version gate
|
|
7
7
|
needed if your CLI meets SKILL.md's version floor.
|
|
8
8
|
|
|
@@ -64,8 +64,8 @@ Top-level fields of a `*.cassette.json` (schema `schema/cassette.v10.json`):
|
|
|
64
64
|
| `preRunHashes` | Pre-run per-path sha256 baseline for `input_unmodified` (added 0.27.0, no cassetteVersion bump); a `null` value marks a path whose recorded artifact body was secret-scrubbed (evidence-unavailable, never a false "modified") |
|
|
65
65
|
| `scenarioSource` | Relative path to the authored YAML this was recorded from |
|
|
66
66
|
| `authoring` | Present iff a live decider answered ≥1 gate during recording (`nonDeterministic: true`) |
|
|
67
|
-
| `sessionFingerprint` | v9
|
|
68
|
-
| `folderPrefixMap` | v9+: the record-time connected-folder host-path → mount-name map. Replay's `computer_links_resolve` uses THIS (never the current session file)
|
|
67
|
+
| `sessionFingerprint` | Optional even on v9+ (the minimum readable version): hash of the session's content-relevant SHAPE (folders/plugins/skills/mcp/egress). Checked ONLY by `verify-cassettes`, never the default replay verdict; absent → not checked |
|
|
68
|
+
| `folderPrefixMap` | Optional even on v9+: the record-time connected-folder host-path → mount-name map. Replay's `computer_links_resolve` uses THIS (never the current session file); absent → the link is treated as evidence-unavailable, never reconstructed from the current session |
|
|
69
69
|
| `timeline`, `timelineHeader` | The recorded per-event timeline (harness-observation timestamps for tool_use/tool_result/subagent_dispatch/thinking/decision/result, in total order) plus its header (`startedAtWall`/`startedAtMono` anchors); informational only — never affects the replay verdict. Absent on a cassette recorded before this field existed |
|
|
70
70
|
| `environment` | Recording provenance: `location` (`"local"` on every cassette this harness produces), plus the resolved `tier` and `agentBinaryFormat` |
|
|
71
71
|
|
|
@@ -28,9 +28,12 @@
|
|
|
28
28
|
"no_delete_in_outputs",
|
|
29
29
|
"no_hook_blocked",
|
|
30
30
|
"no_mcp_error",
|
|
31
|
+
"no_path_denied",
|
|
31
32
|
"no_scratchpad_leak",
|
|
32
33
|
"no_skill_triggered",
|
|
33
34
|
"no_unexpected_files",
|
|
35
|
+
"no_vm_path_file_op",
|
|
36
|
+
"path_denied",
|
|
34
37
|
"present_files_called",
|
|
35
38
|
"question_asked",
|
|
36
39
|
"questions_count_max",
|
|
@@ -42,7 +45,9 @@
|
|
|
42
45
|
"skill_tool_used",
|
|
43
46
|
"skill_triggered",
|
|
44
47
|
"subagent_declared_but_unused",
|
|
48
|
+
"subagent_dispatch_healthy",
|
|
45
49
|
"subagent_dispatched",
|
|
50
|
+
"subagent_file_write",
|
|
46
51
|
"subagent_output_contains",
|
|
47
52
|
"subagent_tool_absent",
|
|
48
53
|
"subagent_tool_used",
|
|
@@ -61,7 +66,8 @@
|
|
|
61
66
|
"transcript_no_host_path",
|
|
62
67
|
"transcript_not_contains",
|
|
63
68
|
"transcript_not_matches",
|
|
64
|
-
"user_visible_artifact"
|
|
69
|
+
"user_visible_artifact",
|
|
70
|
+
"vm_path_denied"
|
|
65
71
|
],
|
|
66
72
|
"topLevelKeys": [
|
|
67
73
|
"allow_host_writes",
|