cowork-harness 0.22.0 → 0.24.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 +54 -31
- package/.claude/skills/cowork-harness/references/ci-recipe.md +28 -7
- package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +4 -3
- package/.claude/skills/cowork-harness/references/scenario-schema.md +25 -10
- package/.claude/skills/cowork-harness/references/task-recipes.md +108 -0
- package/.claude/skills/cowork-harness/scripts/assertion-keys.json +1 -0
- package/.claude/skills/cowork-harness/scripts/scenario.py +85 -9
- package/.cowork-redact.json +11 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +342 -4
- package/CONTRIBUTING.md +1 -0
- package/DESIGN.md +2 -2
- package/README.md +33 -17
- package/SPEC.md +99 -32
- package/baselines/desktop-1.18286.0.json +54 -8
- package/baselines/prompts/desktop-1.18286.0/system-prompt-append.md +1 -2
- package/dist/assert.js +17 -1
- package/dist/cli-args.js +1 -1
- package/dist/cli.js +138 -47
- package/dist/decide/llm-transport.js +19 -4
- package/dist/errors.js +12 -0
- package/dist/glob.js +43 -0
- package/dist/loop-decision.js +0 -3
- package/dist/run/artifacts.js +109 -0
- package/dist/run/cassette.js +351 -76
- package/dist/run/chat.js +13 -3
- package/dist/run/display-translate.js +180 -1
- package/dist/run/doctor.js +25 -1
- package/dist/run/execute.js +53 -89
- package/dist/run/pre-run-manifest.js +61 -0
- package/dist/run/renderer.js +8 -3
- package/dist/run/trace-view.js +17 -7
- package/dist/run/verdict.js +8 -6
- package/dist/run/vm-path-ctx-file.js +123 -0
- package/dist/runtime/argv.js +13 -0
- package/dist/runtime/container.js +3 -0
- package/dist/runtime/hostloop.js +15 -2
- package/dist/runtime/lima.js +3 -3
- package/dist/runtime/microvm.js +5 -0
- package/dist/runtime/protocol.js +3 -0
- package/dist/scan.js +31 -9
- package/dist/session.js +6 -0
- package/dist/sync/cowork-sync.js +550 -11
- package/dist/types.js +7 -21
- package/dist/vm-paths.js +8 -4
- package/docker/Dockerfile.agent +8 -4
- package/docs/README.md +1 -1
- package/docs/cassette.md +48 -28
- package/docs/chat.md +4 -3
- package/docs/cowork-spawn-contract-1.12603.1.md +1 -1
- package/docs/debugging.md +10 -0
- package/docs/discovery.md +6 -0
- package/docs/fidelity-gaps.md +98 -0
- package/docs/invariants.md +1 -1
- package/docs/maintenance.md +1 -1
- package/docs/run-status.md +33 -0
- package/docs/scenario.md +15 -10
- package/docs/session.md +2 -0
- package/examples/replays/README.md +1 -1
- package/examples/replays/hostloop-computer-links.cassette.json +77 -0
- package/llms.txt +1 -1
- package/package.json +3 -2
- package/python/test_scenario_lint.py +92 -0
- package/schema/cassette.v7.json +18 -0
- package/schema/run-result.json +32 -0
- package/schema/scenario.schema.json +9 -1
- package/schema/verify-cassettes.json +70 -0
- package/scripts/boot-rootfs-vz.ts +4 -4
- package/scripts/build-rootfs-image.ts +1 -1
- package/scripts/capture-rootfs-manifest.ts +1 -1
- package/scripts/check-versions.ts +80 -3
- package/scripts/gen-schema.ts +4 -3
|
@@ -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.24.0
|
|
7
|
+
tracks-harness: cowork-harness 0.24.0 (baseline desktop-1.18286.0)
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# cowork-harness
|
|
@@ -22,7 +22,7 @@ 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.
|
|
25
|
+
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.24.0` (baseline
|
|
26
26
|
> `desktop-1.18286.0`). If your checkout is newer, prefer the live `--help`, `SPEC.md`, and
|
|
27
27
|
> `docs/*.md` over this snapshot, and re-run the bundled linter.
|
|
28
28
|
|
|
@@ -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.24.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.24.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.24.0"`. **Pin `@>=0.24.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.24.0 is what gates the commands/assertions this skill teaches: `assertions --list`, `scaffold <run-id>`, `trace --view dispatches`, `artifact_json` incl. the `in:` operator, `verify-cassettes`, batch `record <dir>`/`--rerecord-stale`, `record --concurrency <N>`, record-time redaction, multiSelect/`answer:`, `verify-run` answer-coverage, `record --max-artifact-bytes`, live record-time deciders, `verify-cassettes --allow-domain`/`--allow-email`/`--allow-path`/`--allow-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field, and `computer_links_resolve` (new in 0.22.0).)
|
|
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`).
|
|
@@ -55,7 +55,12 @@ Before the first command, confirm the CLI is reachable and **fail loud** (never
|
|
|
55
55
|
|
|
56
56
|
Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
|
|
57
57
|
verify-run · trace · inspect · diff · stats · decide · gates · answer · scaffold · assertions --list · sync ·
|
|
58
|
-
list · boundary-check · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
|
|
58
|
+
list · boundary-check · status · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
|
|
59
|
+
|
|
60
|
+
**Two different `scaffold` tools — don't confuse them.** The native `cowork-harness scaffold <run-id>`
|
|
61
|
+
above turns an already-*recorded* run into a scenario (needs a run to exist first). The bundled
|
|
62
|
+
`scripts/scenario.py scaffold --name … --skill …` in §9 builds a scenario from flags alone, no run
|
|
63
|
+
required. Passing §9's flag set to the native command fails with `unknown flag: --name` (exit 2).
|
|
59
64
|
|
|
60
65
|
## 2. Two files: session vs scenario
|
|
61
66
|
|
|
@@ -94,7 +99,8 @@ the folder basename (collision-resolved); there is no `to:` override. See `refer
|
|
|
94
99
|
| `hostloop` / `cowork` | Production split-exec: the agent loop is a **native process on the host** (no container around the file tools — matching production), with native Bash/WebFetch disabled and routed host-side via the workspace SDK-MCP server into a Docker VM sidecar | Highest-fidelity / parity runs. A writable connected folder needs `allow_host_writes: true` (see scenario-schema.md). |
|
|
95
100
|
|
|
96
101
|
Set the tier in the **scenario's `fidelity:` field**, not a flag — `run` rejects `--fidelity`
|
|
97
|
-
(it's a `skill
|
|
102
|
+
(it's a `skill`/`chat` flag; `run` takes fidelity only from the scenario). See
|
|
103
|
+
`references/fidelity-and-answers.md`.
|
|
98
104
|
|
|
99
105
|
## 5. Choose an answer path (gates: AskUserQuestion + tool-permission)
|
|
100
106
|
|
|
@@ -265,7 +271,7 @@ known-good skeleton (right tier, scripted `answers:` + `on_unanswered: fail`, co
|
|
|
265
271
|
separated from live-only ones, one concern per item) and **self-lints its own output**:
|
|
266
272
|
|
|
267
273
|
```bash
|
|
268
|
-
S="${CLAUDE_PLUGIN_ROOT}/
|
|
274
|
+
S="${CLAUDE_PLUGIN_ROOT}/scripts/scenario.py"
|
|
269
275
|
# Working from a repo checkout instead of an installed plugin?
|
|
270
276
|
# S=".claude/skills/cowork-harness/scripts/scenario.py"
|
|
271
277
|
python3 "$S" scaffold --name report-check --skill ./skills/report-gen \
|
|
@@ -282,10 +288,14 @@ cowork-harness lint scenarios/*.yaml
|
|
|
282
288
|
```
|
|
283
289
|
|
|
284
290
|
`lint` flags: filesystem/egress-only assertions on a `replay` gate (silent no-op), bad regex
|
|
285
|
-
quoting, an egress assert on `protocol` fidelity,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
291
|
+
quoting, an egress assert on `protocol` fidelity, `transcript_no_host_path` on `hostloop`/`protocol`
|
|
292
|
+
(ERROR — fails by design at those tiers; WARN on `fidelity: cowork`, whose tier resolves per the
|
|
293
|
+
baseline's host-loop gate), non-empty `requires_capabilities` on `protocol` without
|
|
294
|
+
`allow_missing_capability` (ERROR — the capability probe can't run there, so the run hard-fails as
|
|
295
|
+
unverifiable), a `controlOut`-gated key on a non-`controlOut` replay, mixed-class assertion items,
|
|
296
|
+
and hallucinated schema (`assertions:` vs `assert:`, unknown keys). Exit code is non-zero on errors
|
|
297
|
+
(CI-friendly). `scaffold` auto-upgrades the tier if you ask for egress on `protocol`, so it never
|
|
298
|
+
emits a scenario `lint` would reject.
|
|
289
299
|
|
|
290
300
|
CI placement: a **token-free `replay` PR gate** (content/structure only) + a **nightly live `run`**
|
|
291
301
|
(filesystem/egress). Fastest setup: `uses: yaniv-golan/cowork-harness@v1` (a packaged GitHub Action with a
|
|
@@ -356,9 +366,12 @@ are the ones that bite hardest.
|
|
|
356
366
|
sub-agent used other tools. And `subagent_dispatched` matches by dispatch **description** too —
|
|
357
367
|
skills often dispatch with no `subagent_type` (`agentType:"unknown"`), so match the description.
|
|
358
368
|
|
|
359
|
-
6. **`dispatch_count_max`
|
|
360
|
-
|
|
361
|
-
dispatch
|
|
369
|
+
6. **`dispatch_count_max` is an author-chosen budget, not a production cap.** It's a post-hoc count
|
|
370
|
+
assertion: passing means "happened to dispatch ≤N this run," nothing more. Cowork imposes **no**
|
|
371
|
+
in-conversation `Task`-dispatch cap — gate `1648655587`'s `{perTask:1, global:3}` governs the
|
|
372
|
+
separate scheduled/cron-task session scheduler, not the `Task` tool (binary-verified; see SPEC
|
|
373
|
+
§10). So there is no "skip-on-cap" for the harness to reproduce; use this key only to catch a
|
|
374
|
+
fan-out you don't want.
|
|
362
375
|
|
|
363
376
|
7. **`protocol` is rejected (not silently passed) if the scenario asserts egress** — boundary
|
|
364
377
|
assertions need a sandboxed tier (`container`+). Good: this one fails loud by design.
|
|
@@ -392,23 +405,29 @@ are the ones that bite hardest.
|
|
|
392
405
|
prompt. *Fix:* treat `prompt_asset_missing` as a blocking error in CI by checking the signals
|
|
393
406
|
array.
|
|
394
407
|
13. **`result: success` means the agent didn't error, NOT that the task completed — always assert on
|
|
395
|
-
artifacts/content.**
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
408
|
+
artifacts/content.**
|
|
409
|
+
- A turn that ends on a plain-text re-ask ("which file did you mean?") still reports
|
|
410
|
+
`result: success`.
|
|
411
|
+
- The harness catches this with a **`stalled`** verdict signal: a run that ends on a question and
|
|
412
|
+
did **no productive work after its last gate** — both the no-gate case ("which file?" with no
|
|
413
|
+
tool calls) AND the *answered-gate-then-re-ask* case (the agent answers an `AskUserQuestion`,
|
|
414
|
+
then asks again in plain text and stops). Suppress with `allow_stall: true` if ending on a
|
|
415
|
+
question is intended.
|
|
416
|
+
- The signal is a **tool-position heuristic**, not deliverable detection, so it is imprecise both
|
|
417
|
+
ways:
|
|
418
|
+
- **False negative:** a post-gate tool *call* clears the flag whether it **succeeded or
|
|
419
|
+
errored** — an agent that ran a tool after the gate and still stalled is not caught.
|
|
420
|
+
- **False positive:** a deliverable written *before* a final confirmation gate does **not**
|
|
421
|
+
clear it, so a write-then-confirm-then-question run is flagged — use `allow_stall: true` for a
|
|
422
|
+
deliberate confirm-terminal skill.
|
|
423
|
+
- The broad guard is therefore YOUR assertions — assert the deliverable (`file_exists` /
|
|
424
|
+
`artifact_json` / `transcript_matches`), never just `result: success`.
|
|
425
|
+
- `on_unanswered` governs **unanswered** `AskUserQuestion` gates; the `stalled` signal covers
|
|
426
|
+
stalling *after* one is answered — two different failure modes.
|
|
427
|
+
- **Free-text aside:** a "type-it-in-notes" option has **no scripted deterministic answer** today
|
|
428
|
+
(the `OTHER:` directive works only on the LLM-decider path, not scripted `choose:`; on an
|
|
429
|
+
options-bearing gate a bare out-of-set LLM answer fails loud (exit 2) — see the LLM-decider
|
|
430
|
+
free-text note in `references/fidelity-and-answers.md`).
|
|
412
431
|
14. **A positional `choose` (`first` / index) is order-dependent.** `choose: "2"` survives label drift
|
|
413
432
|
but NOT option *re-ordering* — if the gate presents its options in a different order run-to-run, the
|
|
414
433
|
index lands on a different option (a silent re-record flake). Prefer an exact label when order is
|
|
@@ -444,6 +463,10 @@ and the CI recipe, read the files in `references/`.
|
|
|
444
463
|
|
|
445
464
|
## References
|
|
446
465
|
|
|
466
|
+
- `references/task-recipes.md` — end-to-end recipes for the four jobs fleet owners actually hit:
|
|
467
|
+
evolve a cassette's `assert:` (usually no re-record), audit a fleet for tier drift, set up
|
|
468
|
+
redaction before the first hostloop/protocol record, derive budget assertions without a
|
|
469
|
+
two-pass record. Start here when the question is "how do I do X", not "what does flag Y mean".
|
|
447
470
|
- `references/scenario-schema.md` — scenario/session YAML schema, full assertion catalog (with each
|
|
448
471
|
key's replay class), the web_fetch model, and the complete gotcha list.
|
|
449
472
|
- `references/fidelity-and-answers.md` — fidelity tiers, answer paths, the determinism contract.
|
|
@@ -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.24.0` (baseline `desktop-1.18286.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):
|
|
@@ -53,7 +53,7 @@ sha256-*checked* but not hard-blocking on mismatch — it's advisory for an inte
|
|
|
53
53
|
GitHub-hosted runners, no token/Docker/agent:
|
|
54
54
|
|
|
55
55
|
```yaml
|
|
56
|
-
- run: npm i -g cowork-harness@>=0.
|
|
56
|
+
- run: npm i -g "cowork-harness@>=0.24.0"
|
|
57
57
|
- run: cowork-harness lint scenarios/*.yaml # no silent false-greens
|
|
58
58
|
- run: cowork-harness verify-cassettes cassettes/ # privacy + staleness
|
|
59
59
|
- run: cowork-harness replay cassettes/ # token-free content/structure
|
|
@@ -125,11 +125,20 @@ dollar figures). In a skill repo these cassettes get **committed**. So:
|
|
|
125
125
|
customer's cap table.
|
|
126
126
|
- **Opt-in redaction** rewrites configured PII out of the cassette at record time. Drop a
|
|
127
127
|
`.cowork-redact.json` next to your scenarios (or set `COWORK_HARNESS_REDACT_PATTERNS` /
|
|
128
|
-
`_KEYS`); empty by default.
|
|
128
|
+
`_KEYS`); empty by default. The policy is searched in **cwd → the scenario's dir → the cassette's
|
|
129
|
+
dir** (first file found per dir; env vars merge on top). `cowork-harness init-redact` copies the
|
|
130
|
+
packaged reference template (local-path prefixes + a generic email regex) into the cwd as a starting
|
|
131
|
+
point — review and tailor it. Redaction is **verdict-preserving** — `record` refuses to write if
|
|
129
132
|
redaction would flip an assertion (a manufactured green). `--no-redact` skips it for known-synthetic
|
|
130
133
|
inputs.
|
|
131
|
-
- **
|
|
132
|
-
|
|
134
|
+
- **Pre-spawn preflight**: `record` warns (`::warning::`, before the paid run starts — once per batch
|
|
135
|
+
for `record <dir>` / `--rerecord-stale`) when a scenario about to record at a host-path-bearing tier
|
|
136
|
+
(`hostloop`, `protocol`) has an **empty** redaction policy — that combination commits real host paths
|
|
137
|
+
the `path` scanner then hard-fails at `verify-cassettes` time. The always-on scanner remains the
|
|
138
|
+
universal net (container-tier recordings can trip it too).
|
|
139
|
+
- **Always-on scan gate** — `verify-cassettes` flags email / currency / bare-domain / local-path /
|
|
140
|
+
machine-inventory matches it finds in the committed cassettes and **exits non-zero**, so "no leak" is
|
|
141
|
+
a gate, not discipline.
|
|
133
142
|
Suppress synthetic / public reference names (NVCA, Cooley GO, …) with `--allow <regex>`. (Multi-word
|
|
134
143
|
proper names are NOT a default class — too noisy to gate on; add a pattern via config if your corpus
|
|
135
144
|
needs it.)
|
|
@@ -178,7 +187,7 @@ jobs:
|
|
|
178
187
|
with: { node-version: '20' }
|
|
179
188
|
- uses: actions/setup-python@v5
|
|
180
189
|
with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
|
|
181
|
-
- run: npm i -g cowork-harness@>=0.
|
|
190
|
+
- run: npm i -g "cowork-harness@>=0.24.0"
|
|
182
191
|
- run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
|
|
183
192
|
- run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
|
|
184
193
|
- run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
|
|
@@ -207,7 +216,7 @@ jobs:
|
|
|
207
216
|
echo "live=true" >> "$GITHUB_OUTPUT"
|
|
208
217
|
fi
|
|
209
218
|
- if: steps.guard.outputs.live == 'true'
|
|
210
|
-
run: npm i -g cowork-harness@>=0.
|
|
219
|
+
run: npm i -g "cowork-harness@>=0.24.0"
|
|
211
220
|
- if: steps.guard.outputs.live == 'true'
|
|
212
221
|
run: cowork-harness run scenarios/ --output-format json
|
|
213
222
|
env:
|
|
@@ -234,6 +243,11 @@ scenario = `result === "success" && assertions.every(pass)`. Exit code is non-ze
|
|
|
234
243
|
fails or a run errors, so a plain `cowork-harness run scenarios/` is already CI-ready without parsing
|
|
235
244
|
JSON.
|
|
236
245
|
|
|
246
|
+
`verify-cassettes` emits its **own** envelope (`{command, ok, coverage, results[]}` with per-file
|
|
247
|
+
`findings`/`staleness`/`notes`/`version`/`error`), published as `schema/verify-cassettes.json` in the
|
|
248
|
+
npm package. Both envelope schemas are covered 1.0 contract surfaces (SPEC §12) — parse the JSON, not
|
|
249
|
+
the human-readable text (which is explicitly NOT stable).
|
|
250
|
+
|
|
237
251
|
A run writes to `~/.cowork-harness/runs/<name>/<sessionId>/` by default — outside any working tree. In CI,
|
|
238
252
|
set `COWORK_HARNESS_RUNS_DIR` (or pass `--run-dir`) to a workspace-relative path (e.g. `runs`) so an
|
|
239
253
|
artifact-upload step can collect them. Each run dir holds `events.jsonl`, `control-out.jsonl`, `run.jsonl`,
|
|
@@ -260,6 +274,13 @@ does **not** imply the recording is still valid. Each replay result carries `sta
|
|
|
260
274
|
| `format` | recorded under an older hash format | re-record once |
|
|
261
275
|
| `unverifiable-baseline` | the latest baseline couldn't be loaded | couldn't verify (env, not skill) |
|
|
262
276
|
| `unverifiable-skill` | skill dirs unresolvable — skill staleness couldn't be checked | couldn't verify the skill |
|
|
277
|
+
| `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) |
|
|
278
|
+
| `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 |
|
|
279
|
+
|
|
280
|
+
(A pre-`effectiveFidelity` cassette with an **explicit** tier is statically knowable — it passes the tier
|
|
281
|
+
check with a non-failing informational note in the `verify-cassettes` envelope's per-file `notes[]`, a
|
|
282
|
+
`·`-prefixed row in text output. On `verify-cassettes` every staleness *finding* above is a hard fail —
|
|
283
|
+
the gate is class-blind; notes never fail it.)
|
|
263
284
|
|
|
264
285
|
To gate in CI, pick the severity you want:
|
|
265
286
|
|
|
@@ -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.24.0` (baseline `desktop-1.18286.0`).
|
|
4
4
|
|
|
5
5
|
## Fidelity tiers (`fidelity:` in the scenario)
|
|
6
6
|
|
|
@@ -187,5 +187,6 @@ and `scaffold` refuse to treat a partial run's half-finished output as a passing
|
|
|
187
187
|
redact from logs (beyond the auth tokens + `ANTHROPIC_CUSTOM_HEADERS`).
|
|
188
188
|
- `COWORK_HARNESS_SOFT_MISSING` — downgrade a missing mount source from hard-error to warn-and-skip.
|
|
189
189
|
- `COWORK_VM_GATEWAY` / `COWORK_VM_PROXY_PORT` / `COWORK_LIMA_INSTANCE` — L2 (microVM) knobs.
|
|
190
|
-
- `COWORK_LOCKDOWN` — default `on`:
|
|
191
|
-
|
|
190
|
+
- `COWORK_LOCKDOWN` — default `on`: gates sandbox hardening on every isolated tier — **aborts loudly**
|
|
191
|
+
if the L2 guest firewall fails to apply (no silent unprotected run), and also gates the
|
|
192
|
+
`container`/`hostloop` Docker hardening. Set `=off` to opt out and run without isolation deliberately.
|
|
@@ -1,6 +1,6 @@
|
|
|
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.
|
|
3
|
+
Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.24.0`
|
|
4
4
|
(baseline `desktop-1.18286.0`). If your checkout is newer, prefer the live `docs/scenario.md`,
|
|
5
5
|
`docs/session.md`, and `SPEC.md`.
|
|
6
6
|
|
|
@@ -61,6 +61,12 @@ skills: [report-gen] # OPTIONAL — scope the cassette-staleness
|
|
|
61
61
|
# `skills/<name>` dir under a mounted plugin-root) + the plugin's shared
|
|
62
62
|
# roots. Fail-closed to whole-tree on an unknown name. Omit = whole tree.
|
|
63
63
|
|
|
64
|
+
requires_capabilities: [ocr] # OPTIONAL — declare a capability the skill needs (e.g. office_convert,
|
|
65
|
+
# ocr, pdf_tables, opencv). If the running agent image provably omits
|
|
66
|
+
# one, the harness ABORTS before the paid run (exit 3) — unless the
|
|
67
|
+
# scenario also asserts `allow_missing_capability: true`, which downgrades
|
|
68
|
+
# the abort to a notice and proceeds. Live tiers only.
|
|
69
|
+
|
|
64
70
|
allow_host_writes: true # OPTIONAL — required to run `hostloop` fidelity with a WRITABLE
|
|
65
71
|
# connected folder (session `mode: rw`/`rwd`): with no container
|
|
66
72
|
# around hostloop's native file tools, that combination gives the
|
|
@@ -80,8 +86,10 @@ session = your setup.**
|
|
|
80
86
|
```yaml
|
|
81
87
|
# model & reasoning
|
|
82
88
|
model: claude-opus-4-8 # omit for the agent default
|
|
89
|
+
account_name: my-account # OPTIONAL — which configured account/credential set to run as
|
|
83
90
|
effort: high # low | medium | high | xhigh
|
|
84
91
|
max_thinking_tokens: 31999 # positive int, or per-model map {default, <model>: <n>}; default 31999
|
|
92
|
+
extended_thinking: true # INERT — not a real Cowork toggle; use max_thinking_tokens instead
|
|
85
93
|
permission_mode: default # default | acceptEdits | plan | bypassPermissions
|
|
86
94
|
permission_parity: cowork # cowork (unscripted tool calls allowed) | strict (deny unscripted)
|
|
87
95
|
|
|
@@ -97,8 +105,9 @@ plugins:
|
|
|
97
105
|
marketplaces: [] # plugin_marketplaces (git URLs or local paths)
|
|
98
106
|
local_marketplaces: [] # local marketplace dirs (each has a marketplace.json)
|
|
99
107
|
enabled: [my-skill@local] # enabledPlugins (name@marketplace)
|
|
100
|
-
local_plugins: [./skills/my-skill] # host plugin dirs → mnt/.local-plugins/marketplaces/<
|
|
101
|
-
# (≥1.14271.0
|
|
108
|
+
local_plugins: [./skills/my-skill] # host plugin dirs → mnt/.local-plugins/marketplaces/local-desktop-app-uploads/<plugin>
|
|
109
|
+
# (the marketplace segment is that fixed synthetic name; ≥1.14271.0 —
|
|
110
|
+
# older baselines use mnt/.local-plugins/cache)
|
|
102
111
|
remote_plugins: []
|
|
103
112
|
skills:
|
|
104
113
|
local: [] # extra host skill dirs
|
|
@@ -210,6 +219,7 @@ passes only if every key passes. Keep one concern per item unless you mean conju
|
|
|
210
219
|
| `file_exists: <path>` | the path exists under the run's `work/` (anchored at `mnt/`, e.g. `outputs/x.md`). For a user-facing deliverable prefer `user_visible_artifact` — with a connected folder the file lands in `mnt/<folder>` (= `{{workspaceFolder}}`), not `mnt/outputs`, so `file_exists: outputs/x.md` misses it |
|
|
211
220
|
| `user_visible_artifact: <path>` | exists **and** under a user-visible root (`outputs/` + each connected folder's mount name) — the right primitive for a workspace deliverable when a folder is connected |
|
|
212
221
|
| `no_delete_in_outputs: true` | no delete op touched `mnt/outputs` — **only `true` is valid**; `false` is rejected (omit to allow deletes) |
|
|
222
|
+
| `no_unexpected_files: [<glob>, …]` | every **newly created** file under a user-visible root matches ≥1 glob (workRoot-relative paths; `**` = whole path segment for any depth — use `outputs/handoff/**` for per-run subdirs); `[]` = no new files; **new-files-only** — overwriting a pre-existing file in place is invisible (use content-level producer stamping); live/verify-run without a pre-run manifest ⇒ evidence-unavailable hard-fail (live runs capture the baseline only when this key is asserted; recordings always capture); **microvm cannot capture** (use container/hostloop); replay needs `cassette.preRunPaths` (≥0.24 container/hostloop recordings) — cassettes without it **exclude** the key with a loud warning |
|
|
213
223
|
| `self_heal_ran: <bool>` | a plugin-root self-heal script was (not) invoked |
|
|
214
224
|
| `tool_called: <Tool>` | the agent invoked the tool (actually ran it) |
|
|
215
225
|
| `tool_not_called: <Tool>` | the agent never invoked it |
|
|
@@ -219,7 +229,7 @@ passes only if every key passes. Keep one concern per item unless you mean conju
|
|
|
219
229
|
| `subagent_tool_absent: <Tool>` | no sub-agent used the tool |
|
|
220
230
|
| `subagent_dispatched: <regex>` | a sub-agent whose `agentType` **or dispatch description** matches |
|
|
221
231
|
| `subagent_declared_but_unused: <Tool>` | a sub-agent declared the tool but never used **that** tool (even if it used others) |
|
|
222
|
-
| `dispatch_count_max: <N>` | at most N sub-agents dispatched (records only
|
|
232
|
+
| `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) |
|
|
223
233
|
| `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 |
|
|
224
234
|
| `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 |
|
|
225
235
|
| `max_cost_usd: <N>` | the run's SDK-reported cost is ≤ N USD — evidence-unavailable if cost telemetry is absent. **Replay asserts the frozen recording's cost, not fresh spend** — a real regression needs a live `run` |
|
|
@@ -234,7 +244,7 @@ passes only if every key passes. Keep one concern per item unless you mean conju
|
|
|
234
244
|
| `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 |
|
|
235
245
|
| `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 |
|
|
236
246
|
| `allow_stall: true` | verdict modifier — suppresses the `stalled` default-fail when a run ends on a question 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) |
|
|
237
|
-
| `transcript_no_host_path: true` | no host path (`/Users/`, `/opt/cowork/`, `/home/`, `/root/`) leaked into model-visible text — **incompatible with `hostloop`**:
|
|
247
|
+
| `transcript_no_host_path: true` | no host path (`/Users/`, `/opt/cowork/`, `/home/`, `/root/`) leaked into model-visible text — **incompatible with `hostloop` AND `protocol`**: hostloop's native file tools legitimately expose real host paths, and protocol (L0) runs the agent's file tools on the real host cwd with no sealed filesystem, so this fails BY DESIGN on both (the harness warns at run start if asserted anyway); use `container`/`microvm` for this check |
|
|
238
248
|
| `egress_denied: <host>` | the host was blocked by the egress proxy |
|
|
239
249
|
| `egress_allowed: <host>` | the host was allowed through |
|
|
240
250
|
| `artifact_json: {artifact, path, …}` | assert a JSON artifact's contents — `equals`/`gt`/`in`/`exists`/`absent`/`is_null` over a dotted `path` (`in` = membership in a list, for a stochastic/LLM value; `absent` ≠ `is_null`; an unresolved intermediate fails loud) |
|
|
@@ -293,7 +303,9 @@ small-file JSON `body` inlined; a hash-only entry still satisfies `file_exists`.
|
|
|
293
303
|
resolves a `/sessions/…/mnt/…`-shaped link directly against the manifest, and a host-shaped (hostloop) link
|
|
294
304
|
by first normalizing it to a mount-relative path (recorded connected-folder prefixes + the outputs/uploads
|
|
295
305
|
mounts) — replay has no live filesystem to check a host path against directly (that only happens on a live
|
|
296
|
-
`run`/`verify-run`). Without a manifest (older cassettes) all
|
|
306
|
+
`run`/`verify-run`). Without a manifest (older cassettes) all five are skipped; `no_unexpected_files` also
|
|
307
|
+
needs `preRunPaths` (≥0.24 recordings) — without it the key is excluded with a loud warning (live/verify-run
|
|
308
|
+
hard-fails evidence-unavailable instead). A green replay re-confirms
|
|
297
309
|
*record-time* artifacts, not that the current skill still produces them — `replay --strict` fails when the
|
|
298
310
|
staleness `fingerprint` shows ANY skill/baseline drift, or `replay --fail-on-skill-drift` only on
|
|
299
311
|
skill-source drift; every replay result also reports it class-tagged in `staleness[]` for a JSON gate.
|
|
@@ -372,7 +384,8 @@ at the top of this file.
|
|
|
372
384
|
the stochastic path flags the run `nonDeterministic`. The LLM decider is one mechanism, two
|
|
373
385
|
spellings: `on_unanswered: llm` (YAML) and `--decider-llm` (CLI). The bare `--on-unanswered llm`
|
|
374
386
|
is rejected (use `--decider-llm`). `agent` is **retired** — `on_unanswered: agent` is rejected by
|
|
375
|
-
the schema. (`src/types.ts:
|
|
387
|
+
the schema. (`src/types.ts:365` — the `on_unanswered` enum; `src/cli.ts:899` — the CLI-side
|
|
388
|
+
`--on-unanswered` value check.)
|
|
376
389
|
|
|
377
390
|
4. **`--on-unanswered first` is non-deterministic too** — it picks option 1 and is flagged
|
|
378
391
|
`nonDeterministic`; not a deterministic substitute for scripted answers.
|
|
@@ -400,9 +413,11 @@ at the top of this file.
|
|
|
400
413
|
11. **`subagent_declared_but_unused` fires on declared-but-didn't-use-THAT-tool**, even if the
|
|
401
414
|
sub-agent used other tools.
|
|
402
415
|
|
|
403
|
-
12. **`dispatch_count_max`
|
|
404
|
-
|
|
405
|
-
|
|
416
|
+
12. **`dispatch_count_max` is an author-chosen budget, not a production cap.** It records the count
|
|
417
|
+
and asserts on it; passing means "dispatched ≤N this run," not "the harness capped it." Cowork
|
|
418
|
+
imposes no in-conversation Task-dispatch cap to reproduce — gate `1648655587`'s
|
|
419
|
+
`{perTask:1, global:3}` is the scheduled/cron-task session limiter, a different mechanism
|
|
420
|
+
(binary-verified; SPEC §10).
|
|
406
421
|
|
|
407
422
|
13. **`protocol` is rejected (not silently passed) if the scenario asserts egress** — boundary
|
|
408
423
|
assertions need `container`+. Fails loud by design.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Task recipes — end-to-end paths for the four jobs consumers actually reverse-engineer
|
|
2
|
+
|
|
3
|
+
Each recipe composes facts that live scattered across SKILL.md and the other references into one
|
|
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; the `resolved-tier`/`unverifiable-tier` staleness classes (Recipe 2) and
|
|
6
|
+
`init-redact` (Recipe 3) are new in the release AFTER 0.23.0.
|
|
7
|
+
|
|
8
|
+
## Recipe 1 — Evolve the `assert:` block of an existing cassette (usually NO re-record)
|
|
9
|
+
|
|
10
|
+
You added or changed assertion keys in a scenario whose cassette is already committed. Do you need
|
|
11
|
+
a paid live re-record? Walk this tree — the answer is usually no:
|
|
12
|
+
|
|
13
|
+
1. **Content / always-replay keys** (`transcript_*`, `tool_called`/`tool_not_called`,
|
|
14
|
+
`tool_result_*`, `subagent_*`, `dispatch_count_max`, `skill_triggered`/`no_skill_triggered`,
|
|
15
|
+
`result`, `max_turns`, `tool_calls_max`, `max_cost_usd`, `max_tokens`) →
|
|
16
|
+
`cowork-harness replay <cassette> --assert-from <scenario.yaml>`. Token-free, no re-record.
|
|
17
|
+
If the recording genuinely lacks the telemetry a key needs (very old cassettes), the key fails
|
|
18
|
+
**loud** as `evidence-unavailable` — that is correct behavior, not a bug; only then re-record.
|
|
19
|
+
2. **Gate keys** (`question_asked`, `questions_count_max`, `gate_answers_delivered`) on a cassette
|
|
20
|
+
**with `controlOut`** (any modern recording) → same token-free `--assert-from` path.
|
|
21
|
+
3. **Gate keys** on a **pre-`controlOut`** cassette → one re-record unlocks gate asserts for that
|
|
22
|
+
cassette permanently.
|
|
23
|
+
4. **Filesystem / egress keys** (`file_exists` without an artifact manifest, `egress_allowed`,
|
|
24
|
+
`egress_denied`, `no_delete_in_outputs`) → live lane **by design**; replay skips them with a
|
|
25
|
+
loud `::warning::`. Keep them in the scenario, run them on the nightly live gate.
|
|
26
|
+
|
|
27
|
+
**The one hard caveat:** `--assert-from` **hard-fails on recording-shaping drift** — if the
|
|
28
|
+
scenario's `prompt:`, `answers:`, baseline, or skill content differ from what shaped the
|
|
29
|
+
recording, re-asserting under them is meaningless and the harness refuses. The trap you'll
|
|
30
|
+
actually hit is `answers:`: sequence your edits — land assert-only changes (token-free), and land
|
|
31
|
+
answer changes separately (those DO force a re-record).
|
|
32
|
+
|
|
33
|
+
## Recipe 2 — Audit a fleet for tier drift
|
|
34
|
+
|
|
35
|
+
A `fidelity: cowork` scenario records at whatever tier the baseline's host-loop gate resolves to
|
|
36
|
+
at record time. When Cowork flips that gate, your committed cassettes silently stop representing
|
|
37
|
+
what production would do. Two layers of defense:
|
|
38
|
+
|
|
39
|
+
- **Automated (preferred):** `cowork-harness verify-cassettes <dir>` raises a
|
|
40
|
+
**`resolved-tier`** staleness finding when a `fidelity: cowork` cassette's recorded
|
|
41
|
+
`effectiveFidelity` no longer matches what the current baseline resolves to, and
|
|
42
|
+
**`unverifiable-tier`** when a `fidelity: cowork` cassette predates `effectiveFidelity`
|
|
43
|
+
entirely (can't verify → re-record). Explicit-tier cassettes (`hostloop`, `container`, …) are
|
|
44
|
+
statically knowable and only produce a non-failing informational note.
|
|
45
|
+
- **Manual (one-liner):** `grep -h '"effectiveFidelity"' cassettes/*.cassette.json | sort | uniq -c`
|
|
46
|
+
shows the tier distribution of the fleet at a glance.
|
|
47
|
+
|
|
48
|
+
### Cassette anatomy (what you're looking at when you open one)
|
|
49
|
+
|
|
50
|
+
Top-level fields of a `*.cassette.json` (schema `schema/cassette.v7.json`):
|
|
51
|
+
|
|
52
|
+
| Field | What it is |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `$schema`, `generator`, `cassetteVersion` | Provenance: schema URL, producing tool, format version (current: 7) |
|
|
55
|
+
| `scenario` | The embedded scenario snapshot at record time |
|
|
56
|
+
| `events` | The recorded agent event stream (the replay source) |
|
|
57
|
+
| `controlOut` | Driver→agent control responses — presence unlocks gate asserts on replay |
|
|
58
|
+
| `effectiveFidelity` | The tier the live record actually resolved to (the drift-audit key above) |
|
|
59
|
+
| `artifacts` | Output-file manifest (paths + hashes + small inlined bodies) — unlocks `file_exists`/`artifact_json` on replay |
|
|
60
|
+
| `fingerprint` | Skill/baseline staleness tripwire |
|
|
61
|
+
| `userVisibleRoots` | The user-visible mount roots captured at record time |
|
|
62
|
+
| `preRunPaths` | Pre-run file-path baseline for `no_unexpected_files` (workRoot-relative; co-present with `userVisibleRoots`) |
|
|
63
|
+
| `scenarioSource` | Relative path to the authored YAML this was recorded from |
|
|
64
|
+
| `authoring` | Present iff a live decider answered ≥1 gate during recording (`nonDeterministic: true`) |
|
|
65
|
+
|
|
66
|
+
## Recipe 3 — Set up redaction BEFORE your first hostloop/protocol record
|
|
67
|
+
|
|
68
|
+
`hostloop` and `protocol` runs execute on the host, so real host paths (`/Users/you/…`) land in
|
|
69
|
+
the transcript and would be committed inside the cassette. Set the policy up **before** the first
|
|
70
|
+
record — retrofitting means re-recording:
|
|
71
|
+
|
|
72
|
+
1. `cowork-harness init-redact` — copies the reference `.cowork-redact.json` (host-path +
|
|
73
|
+
email patterns) into the current directory. Version-control it.
|
|
74
|
+
2. **Search set:** record looks for `.cowork-redact.json` in the **cwd, the scenario's directory,
|
|
75
|
+
and the cassette's output directory** — every distinct file found is MERGED (plus
|
|
76
|
+
`COWORK_HARNESS_REDACT_PATTERNS`/`_KEYS` from the env). Repo root (= cwd in CI) is the
|
|
77
|
+
conventional home.
|
|
78
|
+
3. **Preflight backstop:** `record` on a host-path-bearing tier with NO effective policy emits a
|
|
79
|
+
loud pre-spawn `::warning::` (once per batch). If you see it, stop and do step 1.
|
|
80
|
+
4. **Scanner relationship:** redaction (record-time, rewrites content) and the
|
|
81
|
+
`verify-cassettes` privacy scanner (CI-time, read-only tripwire over 4 classes: `domain`,
|
|
82
|
+
`email`, `path`, `machine-inventory`) are independent layers. The scanner catches what
|
|
83
|
+
redaction missed.
|
|
84
|
+
5. **Scoping idioms for scanner findings you've reviewed and accepted:** prefer the class-scoped
|
|
85
|
+
flags — `--allow-domain 'api\.example\.com'`, `--allow-email '.*@yourco\.com'`,
|
|
86
|
+
`--allow-path '/opt/ci-runner/.*'` — over bare `--allow` (which applies to every class and can
|
|
87
|
+
let a domain allow swallow an email leak). For a version-controlled allowlist use
|
|
88
|
+
`--allow-file <path>`: bare all-class patterns, one regex per line, `#` comments and blank
|
|
89
|
+
lines ignored.
|
|
90
|
+
|
|
91
|
+
## Recipe 4 — Budget assertions without a two-pass record
|
|
92
|
+
|
|
93
|
+
`max_turns` / `tool_calls_max` / `questions_count_max` / `dispatch_count_max` need observed
|
|
94
|
+
values, but you don't need a second paid run to get them — derive them from evidence you already
|
|
95
|
+
have:
|
|
96
|
+
|
|
97
|
+
1. **From the run you already did:** `run` always keeps its run dir. Read observed turns from
|
|
98
|
+
`result.json` (`usage.turns`), tool-call and question counts from
|
|
99
|
+
`cowork-harness trace <run-id> --view tools` / `--view questions`, and the dispatch count from
|
|
100
|
+
`--view dispatches`.
|
|
101
|
+
2. **From history:** `cowork-harness stats <scenario> --metric turns` gives p50/p95 turns across
|
|
102
|
+
indexed runs — better than a single observation.
|
|
103
|
+
3. **Set the budget with headroom:** observed × 1.5, rounded up (agentic run lengths vary run to
|
|
104
|
+
run; a budget at the observed value flakes).
|
|
105
|
+
4. **Replay-class note:** `max_turns` / `tool_calls_max` / `dispatch_count_max` re-evaluate
|
|
106
|
+
token-free on replay; `questions_count_max` needs `controlOut` (Recipe 1's tree applies).
|
|
107
|
+
`max_cost_usd` / `max_tokens` on replay assert the FROZEN recording's spend — near-zero signal
|
|
108
|
+
as a regression gate; if you need a cost gate, put it on the live lane via `stats`.
|