cowork-harness 0.25.0 → 0.26.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 +4 -4
- package/.claude/skills/cowork-harness/references/ci-recipe.md +4 -4
- package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +1 -1
- package/.claude/skills/cowork-harness/references/scenario-schema.md +1 -1
- package/CHANGELOG.md +40 -0
- package/README.md +3 -3
- package/SPEC.md +11 -4
- package/dist/assert.js +18 -10
- package/dist/run/cassette.js +36 -32
- package/dist/run/execute.js +2 -2
- package/dist/run/renderer.js +1 -1
- package/dist/run/skill-hash.js +14 -4
- package/docs/cassette.md +8 -6
- package/examples/replays/README.md +1 -1
- package/examples/replays/example-pdf-skill.cassette.json +30 -24
- package/package.json +1 -1
- package/schema/cassette.v8.json +166 -0
- package/schema/run-result.json +55 -1
|
@@ -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.26.0
|
|
7
|
+
tracks-harness: cowork-harness 0.26.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.26.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.26.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.26.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.26.0"`. **Pin `@>=0.26.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.26.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-patterns-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0; `--allow-patterns-file <path>` is a FILE of patterns, one regex per line — not a path to allow, unlike `--allow <regex>`), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field, and `computer_links_resolve` (new in 0.22.0).)
|
|
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`).
|
|
@@ -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.26.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.26.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
|
|
@@ -187,7 +187,7 @@ jobs:
|
|
|
187
187
|
with: { node-version: '20' }
|
|
188
188
|
- uses: actions/setup-python@v5
|
|
189
189
|
with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
|
|
190
|
-
- run: npm i -g "cowork-harness@>=0.
|
|
190
|
+
- run: npm i -g "cowork-harness@>=0.26.0"
|
|
191
191
|
- run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
|
|
192
192
|
- run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
|
|
193
193
|
- run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
|
|
@@ -216,7 +216,7 @@ jobs:
|
|
|
216
216
|
echo "live=true" >> "$GITHUB_OUTPUT"
|
|
217
217
|
fi
|
|
218
218
|
- if: steps.guard.outputs.live == 'true'
|
|
219
|
-
run: npm i -g "cowork-harness@>=0.
|
|
219
|
+
run: npm i -g "cowork-harness@>=0.26.0"
|
|
220
220
|
- if: steps.guard.outputs.live == 'true'
|
|
221
221
|
run: cowork-harness run scenarios/ --output-format json
|
|
222
222
|
env:
|
|
@@ -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.26.0` (baseline `desktop-1.18286.0`).
|
|
4
4
|
|
|
5
5
|
## Fidelity tiers (`fidelity:` in the scenario)
|
|
6
6
|
|
|
@@ -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.26.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
|
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,46 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.26.0] — 2026-07-05
|
|
10
|
+
|
|
11
|
+
Cassette/run-result **format-freeze** pass before 1.0 — fixes the parts of these schemas that become
|
|
12
|
+
irreversible once 1.0's semver contract freezes them.
|
|
13
|
+
|
|
14
|
+
> **Upgrade notes.**
|
|
15
|
+
> - **Staleness-hash framing changed → `cassetteVersion` 8.** A pre-v8 cassette's staleness fingerprint
|
|
16
|
+
> is non-comparable with v8, so `verify-cassettes`/`rehash` route a pre-v8 cassette to **re-record**
|
|
17
|
+
> with an honest "algorithm changed" message (not a misleading "content changed"). Re-record your
|
|
18
|
+
> committed cassettes. (Cassettes without a `skillHash` are unaffected and keep replaying.)
|
|
19
|
+
> - **`RunResult.unanswered` renamed to `nonReproducibleAnswers`.** Consumers reading
|
|
20
|
+
> `result.unanswered` must switch to `result.nonReproducibleAnswers` (the field never held literally
|
|
21
|
+
> unanswered gates — it holds gates answered by a non-reproducible source).
|
|
22
|
+
> - **Cassette-level `readonlyFolderRoots` removed** in favor of per-entry
|
|
23
|
+
> `ManifestEntry.truncationReason`. No consumer action unless you parsed the cassette directly.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Two staleness-hash framing collisions (a false-negative — the risk the gate exists to prevent).**
|
|
28
|
+
`skillHash` folded raw file content after a `F:<path>\0` marker, so a file whose content embedded that
|
|
29
|
+
marker could hash identically to a two-file tree; it now folds the fixed-length content SHA
|
|
30
|
+
(self-delimiting, charset disjoint from the `F:`/`L:` prefixes). `contentSig` lacked a type marker and
|
|
31
|
+
used `:` between path and sha, so a file named `a:lnk` could collide with a symlink `a`; entries are now
|
|
32
|
+
type-prefixed (`F:`/`L:`) and NUL-framed, and the `L:` link line NUL-separates path from target. Only
|
|
33
|
+
fixable before the hash algorithm freezes at 1.0.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Cassette format v7 → v8** (`CONTENTSIG_ALGO` 3 → 4). See the framing fix above and the upgrade note.
|
|
38
|
+
- **`RunResult.unanswered` → `nonReproducibleAnswers`** — a self-admitted misnomer in the frozen
|
|
39
|
+
run-result envelope, renamed before the 1.0 freeze.
|
|
40
|
+
- **Body-less manifest entries now carry `ManifestEntry.truncationReason`** (`size` | `readonly` |
|
|
41
|
+
`unreadable`) instead of a cassette-level `readonlyFolderRoots` list — self-describing and
|
|
42
|
+
redaction-immune. Replay reads the reason to give `artifact_json` the precise remedy on every lane
|
|
43
|
+
(assert on a deliverable vs. raise `--max-artifact-bytes`).
|
|
44
|
+
- **`schema/run-result.json` completed** (added `fingerprint`, `userVisibleRoots`, `readonlyFolderRoots`,
|
|
45
|
+
`preRunPaths`, `toolResults`) and now strict-validated by a test, and the `cassette.v8.json`
|
|
46
|
+
`ManifestEntry` shape is fully described — so the schemas frozen at 1.0 match what ships. Corrected the
|
|
47
|
+
git-mode-default documentation (git is the default unless `COWORK_HARNESS_GITSET=0`).
|
|
48
|
+
|
|
9
49
|
## [0.25.0] — 2026-07-05
|
|
10
50
|
|
|
11
51
|
> **Upgrade notes.**
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ Only the committed-cassette `replay` above is fully self-contained. Live `run`/`
|
|
|
84
84
|
> - **Replay only (zero setup):** `cowork-harness replay <cassette>` — no token, no Docker, no agent. The command above.
|
|
85
85
|
> - **`protocol` (real model, no Docker):** needs only the auth token (item 3 below).
|
|
86
86
|
> - **Live `container` / `microvm` / `hostloop` / `cowork`:** needs Docker (or Lima for `microvm`), a staged agent, and the token — run `cowork-harness doctor` first.
|
|
87
|
-
> - **Invocation:** from a source checkout, `node dist/cli.js <cmd>` (or `npm link` to get the `cowork-harness` command); from a global install, `cowork-harness <cmd>`; the companion skill falls back to `npx cowork-harness@>=0.
|
|
87
|
+
> - **Invocation:** from a source checkout, `node dist/cli.js <cmd>` (or `npm link` to get the `cowork-harness` command); from a global install, `cowork-harness <cmd>`; the companion skill falls back to `npx cowork-harness@>=0.26.0`.
|
|
88
88
|
>
|
|
89
89
|
> *(One project, three names: the repo folder is `claude-cowork-headless-emulator`; the npm package, the CLI command, and the GitHub repo are all `cowork-harness`.)*
|
|
90
90
|
|
|
@@ -104,7 +104,7 @@ This repo ships a **companion skill** (`.claude/skills/cowork-harness/`) that te
|
|
|
104
104
|
/plugin install cowork-harness@cowork-harness
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
The skill **self-bootstraps the CLI**: if `cowork-harness` isn't on your PATH it falls back to `npx cowork-harness@>=0.
|
|
107
|
+
The skill **self-bootstraps the CLI**: if `cowork-harness` isn't on your PATH it falls back to `npx cowork-harness@>=0.26.0` (a version floor that fails loud rather than silently fetching a too-old CLI; Node ≥ 20). Tiers above `protocol` still need Docker/Lima and a Claude Desktop agent binary — see the prerequisites below.
|
|
108
108
|
|
|
109
109
|
It also follows the open [Agent Skills](https://github.com/vercel-labs/skills) spec, so it installs cross-editor (Cursor, Codex, OpenCode, …) by pointing the `npx skills` CLI at `.claude/skills/cowork-harness` in this repo. (Working *inside* this repo, the skill auto-loads as a project skill — no install needed.)
|
|
110
110
|
|
|
@@ -119,7 +119,7 @@ A global install is enough for CI `lint`, reading the teaching skill, and replay
|
|
|
119
119
|
To `run` the worked examples live or copy them as a starting point, use a source checkout. (The marketplace
|
|
120
120
|
skill install itself only pulls `.claude/skills/cowork-harness/` — SKILL.md + `references/` + `scenario.py`/
|
|
121
121
|
assertion keys, per `.claude-plugin/marketplace.json`'s `source` — not the rest of this table; the full set
|
|
122
|
-
above becomes available once the skill's first command self-bootstraps `npx cowork-harness@>=0.
|
|
122
|
+
above becomes available once the skill's first command self-bootstraps `npx cowork-harness@>=0.26.0` — see
|
|
123
123
|
[above](#drive-it-from-claude-code-companion-skill) — which pulls the same npm package as the global-install row.)
|
|
124
124
|
|
|
125
125
|
### Prerequisites for anything above `protocol` fidelity
|
package/SPEC.md
CHANGED
|
@@ -504,19 +504,25 @@ and a fail observed) always fails that batch, regardless of the numeric rate.
|
|
|
504
504
|
"egress": [{ "host","decision":"allow|deny" }],
|
|
505
505
|
"assertions":[{ "assertion": <Assertion>, "pass": bool, "message?": "string" }],
|
|
506
506
|
"subagents": [{ "toolUseId","parentToolUseId?","agentType","declaredTools":[],"toolsUsed":[] }],
|
|
507
|
-
"
|
|
507
|
+
"nonReproducibleAnswers?":[{ "question","chosen","by","rationale?","model?" }], // decisions answered by a non-deterministic/non-authoritative source (llm/external/human/first); scripted answers are excluded
|
|
508
508
|
"usage?": { "turns?": number, /* …SDK usage fields (input_tokens, output_tokens, etc.), pass-through */ },
|
|
509
509
|
"cost?": { "usd?": number, "raw?": {...} }, // usd = SDK's total_cost_usd for this invocation; raw = the api_metrics event payload (independent source)
|
|
510
510
|
"durationMs?": number,
|
|
511
|
+
"fingerprint?": { "baseline", "skillHash?", "skillSources?":[], "skillScope?":[], "sharedHash?", "contentSig?", "fileSigs?":[["relpath","contentSha"]], "fileSigsOmitted?": bool, "mode?": "git|raw", "agentScope?": "skill" }, // skill/plugin staleness fingerprint recorded at run time; lets `verify-run` detect a kept run whose gate snapshot predates a skill change
|
|
511
512
|
"outDir": "string",
|
|
512
513
|
"workDir?": "string", // the agent's working root (mnt/) inside the run dir
|
|
513
514
|
"outputsDir?": "string", // the user-visible deliverable mount (mnt/outputs)
|
|
515
|
+
"userVisibleRoots?": ["string"], // user-visible mount roots (relative to mnt/) — `outputs` plus each connected folder's resolved mount name; plugins excluded
|
|
516
|
+
"readonlyFolderRoots?": ["string"], // subset of userVisibleRoots that are read-only (mode:"r") connected-folder mounts — inputs, not deliverables; `artifacts` excludes them
|
|
517
|
+
"artifacts?": [{ "path","bytes" }], // files written under the user-visible roots (paths + sizes only — no content snapshot)
|
|
518
|
+
"preRunPaths?": ["string"], // workRoot-relative paths under the user-visible roots that existed BEFORE the agent ran — the `no_unexpected_files` baseline; absent when the tier didn't capture it (microvm) or the run predates the seam
|
|
514
519
|
"effectiveFidelity?": "string", // tier actually used (differs from `fidelity` when "cowork" resolved)
|
|
515
520
|
"nonDeterministic?": bool, // true if any decision came from a non-deterministic source → not reproducible
|
|
516
521
|
"gateProvenance?": { "total": number, "bySource": {…}, "gates": [{ "question","answeredBy","answer","model?" }] }, // how each AskUserQuestion gate was answered; informational (never fails the verdict); live/partial lane only (absent on replay)
|
|
517
522
|
"permissiveAutoAllow?": ["string"], // tools auto-allowed by cowork parity that real Cowork BLOCKS → green is NOT faithful
|
|
518
523
|
"staleness?": [{ "class": "baseline|skill|shared-root|format|unverifiable-baseline|unverifiable-skill|resolved-tier|unverifiable-tier", "message" }], // replay only; cassette-staleness findings, surfaced for a JSON gate. Non-failing by default (a stale but passing replay stays ok:true); `--strict` fails on every class, `--fail-on-skill-drift` on skill/shared-root/unverifiable-skill only. `resolved-tier` = a `fidelity: cowork` cassette's recorded effectiveFidelity no longer matches the tier the scenario's baseline (pinned `baseline:` or `latest`) resolves to today — the recording exercises the wrong tier; `unverifiable-tier` = the tier check couldn't run for a baseline-dependent (`fidelity: cowork`) cassette (no recorded effectiveFidelity, or its pinned baseline failed to load). Tier resolution is baseline-only (the CLAUDE_FORCE_HOST_LOOP env override is suppressed) so verify results can't differ across machines.
|
|
519
524
|
"skippedAssertions?": { "full": number, "partial": number }, // replay only; count of live-only assertions NOT evaluated (full = whole assertion skipped; partial = content half ran, fs/egress half dropped). The skipped ones are absent from `assertions[]`.
|
|
525
|
+
"toolResults?": [{ "toolUseId?","isError","text","assertText?" }], // tool-result text at assertion-fidelity cap (10 KB); backs tool_result_contains/tool_result_not_contains
|
|
520
526
|
"skillsInvoked?": ["string"], // Wave 1: skill/plugin ids invoked via the Skill tool_use event, call order, duplicates kept. Backs skill_triggered/no_skill_triggered.
|
|
521
527
|
"skillToolAvailable?": bool // Wave 1: whether the agent's init tool list included "Skill" — false ⇒ skill_triggered/no_skill_triggered fail as evidence-unavailable (agent-version drift)
|
|
522
528
|
}
|
|
@@ -641,9 +647,10 @@ nothing here is guaranteed; minor versions may break any surface — see [RELEAS
|
|
|
641
647
|
`findings` / `staleness` / `notes` / `version` / `error` channels. This is the machine output the
|
|
642
648
|
CI recipes and the packaged Action steer consumers to parse; renaming or removing a key is
|
|
643
649
|
breaking, adding one is not.
|
|
644
|
-
- **Cassette format** — the current `cassetteVersion` (**
|
|
645
|
-
Older-version cassettes (the retained
|
|
646
|
-
published) stay replayable; dropping a
|
|
650
|
+
- **Cassette format** — the current `cassetteVersion` (**8**, `schema/cassette.v8.json`) and its
|
|
651
|
+
verdict-modifier assertion keys. Older-version cassettes (the retained
|
|
652
|
+
`schema/cassette.v2/v3/v5/v6/v7.json` — no v4 schema was ever published) stay replayable; dropping a
|
|
653
|
+
still-emitted version's readability is breaking.
|
|
647
654
|
- **Control protocol** — `schema/protocol.v1.json` + the golden control-response vectors (§5).
|
|
648
655
|
- **Environment variables** — the documented `COWORK_HARNESS_*` knobs plus `COWORK_AGENT_BINARY` and
|
|
649
656
|
`COWORK_AGENT_IMAGE`. Renaming a documented var or changing its meaning is breaking.
|
package/dist/assert.js
CHANGED
|
@@ -166,7 +166,7 @@ function check(a, ctx) {
|
|
|
166
166
|
const results = [];
|
|
167
167
|
const ok = () => ({ pass: true });
|
|
168
168
|
const fail = (message) => ({ pass: false, message });
|
|
169
|
-
const truncated = ctx.truncatedPaths ?? new
|
|
169
|
+
const truncated = ctx.truncatedPaths ?? new Map();
|
|
170
170
|
if (a.transcript_contains !== undefined)
|
|
171
171
|
results.push(ctx.transcriptMissing
|
|
172
172
|
? fail(`evidence unavailable: transcript sidecar (run.jsonl) absent — cannot evaluate transcript_contains`)
|
|
@@ -522,8 +522,14 @@ function check(a, ctx) {
|
|
|
522
522
|
// where the real file is still on disk. (Existence keys stay green — existence is provable from
|
|
523
523
|
// the recorded hash — but content is genuinely absent, so this fails loud, never vacuous.)
|
|
524
524
|
const rel = relative(resolve(ctx.workRoot), file);
|
|
525
|
-
|
|
526
|
-
|
|
525
|
+
// Reason sources by lane: LIVE/verify-run derive read-only from the plan's `readonlyFolderRoots`
|
|
526
|
+
// (no manifest exists at eval time); REPLAY reads the per-entry `truncationReason` off the
|
|
527
|
+
// materialized manifest (`truncated.get(rel)`). Keeping both is complementary, not redundant.
|
|
528
|
+
const liveReadonly = (ctx.readonlyFolderRoots ?? []).some((pre) => rel === pre || rel.startsWith(pre + "/"));
|
|
529
|
+
const replayReason = truncated.get(rel); // undefined if not body-less on replay, or a pre-v8 entry with no reason
|
|
530
|
+
const isReadonlyInput = liveReadonly || replayReason === "readonly";
|
|
531
|
+
const isOverCap = replayReason === "size";
|
|
532
|
+
const bodyLess = truncated.has(rel) || liveReadonly;
|
|
527
533
|
if (!realFile) {
|
|
528
534
|
results.push(fail(`unsafe artifact_json path "${aj.artifact}" — symlink target escapes the work root`));
|
|
529
535
|
}
|
|
@@ -531,14 +537,16 @@ function check(a, ctx) {
|
|
|
531
537
|
results.push(fail(`artifact_json: file not found: ${aj.artifact} (under ${ctx.workRoot})`));
|
|
532
538
|
}
|
|
533
539
|
else if (bodyLess) {
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
540
|
+
// Precise remedy when the cause is known (read-only ⇒ assert on a deliverable; over-cap ⇒ raise
|
|
541
|
+
// the cap). A pre-v8 entry carries no reason ⇒ name both causes (we can't tell). "unreadable"
|
|
542
|
+
// also falls here — it's a record-time read failure, so the both-causes text is the safe hint.
|
|
543
|
+
const cause = isReadonlyInput
|
|
544
|
+
? `(read-only connected-folder input — its content is never captured; assert artifact_json on a deliverable instead)`
|
|
545
|
+
: isOverCap
|
|
546
|
+
? `(larger than the artifact-body cap — raise --max-artifact-bytes to capture it)`
|
|
547
|
+
: `(a read-only connected-folder input, or an artifact larger than the body cap — if an input, assert on a deliverable; if a large deliverable, raise --max-artifact-bytes)`;
|
|
538
548
|
results.push(fail(`evidence unavailable: artifact_json target "${aj.artifact}" was captured body-less ` +
|
|
539
|
-
|
|
540
|
-
? `(read-only connected-folder input — its content is never captured; assert artifact_json on a deliverable instead)`
|
|
541
|
-
: `(larger than the artifact-body cap — raise --max-artifact-bytes to capture it)`) +
|
|
549
|
+
cause +
|
|
542
550
|
` — content is not in the cassette, so it cannot be evaluated on replay`));
|
|
543
551
|
}
|
|
544
552
|
else {
|
package/dist/run/cassette.js
CHANGED
|
@@ -77,13 +77,14 @@ const MANIFEST_MAX_FILES = 2000;
|
|
|
77
77
|
// routes pre-v6 cassettes to a re-record (honest "algorithm changed" message, not "content changed").
|
|
78
78
|
// v7: NUL-byte entry separator in hashDir and computeContentSig (replaces `\n`) to prevent hash collisions
|
|
79
79
|
// for file paths containing newline characters.
|
|
80
|
-
export const CASSETTE_VERSION =
|
|
80
|
+
export const CASSETTE_VERSION = 8;
|
|
81
81
|
// The contentSig algorithm version. Bumped whenever computeContentSig's INPUT/encoding changes (the v6
|
|
82
82
|
// unification). `rehash` only byte-compares contentSig within the same algo version; across a bump it
|
|
83
83
|
// re-records. Derived from cassetteVersion: < 6 ⇒ algo 1 (legacy), ≥ 6 ⇒ algo 2 (unified),
|
|
84
|
-
// ≥ 7 ⇒ algo 3 (NUL-byte separator)
|
|
85
|
-
|
|
86
|
-
const
|
|
84
|
+
// ≥ 7 ⇒ algo 3 (NUL-byte separator), ≥ 8 ⇒ algo 4 (skillHash folds fixed-length content shas +
|
|
85
|
+
// contentSig/link entries are type-prefixed & NUL-framed — closes unframed-concatenation collisions).
|
|
86
|
+
const CONTENTSIG_ALGO = 4;
|
|
87
|
+
const contentSigAlgoOf = (cassetteVersion) => (cassetteVersion >= 8 ? 4 : cassetteVersion >= 7 ? 3 : cassetteVersion >= 6 ? 2 : 1);
|
|
87
88
|
/** Canonical URL of the JSON Schema for this cassette format version.
|
|
88
89
|
* Appears in every written cassette as `$schema` so editors and unfamiliar readers
|
|
89
90
|
* can discover what tool produced the file and what the format means. */
|
|
@@ -149,18 +150,23 @@ export function buildManifest(workRoot, cap, roots = ["outputs", ".projects"], b
|
|
|
149
150
|
abs = containedPath(workRoot, path);
|
|
150
151
|
}
|
|
151
152
|
catch {
|
|
152
|
-
return { path, bytes, sha256: "", truncated: true };
|
|
153
|
+
return { path, bytes, sha256: "", truncated: true, truncationReason: "unreadable" };
|
|
153
154
|
}
|
|
154
155
|
let buf;
|
|
155
156
|
try {
|
|
156
157
|
buf = readFileSync(abs);
|
|
157
158
|
}
|
|
158
159
|
catch {
|
|
159
|
-
return { path, bytes, sha256: "", truncated: true };
|
|
160
|
+
return { path, bytes, sha256: "", truncated: true, truncationReason: "unreadable" };
|
|
160
161
|
}
|
|
161
162
|
const sha256 = createHash("sha256").update(buf).digest("hex");
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
// truncationReason names WHY the body is absent so replay can give the precise remedy without a
|
|
164
|
+
// cassette-level roots list: "readonly" (a mode:r input — assert on a deliverable) vs "size" (over
|
|
165
|
+
// the body cap — raise --max-artifact-bytes). "unreadable" is the catch branches above.
|
|
166
|
+
if (isBodyLess(path))
|
|
167
|
+
return { path, bytes, sha256, truncated: true, truncationReason: "readonly" };
|
|
168
|
+
if (buf.length > limit)
|
|
169
|
+
return { path, bytes, sha256, truncated: true, truncationReason: "size" };
|
|
164
170
|
// store an encoding marker. UTF-8-safe bodies stay text (readable cassettes); binary bodies go
|
|
165
171
|
// base64 so the record→replay round-trip is byte-exact and the sha256 verify stays valid.
|
|
166
172
|
if (isLosslessUtf8(buf))
|
|
@@ -183,7 +189,9 @@ function decodeBody(e) {
|
|
|
183
189
|
* against its recorded sha256 (over the decoded RAW bytes) — a mismatch fails replay (throws). */
|
|
184
190
|
export function materializeManifest(entries, roots = ["outputs", ".projects"]) {
|
|
185
191
|
const workRoot = mkdtempSync(join(tmpdir(), "cwh-replay-"));
|
|
186
|
-
|
|
192
|
+
// path → why the body is absent (from the entry's truncationReason; `undefined` on a pre-v8 entry that
|
|
193
|
+
// had no reason). `.has()` still means "is body-less"; `.get()` gives the reason for the precise remedy.
|
|
194
|
+
const truncatedPaths = new Map();
|
|
187
195
|
for (const e of entries) {
|
|
188
196
|
const abs = containedPath(workRoot, e.path); // reject absolute / `..` / out-of-root before writing
|
|
189
197
|
const raw = decodeBody(e); // decode per the encoding marker
|
|
@@ -198,7 +206,7 @@ export function materializeManifest(entries, roots = ["outputs", ".projects"]) {
|
|
|
198
206
|
mkdirSync(dirname(abs), { recursive: true });
|
|
199
207
|
writeFileSync(abs, raw);
|
|
200
208
|
if (e.truncated)
|
|
201
|
-
truncatedPaths.
|
|
209
|
+
truncatedPaths.set(relative(resolve(workRoot), abs), e.truncationReason);
|
|
202
210
|
}
|
|
203
211
|
return { workRoot, prefixes: roots, truncatedPaths };
|
|
204
212
|
}
|
|
@@ -414,9 +422,6 @@ export function scanCassette(cassette, allow) {
|
|
|
414
422
|
// cassettes`. Scan them too, prefixed `metadata:` so a reviewer knows redaction here ALSO rewrites
|
|
415
423
|
// structural paths, distinct from free-text findings in the transcript/deliverable.
|
|
416
424
|
(cassette.userVisibleRoots ?? []).forEach((r, i) => findings.push(...scanText(r, `metadata:userVisibleRoots[${i}]`, allow, FULL)));
|
|
417
|
-
// readonlyFolderRoots is a subset of userVisibleRoots (same strings), but it's a separately-persisted
|
|
418
|
-
// field — scan it too so a reviewer can't be surprised by an unscanned persisted string.
|
|
419
|
-
(cassette.readonlyFolderRoots ?? []).forEach((r, i) => findings.push(...scanText(r, `metadata:readonlyFolderRoots[${i}]`, allow, FULL)));
|
|
420
425
|
findings.push(...scanText(cassette.scenario.name ?? "", "metadata:scenario.name", allow, FULL));
|
|
421
426
|
findings.push(...scanText(cassette.scenario.session ?? "", "metadata:scenario.session", allow, FULL));
|
|
422
427
|
if (cassette.scenarioSource)
|
|
@@ -1024,15 +1029,10 @@ export function redactCassette(cassette, policy) {
|
|
|
1024
1029
|
}
|
|
1025
1030
|
}
|
|
1026
1031
|
const redactedPreRunPaths = cassette.preRunPaths?.map((p) => redactText(p, policy));
|
|
1027
|
-
// readonlyFolderRoots is a subset of userVisibleRoots; redact it with the same context-free redactText
|
|
1028
|
-
// so the prefix relationship with the (redacted) artifact paths is preserved. The `...cassette` spread
|
|
1029
|
-
// would otherwise carry the UNREDACTED set through.
|
|
1030
|
-
const redactedReadonly = cassette.readonlyFolderRoots?.map((r) => redactText(r, policy));
|
|
1031
1032
|
return {
|
|
1032
1033
|
...cassette,
|
|
1033
1034
|
scenario,
|
|
1034
1035
|
userVisibleRoots: redactedRoots,
|
|
1035
|
-
readonlyFolderRoots: redactedReadonly,
|
|
1036
1036
|
artifacts: redactedArtifacts,
|
|
1037
1037
|
preRunPaths: redactedPreRunPaths,
|
|
1038
1038
|
events: cassette.events.map((l) => redactJsonLine(l, policy)),
|
|
@@ -1321,11 +1321,15 @@ export function redactionPreflightMessage(items) {
|
|
|
1321
1321
|
* verify-run, and replay (evidence-unavailable, see assert.ts) — so they are neither an asymmetry nor a
|
|
1322
1322
|
* "too large" problem. Paths are normalized through `resolve` so `./outputs/x.json` and `outputs/x.json`
|
|
1323
1323
|
* join cleanly against the manifest's walk paths. */
|
|
1324
|
-
export function artifactJsonTargetsTruncated(scenario, workRoot, artifacts
|
|
1325
|
-
|
|
1324
|
+
export function artifactJsonTargetsTruncated(scenario, workRoot, artifacts) {
|
|
1325
|
+
// Flag ONLY size-truncated entries (`truncationReason === "size"`) — the genuine green-record/red-replay
|
|
1326
|
+
// case whose remedy is "raise the cap". "readonly"/"unreadable" are excluded: raising the cap can't
|
|
1327
|
+
// capture them, and artifact_json against one already fails loud-and-symmetrically (evidence-unavailable,
|
|
1328
|
+
// assert.ts). (A pre-v8 entry with no reason is not flagged — this guard only runs at record time, where
|
|
1329
|
+
// buildManifest always sets the reason.)
|
|
1326
1330
|
const truncatedAbs = new Set();
|
|
1327
1331
|
for (const a of artifacts)
|
|
1328
|
-
if (a.truncated &&
|
|
1332
|
+
if (a.truncated && a.truncationReason === "size")
|
|
1329
1333
|
truncatedAbs.add(resolve(workRoot, a.path));
|
|
1330
1334
|
if (truncatedAbs.size === 0)
|
|
1331
1335
|
return [];
|
|
@@ -1868,7 +1872,7 @@ async function recordScenarioObject(scenario, opts, extraPolicyDirs = []) {
|
|
|
1868
1872
|
// replay (no committed body). Surface that record→replay asymmetry NOW, at its cause, instead of letting a
|
|
1869
1873
|
// green record produce a red replay in CI. Honor --allow-failing (warn, don't block) like the verdict gate.
|
|
1870
1874
|
if (result.workDir) {
|
|
1871
|
-
const truncatedAsserted = artifactJsonTargetsTruncated(scenario, result.workDir, artifacts
|
|
1875
|
+
const truncatedAsserted = artifactJsonTargetsTruncated(scenario, result.workDir, artifacts);
|
|
1872
1876
|
if (truncatedAsserted.length) {
|
|
1873
1877
|
const cap = opts.maxArtifactBytes ?? defaultBodyCap();
|
|
1874
1878
|
const msg = `artifact_json asserts artifact(s) too large to commit (>${cap} B, stored hash-only): ${truncatedAsserted.join(", ")} — ` +
|
|
@@ -1890,11 +1894,10 @@ async function recordScenarioObject(scenario, opts, extraPolicyDirs = []) {
|
|
|
1890
1894
|
effectiveFidelity: result.effectiveFidelity,
|
|
1891
1895
|
artifacts,
|
|
1892
1896
|
userVisibleRoots: recordRoots,
|
|
1893
|
-
//
|
|
1894
|
-
//
|
|
1895
|
-
|
|
1896
|
-
//
|
|
1897
|
-
// carrying preRunPaths never hits replay's legacy-roots fallback.
|
|
1897
|
+
// (v8: no cassette-level readonlyFolderRoots — the read-only reason rides per-entry on
|
|
1898
|
+
// ManifestEntry.truncationReason, set by buildManifest above from RunResult.readonlyFolderRoots.)
|
|
1899
|
+
// co-present with userVisibleRoots by construction — a cassette carrying preRunPaths never hits
|
|
1900
|
+
// replay's legacy-roots fallback.
|
|
1898
1901
|
preRunPaths: result.preRunPaths,
|
|
1899
1902
|
// persist the authored scenario source file RELATIVE to the cassette dir (relocatable, no
|
|
1900
1903
|
// absolute host path) so `--rerecord-stale` re-records from the edited YAML even when name ≠ filename.
|
|
@@ -2713,7 +2716,7 @@ export async function replayCassette(cassette, hooks = [], opts = {}) {
|
|
|
2713
2716
|
}
|
|
2714
2717
|
const { workRoot: replayWorkRoot, prefixes: replayPrefixes, truncatedPaths: replayTruncatedPaths, } = manifestKeys.length
|
|
2715
2718
|
? materializeManifest(cassette.artifacts, cassette.userVisibleRoots ?? ["outputs", ".projects"])
|
|
2716
|
-
: { workRoot: "", prefixes: [], truncatedPaths: new
|
|
2719
|
+
: { workRoot: "", prefixes: [], truncatedPaths: new Map() };
|
|
2717
2720
|
// materializeManifest created a temp dir (`replayWorkRoot`) above; everything below uses it and
|
|
2718
2721
|
// then returns. Wrap the rest in try/finally so the temp dir is removed on every exit path (normal
|
|
2719
2722
|
// return OR a throw from evaluate/assert building) — otherwise `cwh-replay-*` dirs leak under tmpdir
|
|
@@ -2780,9 +2783,10 @@ export async function replayCassette(cassette, hooks = [], opts = {}) {
|
|
|
2780
2783
|
result: rec.result,
|
|
2781
2784
|
workRoot: replayWorkRoot,
|
|
2782
2785
|
userVisiblePrefixes: replayPrefixes,
|
|
2783
|
-
//
|
|
2784
|
-
//
|
|
2785
|
-
|
|
2786
|
+
// Replay reads the body-less REASON per-entry from the materialized manifest (truncatedPaths, a
|
|
2787
|
+
// Map<path, reason>) — NOT a cassette-level roots list (removed in v8). Live/verify-run alone use
|
|
2788
|
+
// readonlyFolderRoots (they have no manifest at eval time), so it's empty here.
|
|
2789
|
+
readonlyFolderRoots: [],
|
|
2786
2790
|
preRunPaths: cassette.preRunPaths,
|
|
2787
2791
|
outputsDeletes: [],
|
|
2788
2792
|
questions: rec.questions,
|
|
@@ -2896,7 +2900,7 @@ export async function replayCassette(cassette, hooks = [], opts = {}) {
|
|
|
2896
2900
|
egress: [],
|
|
2897
2901
|
assertions,
|
|
2898
2902
|
subagents: rec.subagents,
|
|
2899
|
-
|
|
2903
|
+
nonReproducibleAnswers: rec.unanswered,
|
|
2900
2904
|
// Wave 0 seam: the live/success/partial assemblers already passed these through; replay never did,
|
|
2901
2905
|
// so a cassette that recorded usage/cost silently dropped it on replay. re-drive (rec) recomputes
|
|
2902
2906
|
// them deterministically from the same events, so this is a content key, not a live-only one.
|
package/dist/run/execute.js
CHANGED
|
@@ -780,7 +780,7 @@ export async function executeScenario(scenario, opts = {}) {
|
|
|
780
780
|
assertions,
|
|
781
781
|
toolResults: record.toolResults,
|
|
782
782
|
subagents: record.subagents,
|
|
783
|
-
|
|
783
|
+
nonReproducibleAnswers: record.unanswered,
|
|
784
784
|
usage: record.usage,
|
|
785
785
|
cost: record.cost,
|
|
786
786
|
skillsInvoked: record.skillsInvoked,
|
|
@@ -948,7 +948,7 @@ export function buildPartialResult(args) {
|
|
|
948
948
|
assertions: [],
|
|
949
949
|
toolResults: record.toolResults,
|
|
950
950
|
subagents: record.subagents,
|
|
951
|
-
|
|
951
|
+
nonReproducibleAnswers: record.unanswered,
|
|
952
952
|
usage: record.usage,
|
|
953
953
|
cost: record.cost,
|
|
954
954
|
skillsInvoked: record.skillsInvoked,
|
package/dist/run/renderer.js
CHANGED
|
@@ -282,7 +282,7 @@ function renderGateProvenance(r, plan, write) {
|
|
|
282
282
|
write(` ${dim(plan, line)}\n`);
|
|
283
283
|
}
|
|
284
284
|
function renderAnswerHints(r, plan, write) {
|
|
285
|
-
const u = r.
|
|
285
|
+
const u = r.nonReproducibleAnswers ?? [];
|
|
286
286
|
if (!u.length)
|
|
287
287
|
return;
|
|
288
288
|
// partition by the REAL `by:` vocabulary (decider.ts:25 — scripted|cowork|strict|human|agent|
|
package/dist/run/skill-hash.js
CHANGED
|
@@ -125,7 +125,8 @@ function hashDir(dir, hash, errors, rel = "", accept, onFile, root = dir) {
|
|
|
125
125
|
continue;
|
|
126
126
|
}
|
|
127
127
|
const targetRel = relative(rootResolved, resolvedTarget).split(sep).join("/");
|
|
128
|
-
hash.update(`L:${relPath}
|
|
128
|
+
hash.update(`L:${relPath}\0${targetRel}\0`); // link structure; a re-point changes the digest. NUL-separate
|
|
129
|
+
// path from target (not ` -> `): otherwise `a`->`b -> c` and `a -> b`->`c` both fold `L:a -> b -> c\0`.
|
|
129
130
|
if (onFile)
|
|
130
131
|
onFile(relPath, `lnk:${targetRel}`);
|
|
131
132
|
continue;
|
|
@@ -181,9 +182,15 @@ function hashDir(dir, hash, errors, rel = "", accept, onFile, root = dir) {
|
|
|
181
182
|
/* not valid JSON — fall through to raw-byte hashing */
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
|
-
|
|
185
|
+
// Fold the fixed-length content SHA, NOT the raw content: raw bytes have no length/terminator, so
|
|
186
|
+
// `F:a\0<A>F:b\0<B>` would collide with a single file `a` whose content is `<A>F:b\0<B>` (a staleness
|
|
187
|
+
// false-negative). A 64-hex sha is self-delimiting and its charset is disjoint from the `F:`/`L:`/`D:`
|
|
188
|
+
// prefixes, closing the unframed-concatenation hole. Hoisted out of `if (onFile)` so the digest is
|
|
189
|
+
// always folded (the no-sink path must fold the same bytes).
|
|
190
|
+
const contentSha = createHash("sha256").update(hashedContent).digest("hex");
|
|
191
|
+
hash.update(contentSha);
|
|
185
192
|
if (onFile)
|
|
186
|
-
onFile(relPath,
|
|
193
|
+
onFile(relPath, contentSha);
|
|
187
194
|
}
|
|
188
195
|
}
|
|
189
196
|
}
|
|
@@ -295,7 +302,10 @@ function scopedAccept(keep, dirSkills, scopeAgents) {
|
|
|
295
302
|
export function computeContentSig(dirs, scopeSkills, sessionIgnore) {
|
|
296
303
|
if (dirs.length === 0)
|
|
297
304
|
return undefined;
|
|
298
|
-
|
|
305
|
+
// Type-prefix + NUL-separate path from sha: without them, a file named `a:lnk` with content-sha
|
|
306
|
+
// `<64hex>` collides with a symlink `a` -> target `<64hex>` (both `a:lnk:<64hex>`). `e.sha` starts with
|
|
307
|
+
// `lnk:` only for links (a 64-hex file sha cannot — l/n/k/: are non-hex), so it disambiguates the kind.
|
|
308
|
+
const entries = skillHashEntries(dirs, scopeSkills, sessionIgnore).map((e) => e.sha.startsWith("lnk:") ? `L:${e.path}\0${e.sha}` : `F:${e.path}\0${e.sha}`);
|
|
299
309
|
if (entries.length === 0)
|
|
300
310
|
return undefined;
|
|
301
311
|
return createHash("sha256").update(entries.join("\0")).digest("hex");
|
package/docs/cassette.md
CHANGED
|
@@ -48,18 +48,17 @@ re-record). `expect_denied`/filesystem/egress keys are sourced but stay live-onl
|
|
|
48
48
|
|
|
49
49
|
```jsonc
|
|
50
50
|
{
|
|
51
|
-
"cassetteVersion":
|
|
51
|
+
"cassetteVersion": 8, // format version; ABSENT = legacy (0); a FUTURE version warns
|
|
52
52
|
"scenario": { /* Scenario object — same schema as the .yaml */ },
|
|
53
53
|
"events": [ /* JSON lines from events.jsonl (child→driver stdout) */ ],
|
|
54
54
|
"controlOut": [ /* JSON lines from control-out.jsonl (driver→child control_responses) */ ],
|
|
55
55
|
"userVisibleRoots": ["outputs", "myproject"], // visible roots = outputs + each connected folder's mount name (its basename; `.projects` is the pre-1.14271.0 legacy fallback)
|
|
56
|
-
"readonlyFolderRoots": ["carta-folder"], // the mode:r subset of userVisibleRoots (body-less); lets replay give artifact_json the precise remedy. Optional; absent when no read-only folder
|
|
57
56
|
"artifacts": [ // snapshot of outputs/ + connected folders (optional)
|
|
58
57
|
{ "path": "outputs/x.json", "bytes": 24, "sha256": "…", "body": "{…}" }, // body inlined ≤ 64 KiB
|
|
59
|
-
{ "path": "outputs/big.bin", "bytes": 9e6, "sha256": "…", "truncated": true }, // oversized → hash-only
|
|
60
|
-
{ "path": "carta-folder/input.xlsx", "bytes": 4096, "sha256": "…", "truncated": true } // mode:r connected-folder INPUT → body-less (see below), regardless of size
|
|
58
|
+
{ "path": "outputs/big.bin", "bytes": 9e6, "sha256": "…", "truncated": true, "truncationReason": "size" }, // oversized → hash-only (raise --max-artifact-bytes)
|
|
59
|
+
{ "path": "carta-folder/input.xlsx", "bytes": 4096, "sha256": "…", "truncated": true, "truncationReason": "readonly" } // mode:r connected-folder INPUT → body-less (see below), regardless of size
|
|
61
60
|
],
|
|
62
|
-
"fingerprint": { "baseline": "1.15962.1", "skillHash": "…", "mode": "git", "contentSig": "…", "fileSigs": [["skills/x/SKILL.md", "…"]], "skillSources": ["…"] }, // staleness tripwire (v5: fileSigs only; v6: mode + git default; v7: NUL-delimited hash entries)
|
|
61
|
+
"fingerprint": { "baseline": "1.15962.1", "skillHash": "…", "mode": "git", "contentSig": "…", "fileSigs": [["skills/x/SKILL.md", "…"]], "skillSources": ["…"] }, // staleness tripwire (v5: fileSigs only; v6: mode + git default; v7: NUL-delimited hash entries; v8: folds fixed-length content shas + type-prefixed/NUL-framed entries)
|
|
63
62
|
"authoring": { "nonDeterministic": true, "channel": "decider-dir" } // present ONLY when a live decider answered ≥1 gate (see §Answering gates during recording); re-record may drift, replay is still deterministic
|
|
64
63
|
}
|
|
65
64
|
```
|
|
@@ -390,7 +389,10 @@ possible.
|
|
|
390
389
|
- **`recorded under an older hash format (vN → vM)`** — format upgrade; re-record once and the message
|
|
391
390
|
goes away. (Cassettes recorded before format **v6** all need one re-record — see the boundary note below.
|
|
392
391
|
Cassettes recorded at **v6** need one re-record after upgrading to the v7 format, which switched the
|
|
393
|
-
hash-entry delimiter from `\n` to `\0`.
|
|
392
|
+
hash-entry delimiter from `\n` to `\0`. Cassettes recorded at **v7** need one re-record after upgrading
|
|
393
|
+
to **v8**, which folds fixed-length content shas and type-prefixes/NUL-frames the hash entries — a v7
|
|
394
|
+
fingerprint is non-comparable, so `rehash` routes v7 cassettes to re-record. A cassette that carries no
|
|
395
|
+
`skillHash` is unaffected and keeps replaying.)
|
|
394
396
|
- **`skill files changed since record — N changed (path, …)`** — the **exact** changed/added/removed file(s),
|
|
395
397
|
from the per-file manifest (`fileSigs`). For a scoped cassette the drift is attributed **per bucket** by the
|
|
396
398
|
actual changed paths: a `shared root changed (scope: skills/x) [N changed (…)]` message for shared-dependency
|
|
@@ -13,7 +13,7 @@ replay path end-to-end, including:
|
|
|
13
13
|
|
|
14
14
|
Run it with:
|
|
15
15
|
|
|
16
|
-
> Assumes the `cowork-harness` CLI is available — from a source checkout run `npm ci && npm run build && npm link` first, or `npm i -g cowork-harness@>=0.
|
|
16
|
+
> Assumes the `cowork-harness` CLI is available — from a source checkout run `npm ci && npm run build && npm link` first, or `npm i -g cowork-harness@>=0.26.0`. (`replay` itself needs nothing else — no token, no Docker.)
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
cowork-harness replay examples/replays/example-pdf-skill.cassette.json
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/yaniv-golan/cowork-harness/main/schema/cassette.
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/yaniv-golan/cowork-harness/main/schema/cassette.v8.json",
|
|
3
3
|
"generator": "cowork-harness",
|
|
4
|
-
"cassetteVersion":
|
|
4
|
+
"cassetteVersion": 8,
|
|
5
5
|
"scenario": {
|
|
6
6
|
"name": "example-pdf-skill",
|
|
7
7
|
"baseline": "latest",
|
|
@@ -46,28 +46,31 @@
|
|
|
46
46
|
},
|
|
47
47
|
"events": [
|
|
48
48
|
"{\"type\":\"control_response\",\"response\":{\"subtype\":\"success\",\"request_id\":\"init-1\",\"response\":{\"commands\":[{\"name\":\"deep-research\",\"description\":\"Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report. (dynamic workflow)\",\"argumentHint\":\"\"},{\"name\":\"my-pdf-skill\",\"description\":\"(my-pdf-skill) Example skill under test. Summarize a PDF and write action items.\",\"argumentHint\":\"\"},{\"name\":\"clear\",\"description\":\"Start a new session with empty context; previous session stays on disk (resumable with /resume)\",\"argumentHint\":\"[name]\",\"aliases\":[\"reset\",\"new\"]},{\"name\":\"compact\",\"description\":\"Free up context by summarizing the conversation so far\",\"argumentHint\":\"<optional custom summarization instructions>\"},{\"name\":\"config\",\"description\":\"Set a setting by key\",\"argumentHint\":\"key=value\",\"aliases\":[\"settings\"]},{\"name\":\"context\",\"description\":\"Show current context usage\",\"argumentHint\":\"\"},{\"name\":\"heapdump\",\"description\":\"Dump the JS heap to ~/Desktop\",\"argumentHint\":\"\"},{\"name\":\"init\",\"description\":\"Initialize a new CLAUDE.md file with codebase documentation\",\"argumentHint\":\"\"},{\"name\":\"reload-skills\",\"description\":\"Pick up skills added or changed on disk during this session\",\"argumentHint\":\"\"},{\"name\":\"review\",\"description\":\"Review a GitHub pull request; for your working diff use /code-review\",\"argumentHint\":\"[pr number]\"},{\"name\":\"security-review\",\"description\":\"Complete a security review of the pending changes on the current branch\",\"argumentHint\":\"\"},{\"name\":\"usage\",\"description\":\"Show session cost, plan usage, and what's contributing to your limits\",\"argumentHint\":\"\",\"aliases\":[\"cost\",\"stats\"]},{\"name\":\"insights\",\"description\":\"Generate a report analyzing your Claude Code sessions\",\"argumentHint\":\"\"},{\"name\":\"goal\",\"description\":\"Set a goal — keep working until the condition is met\",\"argumentHint\":\"\"},{\"name\":\"team-onboarding\",\"description\":\"Help teammates ramp on Claude Code with a guide from your usage\",\"argumentHint\":\"\"}],\"agents\":[{\"name\":\"claude\",\"description\":\"Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed.\"},{\"name\":\"claude-code-guide\",\"description\":\"Use this agent when the user asks questions (\\\"Can Claude...\\\", \\\"Does Claude...\\\", \\\"How do I...\\\") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage.\",\"model\":\"haiku\"},{\"name\":\"Explore\",\"description\":\"Fast read-only search agent for locating code. Use it to find files by pattern (eg. \\\"src/components/**/*.tsx\\\"), grep for symbols or keywords (eg. \\\"API endpoints\\\"), or answer \\\"where is X defined / which files reference Y.\\\" Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: \\\"quick\\\" for a single targeted lookup, \\\"medium\\\" for moderate exploration, or \\\"very thorough\\\" to search across multiple locations and naming conventions.\",\"model\":\"haiku\"},{\"name\":\"general-purpose\",\"description\":\"General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.\"},{\"name\":\"Plan\",\"description\":\"Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs.\"},{\"name\":\"statusline-setup\",\"description\":\"Use this agent to configure the user's Claude Code status line setting.\",\"model\":\"sonnet\"}],\"output_style\":\"default\",\"available_output_styles\":[\"default\",\"Proactive\",\"Explanatory\",\"Learning\"],\"models\":[{\"value\":\"default\",\"resolvedModel\":\"claude-sonnet-5\",\"displayName\":\"Default (recommended)\",\"description\":\"Sonnet 5 · Efficient for routine tasks\",\"supportsEffort\":true,\"supportedEffortLevels\":[\"low\",\"medium\",\"high\",\"xhigh\",\"max\"],\"supportsAdaptiveThinking\":true,\"supportsAutoMode\":true},{\"value\":\"sonnet\",\"resolvedModel\":\"claude-sonnet-5\",\"displayName\":\"Sonnet\",\"description\":\"Sonnet 5 · Efficient for routine tasks\",\"supportsEffort\":true,\"supportedEffortLevels\":[\"low\",\"medium\",\"high\",\"xhigh\",\"max\"],\"supportsAdaptiveThinking\":true,\"supportsAutoMode\":true},{\"value\":\"opus\",\"resolvedModel\":\"claude-opus-4-8\",\"displayName\":\"Opus\",\"description\":\"Opus 4.8 · Best for everyday, complex tasks\",\"supportsEffort\":true,\"supportedEffortLevels\":[\"low\",\"medium\",\"high\",\"xhigh\",\"max\"],\"supportsAdaptiveThinking\":true,\"supportsFastMode\":true,\"supportsAutoMode\":true},{\"value\":\"haiku\",\"resolvedModel\":\"claude-haiku-4-5-20251001\",\"displayName\":\"Haiku\",\"description\":\"Haiku 4.5 · Fastest for quick answers\"}],\"account\":{\"tokenSource\":\"CLAUDE_CODE_OAUTH_TOKEN\",\"apiProvider\":\"firstParty\"},\"pid\":1}}}",
|
|
49
|
-
"{\"type\":\"system\",\"subtype\":\"init\",\"cwd\":\"/sessions/
|
|
50
|
-
"{\"type\":\"system\",\"subtype\":\"thinking_tokens\",\"estimated_tokens\":50,\"estimated_tokens_delta\":50,\"uuid\":\"
|
|
51
|
-
"{\"type\":\"system\",\"subtype\":\"thinking_tokens\",\"estimated_tokens\":
|
|
52
|
-
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"
|
|
53
|
-
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"
|
|
54
|
-
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"
|
|
55
|
-
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"
|
|
56
|
-
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Base directory for this skill: /sessions/
|
|
57
|
-
"{\"type\":\"
|
|
58
|
-
"{\"type\":\"
|
|
59
|
-
"{\"type\":\"
|
|
60
|
-
"{\"type\":\"
|
|
61
|
-
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"
|
|
62
|
-
"{\"type\":\"
|
|
63
|
-
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"
|
|
64
|
-
"{\"type\":\"
|
|
65
|
-
"{\"type\":\"
|
|
66
|
-
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"
|
|
67
|
-
"{\"type\":\"
|
|
49
|
+
"{\"type\":\"system\",\"subtype\":\"init\",\"cwd\":\"/sessions/local_2ckhgi8bsn\",\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"tools\":[\"Task\",\"AskUserQuestion\",\"Bash\",\"Edit\",\"Glob\",\"Grep\",\"NotebookEdit\",\"Read\",\"Skill\",\"TaskCreate\",\"TaskGet\",\"TaskList\",\"TaskStop\",\"TaskUpdate\",\"ToolSearch\",\"WebFetch\",\"WebSearch\",\"Write\"],\"mcp_servers\":[],\"model\":\"claude-opus-4-8\",\"permissionMode\":\"default\",\"slash_commands\":[\"deep-research\",\"my-pdf-skill:my-pdf-skill\",\"clear\",\"compact\",\"config\",\"context\",\"heapdump\",\"init\",\"reload-skills\",\"review\",\"security-review\",\"usage\",\"insights\",\"goal\",\"team-onboarding\"],\"apiKeySource\":\"none\",\"claude_code_version\":\"2.1.197\",\"output_style\":\"default\",\"agents\":[\"claude\",\"claude-code-guide\",\"Explore\",\"general-purpose\",\"Plan\",\"statusline-setup\"],\"skills\":[\"deep-research\",\"my-pdf-skill:my-pdf-skill\"],\"plugins\":[{\"name\":\"my-pdf-skill\",\"path\":\"/sessions/local_2ckhgi8bsn/mnt/.local-plugins/marketplaces/local-desktop-app-uploads/my-pdf-skill\",\"source\":\"my-pdf-skill@inline\"}],\"analytics_disabled\":false,\"product_feedback_disabled\":false,\"uuid\":\"97aa4c66-d25d-41e5-9322-3a1e658e5dc7\",\"memory_paths\":{\"auto\":\"/sessions/local_2ckhgi8bsn/mnt/.claude/projects/-sessions-local-2ckhgi8bsn/memory/\"},\"fast_mode_state\":\"off\"}",
|
|
50
|
+
"{\"type\":\"system\",\"subtype\":\"thinking_tokens\",\"estimated_tokens\":50,\"estimated_tokens_delta\":50,\"uuid\":\"610b4a90-abcc-4c76-a224-c9670e90e94b\",\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\"}",
|
|
51
|
+
"{\"type\":\"system\",\"subtype\":\"thinking_tokens\",\"estimated_tokens\":138,\"estimated_tokens_delta\":88,\"uuid\":\"2c44e99c-4ae1-413b-8d64-77ecfb8ac350\",\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\"}",
|
|
52
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01ESWwohWenfu2P6f1md7zaG\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"Ep4ECmMIDxgCKkD6xJtwa3QbT+Ll2Dj06vKwNfO6Qh9i9ftukHtjkuM92W9orMX4KUsdkN86MXPfAxM9qydsEL9LAyCIFNCuMIb9Mg9jbGF1ZGUtb3B1cy00LTg4AEIIdGhpbmtpbmcSDMbyCvivxGT5atJDsxoMAyC9RLeZ+MJ1xKxqIjDuKGOTp8e6PL9ZdqbYGcV65Lc5udkuS1rTl9zRN4ws/i9sckHiHjLG2M30rv8Sorgq6AI/zreKlp+m5T2hFejCh+BirC8V+6rrsYKX92Qu0rN37RepFNO6EwOzboGsVkDf3wtAuVVa8vhlBo7nMwc2Bn4y7xWgkDSq0te9Q7Mi2RrHPlN+xpEhxAAm2Bx+QEN3N1x3V5GZDDkXYSnFdLWyScaSjktwfpVbvPGWCs3Fk1RJRSxab+A2LVAXrauPucs7xb3Bd7pYCKzZQPFNr8x2Aw8Sv5g/vixbgIXiZMToS/jjZg6sCYTWsYLzSqANpz8GvQNHJYSELUlIAytqVEYfMq268jC1369OPvi/Fq0Bj4frAU7O/dxaTa4hQY/hQ3mYiNHPWyc8c2B8GWfP+lDnMNHINN5kJuLHjXPYUxfIwXWSoDj2DAJvT5UCJ68Jni4y9E3TsDJtZsgImCgsoqa4XkvSIeJsLnRmmCkkYWY3gK66BtJowOgB+NVKmsZsRVMEiygeaLFwHy0rY8EjYEV1ZBuWayE1rndqW5YYAQ==\"}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1185,\"cache_creation_input_tokens\":16350,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":16350},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"e0e6cab3-6ff8-4d31-a7d2-5da6e0569e0a\",\"request_id\":\"req_011CcihszwEGjxG9JfCdHiUY\"}",
|
|
53
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01ESWwohWenfu2P6f1md7zaG\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I'll use the dedicated PDF skill for this since it matches the task exactly. Let me start.\"}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1185,\"cache_creation_input_tokens\":16350,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":16350},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"50bb41ff-2098-4694-a06a-d34fa3cb2414\",\"request_id\":\"req_011CcihszwEGjxG9JfCdHiUY\"}",
|
|
54
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01ESWwohWenfu2P6f1md7zaG\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01CNTVD5HCViGsHkAgUJ5dpQ\",\"name\":\"Skill\",\"input\":{\"skill\":\"my-pdf-skill:my-pdf-skill\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1185,\"cache_creation_input_tokens\":16350,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":16350},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"4394aa85-d85f-4d02-b5a4-6bb3b0a7de10\",\"request_id\":\"req_011CcihszwEGjxG9JfCdHiUY\"}",
|
|
55
|
+
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_01CNTVD5HCViGsHkAgUJ5dpQ\",\"content\":\"Launching skill: my-pdf-skill:my-pdf-skill\"}]},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"0d37a834-50f4-4b2f-b432-2e3a0e4e9f3b\",\"timestamp\":\"2026-07-05T09:52:33.514Z\",\"tool_use_result\":{\"success\":true,\"commandName\":\"my-pdf-skill:my-pdf-skill\"}}",
|
|
56
|
+
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Base directory for this skill: /sessions/local_2ckhgi8bsn/mnt/.local-plugins/marketplaces/local-desktop-app-uploads/my-pdf-skill/skills/my-pdf-skill\\n\\n# my-pdf-skill\\nPlaceholder skill so the example scenario resolves. Replace with the skill you're testing.\\n\"}]},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"81eebe67-5039-4989-a483-b1da46fecc26\",\"timestamp\":\"2026-07-05T09:52:33.513Z\",\"isSynthetic\":true}",
|
|
57
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01ESWwohWenfu2P6f1md7zaG\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01AkwRnMPxB68b773Kcmwhfx\",\"name\":\"Bash\",\"input\":{\"command\":\"ls -la /sessions/local_2ckhgi8bsn/mnt/uploads/ 2>/dev/null; echo \\\"---project---\\\"; ls -la /sessions/local_2ckhgi8bsn/mnt/project/ 2>/dev/null\",\"description\":\"List uploads and project folders\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1185,\"cache_creation_input_tokens\":16350,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":16350},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"cfdcd3af-2f38-4610-be8c-116e25185731\",\"request_id\":\"req_011CcihszwEGjxG9JfCdHiUY\"}",
|
|
58
|
+
"{\"type\":\"rate_limit_event\",\"rate_limit_info\":{\"status\":\"allowed_warning\",\"resetsAt\":1783317600,\"rateLimitType\":\"seven_day\",\"utilization\":0.85,\"isUsingOverage\":false,\"surpassedThreshold\":0.75},\"uuid\":\"9950e542-4a6b-45db-9a3f-275c9ecd6ed8\",\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\"}",
|
|
59
|
+
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"tool_use_id\":\"toolu_01AkwRnMPxB68b773Kcmwhfx\",\"type\":\"tool_result\",\"content\":\"total 4\\ndrwxr-xr-x 3 ubuntu ubuntu 96 Jul 5 09:52 .\\ndrwxr-xr-x 9 ubuntu ubuntu 288 Jul 5 09:52 ..\\n-rw-r--r-- 1 ubuntu ubuntu 1056 Jul 5 09:52 report.pdf\\n---project---\\ntotal 4\\ndrwxr-xr-x 3 ubuntu ubuntu 96 Jul 5 09:52 .\\ndrwxr-xr-x 9 ubuntu ubuntu 288 Jul 5 09:52 ..\\n-rw-r--r-- 1 ubuntu ubuntu 21 Jul 5 09:52 notes.txt\",\"is_error\":false}]},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"b74c4151-0b03-405f-8d8f-2c3c292248ec\",\"timestamp\":\"2026-07-05T09:52:34.948Z\",\"tool_use_result\":{\"stdout\":\"total 4\\ndrwxr-xr-x 3 ubuntu ubuntu 96 Jul 5 09:52 .\\ndrwxr-xr-x 9 ubuntu ubuntu 288 Jul 5 09:52 ..\\n-rw-r--r-- 1 ubuntu ubuntu 1056 Jul 5 09:52 report.pdf\\n---project---\\ntotal 4\\ndrwxr-xr-x 3 ubuntu ubuntu 96 Jul 5 09:52 .\\ndrwxr-xr-x 9 ubuntu ubuntu 288 Jul 5 09:52 ..\\n-rw-r--r-- 1 ubuntu ubuntu 21 Jul 5 09:52 notes.txt\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"noOutputExpected\":false}}",
|
|
60
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01KMY95bx1dhZdBNGk9oQWcm\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"EoUDCmMIDxgCKkASHAt3SGLrdHvN3dx+cqVULblcA1jKFmFH9/lsuv8G/VQm4JMCnfAgYvtaA3gcsrQC+mRyGsQgirk1+VI7IZfBMg9jbGF1ZGUtb3B1cy00LTg4AEIIdGhpbmtpbmcSDH/hoL4KWrtlfb1x3BoMEFotL1GkN9e9Q1PPIjAbA1hs/ZO6LYT7Woiwe9gQHH9URri6QY/MoL1TCbF03NoB+CWwmknjqpTcSjncLjkqzwG1MxTpJTwAWn/A3kXT2BIpHuEW/QV0mpq+Z0y+HJLVqueBp6O6+vAoemiSdilGoSFBeiTQsCpPxCYJfm5S7bjnoMrjEaYuHpuATd0B8PO+EgLxeiNsubI8/+pputZFTiqegBtAYCP/4bmBXcLB96tmjGRjv+gOKbK6sKVzVOZEINTZdBwaaAea9EV8IerpagPMquP4ZW34nXGAlz/97Z9559fKIc4bcWXI047JPBByS1YuipMb9vVhKhZg009KqR2BPALxCZIMD3W6ed2DtHsYAQ==\"}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":1993,\"cache_read_input_tokens\":16350,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":1993},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"293e7b8e-f1eb-4ebc-8f29-78fb68874737\",\"request_id\":\"req_011CcihtUSCZCXUdn4Y8dGQ9\"}",
|
|
61
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01KMY95bx1dhZdBNGk9oQWcm\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"The skill is a placeholder, so I'll proceed with the task directly. Let me read the PDF.\"}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":1993,\"cache_read_input_tokens\":16350,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":1993},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"c7187d57-5920-4c2f-b9f5-a3454133d0b2\",\"request_id\":\"req_011CcihtUSCZCXUdn4Y8dGQ9\"}",
|
|
62
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01KMY95bx1dhZdBNGk9oQWcm\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01QDXZpwCiBoBkRRwWiSYv3Z\",\"name\":\"Read\",\"input\":{\"file_path\":\"/sessions/local_2ckhgi8bsn/mnt/uploads/report.pdf\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":1,\"cache_creation_input_tokens\":1993,\"cache_read_input_tokens\":16350,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":1993},\"output_tokens\":2,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"7fd5d38c-723f-4e38-b83a-b150791dda2f\",\"request_id\":\"req_011CcihtUSCZCXUdn4Y8dGQ9\"}",
|
|
63
|
+
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"tool_use_id\":\"toolu_01QDXZpwCiBoBkRRwWiSYv3Z\",\"type\":\"tool_result\",\"content\":\"PDF file read: /sessions/local_2ckhgi8bsn/mnt/uploads/report.pdf (1KB)\"}]},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"bcf5c078-7637-4ee9-939d-25be541d5e0b\",\"timestamp\":\"2026-07-05T09:52:38.805Z\",\"tool_use_result\":{\"type\":\"pdf\",\"file\":{\"filePath\":\"/sessions/local_2ckhgi8bsn/mnt/uploads/report.pdf\",\"base64\":\"JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA0IDAgUiA+PiA+PiAvQ29udGVudHMgNSAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1R5cGUxIC9CYXNlRm9udCAvSGVsdmV0aWNhID4+CmVuZG9iago1IDAgb2JqCjw8IC9MZW5ndGggNTA1ID4+CnN0cmVhbQpCVAovRjEgMTQgVGYKNzIgNzIwIFRkCjE2IFRMCihRMiBQcm9qZWN0IFN0YXR1cyBSZXBvcnQpIFRqClQqClQqCihUaGUgbWlncmF0aW9uIHRvIHRoZSBuZXcgaW5nZXN0IHBpcGVsaW5lIGlzIG9uIHRyYWNrLiBFbmQtdG8tZW5kKSBUagpUKgoobGF0ZW5jeSBkcm9wcGVkIDMwJSBhZnRlciB0aGUgY2FjaGluZyBsYXllciBsYW5kZWQsIGFuZCBlcnJvciByYXRlcykgVGoKVCoKKGFyZSB3aXRoaW4gdGFyZ2V0LiBSZW1haW5pbmcgcmlzayBpcyB0aGUgZGF0YS1iYWNrZmlsbCB3aW5kb3cuKSBUagpUKgpUKgooQWN0aW9uIGl0ZW1zOikgVGoKVCoKKDEuIEFsaWNlOiBmaW5hbGl6ZSB0aGUgcHJvZHVjdGlvbiByb2xsb3V0IHBsYW4gYnkgSnVuZSAyMC4pIFRqClQqCigyLiBCb2I6IGFkZCBtb25pdG9yaW5nIGRhc2hib2FyZHMgZm9yIHRoZSBuZXcgcGlwZWxpbmUuKSBUagpUKgooMy4gQ2Fyb2w6IHVwZGF0ZSB0aGUgb24tY2FsbCBydW5ib29rIGFuZCBub3RpZnkgc3VwcG9ydC4pIFRqCkVUCmVuZHN0cmVhbQplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDAwNjQgMDAwMDAgbiAKMDAwMDAwMDEyMSAwMDAwMCBuIAowMDAwMDAwMjQ3IDAwMDAwIG4gCjAwMDAwMDAzMTcgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo4NzMKJSVFT0YK\",\"originalSize\":1056}}}",
|
|
64
|
+
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"document\",\"source\":{\"type\":\"base64\",\"media_type\":\"application/pdf\",\"data\":\"JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA0IDAgUiA+PiA+PiAvQ29udGVudHMgNSAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL1R5cGUgL0ZvbnQgL1N1YnR5cGUgL1R5cGUxIC9CYXNlRm9udCAvSGVsdmV0aWNhID4+CmVuZG9iago1IDAgb2JqCjw8IC9MZW5ndGggNTA1ID4+CnN0cmVhbQpCVAovRjEgMTQgVGYKNzIgNzIwIFRkCjE2IFRMCihRMiBQcm9qZWN0IFN0YXR1cyBSZXBvcnQpIFRqClQqClQqCihUaGUgbWlncmF0aW9uIHRvIHRoZSBuZXcgaW5nZXN0IHBpcGVsaW5lIGlzIG9uIHRyYWNrLiBFbmQtdG8tZW5kKSBUagpUKgoobGF0ZW5jeSBkcm9wcGVkIDMwJSBhZnRlciB0aGUgY2FjaGluZyBsYXllciBsYW5kZWQsIGFuZCBlcnJvciByYXRlcykgVGoKVCoKKGFyZSB3aXRoaW4gdGFyZ2V0LiBSZW1haW5pbmcgcmlzayBpcyB0aGUgZGF0YS1iYWNrZmlsbCB3aW5kb3cuKSBUagpUKgpUKgooQWN0aW9uIGl0ZW1zOikgVGoKVCoKKDEuIEFsaWNlOiBmaW5hbGl6ZSB0aGUgcHJvZHVjdGlvbiByb2xsb3V0IHBsYW4gYnkgSnVuZSAyMC4pIFRqClQqCigyLiBCb2I6IGFkZCBtb25pdG9yaW5nIGRhc2hib2FyZHMgZm9yIHRoZSBuZXcgcGlwZWxpbmUuKSBUagpUKgooMy4gQ2Fyb2w6IHVwZGF0ZSB0aGUgb24tY2FsbCBydW5ib29rIGFuZCBub3RpZnkgc3VwcG9ydC4pIFRqCkVUCmVuZHN0cmVhbQplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDAwNjQgMDAwMDAgbiAKMDAwMDAwMDEyMSAwMDAwMCBuIAowMDAwMDAwMjQ3IDAwMDAwIG4gCjAwMDAwMDAzMTcgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo4NzMKJSVFT0YK\"}}]},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"aa33c287-e709-41ad-bef6-4110843f6633\",\"timestamp\":\"2026-07-05T09:52:38.800Z\",\"isSynthetic\":true}",
|
|
65
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01WFLkoXaKXUhuiLwn34ozK3\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"EuECCmMIDxgCKkBgZbRkVqyblTGmoFqhQq9lyfVfCvHpIrPctPEsDO+c4M3PE8nQ22F71xQxgLSrAfrv0V7w+7PVhKSupnjT3qHeMg9jbGF1ZGUtb3B1cy00LTg4AEIIdGhpbmtpbmcSDLBpIbibWwGKFj3rZBoMtRG5pGWgEA9EeOr1IjB1g9fuh/Hdah6bFdTGtslbzm12kCBE2VKOzsnnfE0cHtaGyqZO6otY565r012YWGwqqwGrk8UfN0XeOtRAxVcVr04FnmnWRfC6sp7aGVenkJTF69IV+IhdD6oKRJGvpEcOtJ1REDMkiX1F66b3fuHJcb/wDGsHJ0IsXrd3xx93KeOvuxmD3iyunTXd8dtDWpnaYDQdmSjKyk/kobpabRBNCe381eemZBxq8uqXKmHyW0szaNH/DKIoizbY5WI6AtVR0nzfCZQBTdwB/5ziJRcxNGqInF0eY9um0pDAr88YAQ==\"}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":2,\"cache_creation_input_tokens\":1955,\"cache_read_input_tokens\":18343,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":1955},\"output_tokens\":5,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"1d8c5d19-1730-4476-b071-1704157e204b\",\"request_id\":\"req_011Ccihtmz8pyMu9AJamFFyb\"}",
|
|
66
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01WFLkoXaKXUhuiLwn34ozK3\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I have the report. Let me write the action items checklist to the outputs folder.\"}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":2,\"cache_creation_input_tokens\":1955,\"cache_read_input_tokens\":18343,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":1955},\"output_tokens\":5,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"874fe646-32f1-4df6-8e09-9720d17303fa\",\"request_id\":\"req_011Ccihtmz8pyMu9AJamFFyb\"}",
|
|
67
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_01WFLkoXaKXUhuiLwn34ozK3\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01JALx8ysLPTcwFDqpnZMweX\",\"name\":\"Write\",\"input\":{\"file_path\":\"/sessions/local_2ckhgi8bsn/mnt/project/outputs/actions.md\",\"content\":\"# Q2 Project Status Report — Action Items\\n\\n- [ ] **Alice:** Finalize the production rollout plan by June 20.\\n- [ ] **Bob:** Add monitoring dashboards for the new pipeline.\\n- [ ] **Carol:** Update the on-call runbook and notify support.\\n\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":2,\"cache_creation_input_tokens\":1955,\"cache_read_input_tokens\":18343,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":1955},\"output_tokens\":5,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"7193d35b-ab7b-4a2a-9bff-6d3ed0dc2cf5\",\"request_id\":\"req_011Ccihtmz8pyMu9AJamFFyb\"}",
|
|
68
|
+
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"tool_use_id\":\"toolu_01JALx8ysLPTcwFDqpnZMweX\",\"type\":\"tool_result\",\"content\":\"File created successfully at: /sessions/local_2ckhgi8bsn/mnt/project/outputs/actions.md (file state is current in your context — no need to Read it back)\"}]},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"996d3b1e-f5c1-4a4a-86b8-6b4ccb61ad55\",\"timestamp\":\"2026-07-05T09:52:44.239Z\",\"tool_use_result\":{\"type\":\"create\",\"filePath\":\"/sessions/local_2ckhgi8bsn/mnt/project/outputs/actions.md\",\"content\":\"# Q2 Project Status Report — Action Items\\n\\n- [ ] **Alice:** Finalize the production rollout plan by June 20.\\n- [ ] **Bob:** Add monitoring dashboards for the new pipeline.\\n- [ ] **Carol:** Update the on-call runbook and notify support.\\n\",\"structuredPatch\":[],\"originalFile\":null,\"userModified\":false}}",
|
|
69
|
+
"{\"type\":\"assistant\",\"message\":{\"model\":\"claude-opus-4-8\",\"id\":\"msg_017XuKKAtw1jNYVyu1b7RTtn\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"The report is a Q2 Project Status Report on the ingest pipeline migration. In short: the migration is on track, end-to-end latency dropped 30% after the caching layer landed, and error rates are within target. The one remaining risk called out is the data-backfill window. It closes with three owner-assigned action items.\\n\\nI saved those as a checklist here: [View actions.md](computer:///sessions/local_2ckhgi8bsn/mnt/project/outputs/actions.md)\\n\\n- Alice — finalize the production rollout plan by June 20\\n- Bob — add monitoring dashboards for the new pipeline\\n- Carol — update the on-call runbook and notify support\"}],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":2,\"cache_creation_input_tokens\":334,\"cache_read_input_tokens\":20298,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":334},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"},\"diagnostics\":null,\"context_management\":null},\"parent_tool_use_id\":null,\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"uuid\":\"2d8a88b5-edf6-453a-91ce-df31db767377\",\"request_id\":\"req_011CcihuA4SCpG4JvaqvRd3J\"}",
|
|
70
|
+
"{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"api_error_status\":null,\"duration_ms\":19763,\"duration_api_ms\":19025,\"ttft_ms\":3621,\"ttft_stream_ms\":2071,\"time_to_request_ms\":32,\"num_turns\":7,\"result\":\"The report is a Q2 Project Status Report on the ingest pipeline migration. In short: the migration is on track, end-to-end latency dropped 30% after the caching layer landed, and error rates are within target. The one remaining risk called out is the data-backfill window. It closes with three owner-assigned action items.\\n\\nI saved those as a checklist here: [View actions.md](computer:///sessions/local_2ckhgi8bsn/mnt/project/outputs/actions.md)\\n\\n- Alice — finalize the production rollout plan by June 20\\n- Bob — add monitoring dashboards for the new pipeline\\n- Carol — update the on-call runbook and notify support\",\"stop_reason\":\"end_turn\",\"session_id\":\"9fefa516-b19c-48f5-b636-97381a0e56d9\",\"total_cost_usd\":0.2657655,\"usage\":{\"input_tokens\":1190,\"cache_creation_input_tokens\":20632,\"cache_read_input_tokens\":54991,\"output_tokens\":1040,\"server_tool_use\":{\"web_search_requests\":0,\"web_fetch_requests\":0},\"service_tier\":\"standard\",\"cache_creation\":{\"ephemeral_1h_input_tokens\":20632,\"ephemeral_5m_input_tokens\":0},\"inference_geo\":\"not_available\",\"iterations\":[{\"input_tokens\":2,\"output_tokens\":215,\"cache_read_input_tokens\":20298,\"cache_creation_input_tokens\":334,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":334},\"type\":\"message\"}],\"speed\":\"standard\"},\"modelUsage\":{\"claude-opus-4-8\":{\"inputTokens\":1190,\"outputTokens\":1040,\"cacheReadInputTokens\":54991,\"cacheCreationInputTokens\":20632,\"webSearchRequests\":0,\"costUSD\":0.2657655,\"contextWindow\":1000000,\"maxOutputTokens\":64000}},\"permission_denials\":[],\"terminal_reason\":\"completed\",\"fast_mode_state\":\"off\",\"uuid\":\"ea4d1b1b-4c21-47d8-b5ff-c0a4593758f8\"}"
|
|
68
71
|
],
|
|
69
72
|
"controlOut": [
|
|
70
|
-
"{\"type\":\"control_request\",\"request_id\":\"init-1\",\"request\":{\"subtype\":\"initialize\",\"appendSubagentSystemPrompt\":\"## Cowork environment\\n\\nYou are running as a subagent inside a Cowork session. Shell commands execute in an isolated Linux sandbox rooted at /sessions/
|
|
73
|
+
"{\"type\":\"control_request\",\"request_id\":\"init-1\",\"request\":{\"subtype\":\"initialize\",\"appendSubagentSystemPrompt\":\"## Cowork environment\\n\\nYou are running as a subagent inside a Cowork session. Shell commands execute in an isolated Linux sandbox rooted at /sessions/local_2ckhgi8bsn — files created there (or under /tmp) exist only in the sandbox, not on the user's real computer. User-attached folders are mounted under /sessions/local_2ckhgi8bsn/mnt/.\",\"hooks\":{\"PreToolUse\":[{\"matcher\":\"Task\",\"hookCallbackIds\":[\"cowork-task-bg-block\"]}]}}}",
|
|
71
74
|
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Read report.pdf from your uploads, summarize it, and write the action items\\nto outputs/actions.md as a markdown checklist.\\n\"}]}}"
|
|
72
75
|
],
|
|
73
76
|
"effectiveFidelity": "container",
|
|
@@ -89,11 +92,14 @@
|
|
|
89
92
|
"outputs",
|
|
90
93
|
"project"
|
|
91
94
|
],
|
|
95
|
+
"preRunPaths": [
|
|
96
|
+
"project/notes.txt"
|
|
97
|
+
],
|
|
92
98
|
"scenarioSource": "../scenarios/example-pdf-skill.yaml",
|
|
93
99
|
"fingerprint": {
|
|
94
100
|
"baseline": "1.18286.0",
|
|
95
|
-
"skillHash": "
|
|
96
|
-
"contentSig": "
|
|
101
|
+
"skillHash": "b760ea90682777367fbd44d866ea66d316452da1abb18bf8cb61f1cec8e67806",
|
|
102
|
+
"contentSig": "ddf68cf6d74e0599729589f8be4f0a9eec440093c99c1060cd6f47dd671a0dfd",
|
|
97
103
|
"skillSources": [
|
|
98
104
|
"../skills/my-pdf-skill"
|
|
99
105
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cowork-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Scriptable, CI-friendly harness for Claude Cowork's runtime contract for testing skills across scenarios — same agent, mounts, egress allowlist, permission protocol, and sandbox limitations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/yaniv-golan/cowork-harness/main/schema/cassette.v8.json",
|
|
4
|
+
"title": "cowork-harness cassette v8",
|
|
5
|
+
"description": "Recorded control-protocol cassette produced by cowork-harness (https://github.com/yaniv-golan/cowork-harness). Contains a frozen scenario, recorded agent event stream, control-protocol responses, artifact manifest, and a staleness fingerprint. v3 adds `contentSig`. v4 persists `userVisibleRoots`. v6 (staleness redesign, breaking): contentSig unified onto the skillHash walk (same file set incl. OS-junk exclusion, in-tree-symlink-by-target); git-tracked file set is the DEFAULT boundary (opt out with COWORK_HARNESS_GITSET=0). v7: NUL-byte entry separator in hashDir and computeContentSig to prevent hash collisions for file paths containing newline characters. v8: skillHash folds fixed-length content shas (not raw bytes) and contentSig/link entries are type-prefixed & NUL-framed (closes unframed-concatenation collisions → a v8 staleness hash is non-comparable with pre-v8, so `rehash` routes pre-v8 cassettes to re-record); `ManifestEntry.truncationReason` names why a body is absent (the cassette-level `readonlyFolderRoots` list is removed).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"scenario",
|
|
9
|
+
"events"
|
|
10
|
+
],
|
|
11
|
+
"properties": {
|
|
12
|
+
"$schema": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"format": "uri"
|
|
15
|
+
},
|
|
16
|
+
"generator": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"const": "cowork-harness"
|
|
19
|
+
},
|
|
20
|
+
"cassetteVersion": {
|
|
21
|
+
"type": "integer",
|
|
22
|
+
"minimum": 0
|
|
23
|
+
},
|
|
24
|
+
"scenario": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "Embedded scenario snapshot at record time."
|
|
27
|
+
},
|
|
28
|
+
"events": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "string"
|
|
32
|
+
},
|
|
33
|
+
"description": "Recorded agent event stream (newline-delimited JSON lines)."
|
|
34
|
+
},
|
|
35
|
+
"controlOut": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"description": "Driver→agent control_response stream (newline-delimited JSON lines)."
|
|
41
|
+
},
|
|
42
|
+
"effectiveFidelity": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "The fidelity tier the live record resolved to."
|
|
45
|
+
},
|
|
46
|
+
"artifacts": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"description": "Snapshot of user-visible output files produced by the run (the manifest). Each entry carries path + bytes + sha256; small text/JSON bodies are inlined, larger ones are hash-only.",
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"required": ["path", "bytes", "sha256"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"path": { "type": "string", "description": "workRoot-relative path, e.g. outputs/report.json" },
|
|
54
|
+
"bytes": { "type": "number", "description": "file size in bytes at record time" },
|
|
55
|
+
"sha256": { "type": "string", "description": "sha256 of the raw bytes (empty string only on an unreadable/containment-failed entry)" },
|
|
56
|
+
"body": { "type": "string", "description": "inlined body (≤ the artifact-body cap); materialized on replay so artifact_json can parse it. Absent on a truncated entry." },
|
|
57
|
+
"encoding": { "type": "string", "enum": ["utf8", "base64"], "description": "how `body` is encoded (base64 for non-UTF-8/binary)." },
|
|
58
|
+
"truncated": { "type": "boolean", "description": "body omitted (hash-only). file_exists/user_visible_artifact still PASS (existence proven by path+sha); artifact_json is evidence-unavailable." },
|
|
59
|
+
"truncationReason": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"enum": ["size", "readonly", "unreadable"],
|
|
62
|
+
"description": "v8+: WHY the body is absent, so replay gives the precise artifact_json remedy. `size` = over the body cap (raise --max-artifact-bytes); `readonly` = a mode:r connected-folder input (assert on a deliverable); `unreadable` = a record-time read/containment failure (sha256 is \"\"). ABSENT on pre-v8 entries."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"userVisibleRoots": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"items": {
|
|
70
|
+
"type": "string"
|
|
71
|
+
},
|
|
72
|
+
"description": "v4+: the user-visible mount roots captured at record time (`outputs` + each connected folder's resolved mount name). Replay reads THIS instead of a hardcoded prefix. ABSENT on pre-v4 cassettes → replay falls back to the legacy prefix."
|
|
73
|
+
},
|
|
74
|
+
"preRunPaths": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
},
|
|
79
|
+
"description": "workRoot-relative paths under userVisibleRoots that existed BEFORE the agent ran — the baseline `no_unexpected_files` diffs against on replay. Optional metadata (no cassetteVersion bump); ABSENT on pre-0.24 and microvm recordings → replay EXCLUDES `no_unexpected_files` with a loud warning. Always co-present with `userVisibleRoots` when present."
|
|
80
|
+
},
|
|
81
|
+
"scenarioSource": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "The authored scenario SOURCE file this cassette was recorded from, RELATIVE to the cassette dir (relocatable, no absolute host path). `record --rerecord-stale` prefers this over a slug guess. ABSENT when recorded from an in-memory/inline scenario."
|
|
84
|
+
},
|
|
85
|
+
"fingerprint": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"description": "Staleness tripwire over the skill/baseline inputs that determined the recording.",
|
|
88
|
+
"properties": {
|
|
89
|
+
"baseline": {
|
|
90
|
+
"type": "string"
|
|
91
|
+
},
|
|
92
|
+
"skillHash": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"description": "Algorithm-specific hash of skill dir contents (staleness detection)."
|
|
95
|
+
},
|
|
96
|
+
"contentSig": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "v3+: Algorithm-independent SHA-256 over sorted relpath:content-sha256 pairs. Used by `rehash` to verify content is unchanged when the staleness algorithm changes."
|
|
99
|
+
},
|
|
100
|
+
"skillSources": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"items": {
|
|
103
|
+
"type": "string"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"skillScope": {
|
|
107
|
+
"type": "array",
|
|
108
|
+
"items": {
|
|
109
|
+
"type": "string"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"sharedHash": {
|
|
113
|
+
"type": "string"
|
|
114
|
+
},
|
|
115
|
+
"fileSigs": {
|
|
116
|
+
"type": "array",
|
|
117
|
+
"description": "v5+: per-file manifest [relpath, contentSha] of files feeding skillHash, for exact-diff staleness reporting. Root-relative paths; scanned+redacted like skillSources.",
|
|
118
|
+
"items": {
|
|
119
|
+
"type": "array",
|
|
120
|
+
"items": {
|
|
121
|
+
"type": "string"
|
|
122
|
+
},
|
|
123
|
+
"minItems": 2,
|
|
124
|
+
"maxItems": 2
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"fileSigsOmitted": {
|
|
128
|
+
"type": "boolean",
|
|
129
|
+
"description": "v5+: true when the per-file manifest was omitted because the tree exceeded the size cap (degrades to bucket-level staleness messages)."
|
|
130
|
+
},
|
|
131
|
+
"mode": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"enum": [
|
|
134
|
+
"git",
|
|
135
|
+
"raw"
|
|
136
|
+
],
|
|
137
|
+
"description": "Phase C: the file-set boundary used for skillHash — 'git' (git-tracked set — the DEFAULT unless COWORK_HARNESS_GITSET=0, and every dir is a git work tree) or 'raw' (filesystem walk; used when GITSET=0 OR any dir is not a repo). A record-vs-verify mode flip means hashes aren't comparable → re-record."
|
|
138
|
+
},
|
|
139
|
+
"agentScope": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"enum": ["skill"],
|
|
142
|
+
"description": "Opt-in per-skill agent scoping (COWORK_HARNESS_AGENT_SCOPE=skill) was active for this scoped hash: a skill-named agents/<n> was treated as skill <n>'s private input, not a fleet-wide shared root. ABSENT = the default (agents/ is shared). A record-vs-verify flip → re-record under the same setting."
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"authoring": {
|
|
147
|
+
"type": "object",
|
|
148
|
+
"description": "Provenance of how the gate answers were authored. PRESENT with nonDeterministic:true only when a live decider answered at least one gate during recording (a driving agent via --decider-dir, a model via --decider-llm, or an --on-unanswered first auto-pick), so RE-recording may drift. The cassette itself still replays deterministically (the answers are frozen). ABSENT means fully scripted/deterministic authoring.",
|
|
149
|
+
"properties": {
|
|
150
|
+
"nonDeterministic": {
|
|
151
|
+
"type": "boolean"
|
|
152
|
+
},
|
|
153
|
+
"channel": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"enum": [
|
|
156
|
+
"decider-dir",
|
|
157
|
+
"decider-llm"
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"required": [
|
|
162
|
+
"nonDeterministic"
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
package/schema/run-result.json
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"egress": { "type": "array" },
|
|
42
42
|
"assertions": { "type": "array" },
|
|
43
43
|
"subagents": { "type": "array" },
|
|
44
|
-
"
|
|
44
|
+
"nonReproducibleAnswers": { "type": "array" },
|
|
45
45
|
"usage": {
|
|
46
46
|
"type": "object",
|
|
47
47
|
"description": "SDK usage payload (input_tokens, output_tokens, etc.), pass-through, plus a harness-computed 'turns' field (from the SDK result message's num_turns) when available. Wave 0 seam.",
|
|
@@ -58,10 +58,50 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"durationMs": { "type": "number" },
|
|
61
|
+
"fingerprint": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"description": "Skill/plugin staleness fingerprint recorded at run time; persisted so `verify-run` can detect a kept run that predates a skill change and refuse to vouch for answer-coverage against stale gate labels.",
|
|
64
|
+
"required": ["baseline"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"baseline": { "type": "string", "description": "appVersion at record time" },
|
|
67
|
+
"skillHash": { "type": "string", "description": "hash of the session's local skill/plugin/marketplace dir contents (if any)" },
|
|
68
|
+
"skillSources": { "type": "array", "items": { "type": "string" } },
|
|
69
|
+
"skillScope": { "type": "array", "items": { "type": "string" } },
|
|
70
|
+
"sharedHash": { "type": "string", "description": "shared-root hash for scoped cassettes; absent on whole-tree or non-plugin-root mounts" },
|
|
71
|
+
"contentSig": { "type": "string", "description": "algorithm-independent content fingerprint; used by `rehash` to verify content is unchanged across format bumps" },
|
|
72
|
+
"fileSigs": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"description": "per-file manifest [relpath, contentSha] of the exact files feeding skillHash",
|
|
75
|
+
"items": { "type": "array", "items": { "type": "string" }, "minItems": 2, "maxItems": 2 }
|
|
76
|
+
},
|
|
77
|
+
"fileSigsOmitted": { "type": "boolean" },
|
|
78
|
+
"mode": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"enum": ["git", "raw"],
|
|
81
|
+
"description": "the file-set boundary used for skillHash — \"git\" (git-tracked set — the DEFAULT for a git work tree unless COWORK_HARNESS_GITSET=0) or \"raw\" (filesystem walk; used when GITSET=0 or the dir is not a git work tree)"
|
|
82
|
+
},
|
|
83
|
+
"agentScope": { "type": "string", "enum": ["skill"] }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
61
86
|
"outDir": { "type": "string" },
|
|
62
87
|
"workDir": { "type": "string" },
|
|
63
88
|
"outputsDir": { "type": "string" },
|
|
89
|
+
"userVisibleRoots": {
|
|
90
|
+
"type": "array",
|
|
91
|
+
"items": { "type": "string" },
|
|
92
|
+
"description": "The user-visible mount roots (relative to mnt/) for this run — `outputs` plus each connected work folder's resolved mount name. Plugins are not included (read-only inputs, never artifact roots)."
|
|
93
|
+
},
|
|
94
|
+
"readonlyFolderRoots": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": { "type": "string" },
|
|
97
|
+
"description": "Subset of userVisibleRoots that are read-only (mode:\"r\") connected-folder mounts — inputs, not deliverables. `artifacts` excludes them so `scaffold` doesn't emit file_exists for an input."
|
|
98
|
+
},
|
|
64
99
|
"artifacts": { "type": "array" },
|
|
100
|
+
"preRunPaths": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"items": { "type": "string" },
|
|
103
|
+
"description": "workRoot-relative paths that existed under the user-visible roots BEFORE the agent ran (captured post-staging, pre-spawn) — the baseline `no_unexpected_files` diffs against. Absent when the tier didn't capture it (microvm) or the run predates the seam."
|
|
104
|
+
},
|
|
65
105
|
"partial": { "type": "boolean" },
|
|
66
106
|
"unansweredGate": { "type": "object" },
|
|
67
107
|
"nonDeterministic": { "type": "boolean" },
|
|
@@ -144,6 +184,20 @@
|
|
|
144
184
|
"full": { "type": "number" },
|
|
145
185
|
"partial": { "type": "number" }
|
|
146
186
|
}
|
|
187
|
+
},
|
|
188
|
+
"toolResults": {
|
|
189
|
+
"type": "array",
|
|
190
|
+
"description": "Tool-result text at assertion-fidelity cap (10 KB per result). Used by tool_result_contains / tool_result_not_contains. `assertText` is preferred when present; falls back to `text` (500-char display cap) for cassettes recorded before this field was added.",
|
|
191
|
+
"items": {
|
|
192
|
+
"type": "object",
|
|
193
|
+
"required": ["isError", "text"],
|
|
194
|
+
"properties": {
|
|
195
|
+
"toolUseId": { "type": "string" },
|
|
196
|
+
"isError": { "type": "boolean" },
|
|
197
|
+
"text": { "type": "string" },
|
|
198
|
+
"assertText": { "type": "string" }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
147
201
|
}
|
|
148
202
|
}
|
|
149
203
|
}
|