cowork-harness 4.8.0 → 4.8.1
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/assertion-catalog-agents-skills-budgets.md +1 -1
- package/.claude/skills/cowork-harness/references/assertion-catalog-gates-hooks-modifiers.md +1 -1
- package/.claude/skills/cowork-harness/references/assertion-catalog-outcome-files-tools.md +1 -1
- package/.claude/skills/cowork-harness/references/assertion-catalog.md +1 -1
- package/.claude/skills/cowork-harness/references/assertions-guide.md +1 -1
- package/.claude/skills/cowork-harness/references/authoring.md +1 -1
- package/.claude/skills/cowork-harness/references/ci-recipe.md +5 -5
- package/.claude/skills/cowork-harness/references/critique.md +1 -1
- package/.claude/skills/cowork-harness/references/debugging.md +1 -1
- package/.claude/skills/cowork-harness/references/eval.md +1 -1
- package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +1 -1
- package/.claude/skills/cowork-harness/references/gotchas.md +1 -1
- package/.claude/skills/cowork-harness/references/hillclimb-recipe.md +1 -1
- package/.claude/skills/cowork-harness/references/hillclimb.md +1 -1
- package/.claude/skills/cowork-harness/references/measurement.md +1 -1
- package/.claude/skills/cowork-harness/references/run-record-replay.md +1 -1
- package/.claude/skills/cowork-harness/references/scenario-schema.md +1 -1
- package/.claude/skills/cowork-harness/references/semantic-judging.md +1 -1
- package/.claude/skills/cowork-harness/references/task-recipes.md +1 -1
- package/CHANGELOG.md +31 -0
- package/README.md +3 -3
- package/RELEASING.md +4 -1
- package/baselines/desktop-2.31226.1.json +81 -0
- package/dist/cli.js +26 -7
- package/dist/runtime/hostloop.js +1 -1
- package/dist/sync/baseline-diff.js +92 -4
- package/dist/sync/cowork-sync.js +80 -216
- package/dist/sync/observed-spawn-env.js +350 -0
- package/dist/sync/spawn-env-post-writes.js +371 -0
- package/dist/sync/spawn-env-registry.js +460 -0
- package/dist/types.js +39 -0
- package/docs/ci.md +1 -1
- package/docs/cli.md +6 -6
- package/docs/companion-skill.md +2 -2
- package/docs/invariants.md +2 -1
- package/docs/maintenance.md +50 -3
- package/examples/replays/README.md +1 -1
- package/package.json +1 -1
- package/scripts/release-preflight.ts +39 -0
|
@@ -3,8 +3,8 @@ name: cowork-harness
|
|
|
3
3
|
description: Test or debug a Claude Code skill/plugin under Claude Cowork's runtime — sandboxed agent, default-deny egress, the can_use_tool permission/question protocol — using the cowork-harness CLI. Use when validating or regression-testing a skill, authoring or debugging a scenario YAML (prompt + scripted answers + assert:), choosing a fidelity tier, scripting AskUserQuestion / tool-permission answers, asserting artifacts, egress, or sub-agent dispatch, measuring how long each tool call took (toolDurations / trace), or debugging a failed run or verdict from its result.json or transcript. Especially when a harness run no-ops an assertion, fails on an unanswered gate, false-greens, a steered answer never reaches the model, or a web_fetch is unexpectedly denied or gated. Also when iterating or hardening a skill across fixes, or grounding a skill's self-critique against its own run evidence — including a document-analysis skill (cap table, deck, financial model, transcript) that needs an uploaded file attached to be critiqued at all. Also for comparing two versions of a skill before merging an edit — did it make answers worse? (`eval`: paired A/B, pinned models) — or improving one round by round (`hillclimb`, the `/claude-api hillclimb` runner). NOT for generic unit testing (pytest/vitest of your own scripts) or non-Cowork CI. Covers the skill / run / chat / record / replay / trace / decide / assertions / scaffold / critique / stats / eval / hillclimb commands and the session-vs-scenario split.
|
|
4
4
|
metadata:
|
|
5
5
|
author: cowork-harness
|
|
6
|
-
version: 4.8.
|
|
7
|
-
tracks-harness: cowork-harness 4.8.
|
|
6
|
+
version: 4.8.1
|
|
7
|
+
tracks-harness: cowork-harness 4.8.1 (baseline desktop-2.31226.1)
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# cowork-harness
|
|
@@ -26,7 +26,7 @@ allowlist). This skill exists mostly to keep you out of those traps — the *Inv
|
|
|
26
26
|
full landmine catalog in [`references/gotchas.md`](references/gotchas.md) are the highest-value part.
|
|
27
27
|
Read them.
|
|
28
28
|
|
|
29
|
-
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 4.8.
|
|
29
|
+
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 4.8.1` (baseline
|
|
30
30
|
> `desktop-2.31226.1`). If your checkout is newer, prefer the live `--help` and — in a repo checkout —
|
|
31
31
|
> `SPEC.md` / `docs/*.md` over this snapshot, and re-run the bundled linter.
|
|
32
32
|
|
|
@@ -43,7 +43,7 @@ Before the first command, confirm the CLI is reachable and **fail loud** (never
|
|
|
43
43
|
|
|
44
44
|
- **One-shot check.** Run `cowork-harness doctor [--tier <tier>]` first — a read-only prerequisite check that inspects Docker, the staged agent, the token, and the baseline in one pass. The bullets below explain each thing it checks (and how to fix it).
|
|
45
45
|
- **Replay-only? Skip `doctor`.** Replaying committed cassettes needs no Docker, no staged agent, and no token — and every tier's `doctor` validates the auth token (the live tiers also Docker + the staged agent), so a ✗ there is expected, not a blocker. Go straight to `cowork-harness replay <cassette>`.
|
|
46
|
-
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 4.8.
|
|
46
|
+
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 4.8.1**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@^4.8.1" <cmd>` (Node ≥ 22), or install once with `npm i -g "cowork-harness@^4.8.1"`. **Pin `@^4.8.1`, never `@latest`** — `@latest` can silently fetch an older CLI and the new commands fail as "unknown command", whereas the floor **fails loud** if no compatible version is published.
|
|
47
47
|
|
|
48
48
|
This skill documents the CURRENT surface, not release history. If `cowork-harness --version` is
|
|
49
49
|
OLDER than the floor, the per-release record of what you are missing is [CHANGELOG.md](https://github.com/yaniv-golan/cowork-harness/blob/main/CHANGELOG.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertion catalog: sub-agents, skills and connectors, budgets, tasks, delivery
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Part 2 of 3 of the per-key table. The conventions every key shares (an item with several
|
|
4
4
|
keys is an AND; glob vs regex name matching) and the verdict-signal table are in
|
|
5
5
|
[`assertion-catalog.md`](./assertion-catalog.md); the other parts are [`assertion-catalog-outcome-files-tools.md`](./assertion-catalog-outcome-files-tools.md), [`assertion-catalog-gates-hooks-modifiers.md`](./assertion-catalog-gates-hooks-modifiers.md).
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertion catalog: gates, hooks, path denial, verdict modifiers, egress, judged keys
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Part 3 of 3 of the per-key table. The conventions every key shares (an item with several
|
|
4
4
|
keys is an AND; glob vs regex name matching) and the verdict-signal table are in
|
|
5
5
|
[`assertion-catalog.md`](./assertion-catalog.md); the other parts are [`assertion-catalog-outcome-files-tools.md`](./assertion-catalog-outcome-files-tools.md), [`assertion-catalog-agents-skills-budgets.md`](./assertion-catalog-agents-skills-budgets.md).
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertion catalog: outcome, transcript, files and artifacts, tools, tool results
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Part 1 of 3 of the per-key table. The conventions every key shares (an item with several
|
|
4
4
|
keys is an AND; glob vs regex name matching) and the verdict-signal table are in
|
|
5
5
|
[`assertion-catalog.md`](./assertion-catalog.md); the other parts are [`assertion-catalog-agents-skills-budgets.md`](./assertion-catalog-agents-skills-budgets.md), [`assertion-catalog-gates-hooks-modifiers.md`](./assertion-catalog-gates-hooks-modifiers.md).
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertion catalog
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Every `assert:` key with its semantics, and the
|
|
4
4
|
verdict-signal table. Which keys survive `replay` is in [`scenario-schema.md`](./scenario-schema.md#replay-class);
|
|
5
5
|
the scenario and session YAML fields are there too.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertions guide
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Read it when choosing assertion keys: the two orthogonal axes and the goal → key map. The full catalog starts at `assertion-catalog.md`, which indexes the per-family files that hold every key's row.
|
|
4
4
|
|
|
5
5
|
### Assertions: two orthogonal axes
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Authoring a scenario
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Read it when composing a `scenarios/*.yaml`: session vs scenario, discovery, the fidelity tier, the answer path, `web_fetch`, and scaffold + lint.
|
|
4
4
|
|
|
5
5
|
## Part I — AUTHOR a scenario
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CI recipe — replay vs live lanes
|
|
2
2
|
|
|
3
|
-
Self-contained reference. Tracks `cowork-harness 4.8.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`).
|
|
4
4
|
|
|
5
5
|
**Fastest path: the packaged Action.** One step gets you `replay`/`lint`/`verify-cassettes` plus a PR
|
|
6
6
|
job-summary reporter (verdict table, staleness findings, cost/turns when available):
|
|
@@ -17,7 +17,7 @@ job-summary reporter (verdict table, staleness findings, cost/turns when availab
|
|
|
17
17
|
CLI major reaches your workflow the moment it is promoted even though your `uses:` ref never changed — so a
|
|
18
18
|
copy-pasted recipe that omits the input takes a major bump with no say in it. `^4` holds the major, needs no
|
|
19
19
|
patch number to remember, and only wants a human decision at the next major. Pin an exact version
|
|
20
|
-
(e.g. `version: "4.8.
|
|
20
|
+
(e.g. `version: "4.8.1"`) instead when you want byte-reproducible CI.
|
|
21
21
|
|
|
22
22
|
Reach for the manual multi-step form below only when you need per-step control the Action's inputs don't
|
|
23
23
|
cover (a custom flag combination, a different runner matrix per step, or `lint`/`verify-cassettes` gated
|
|
@@ -82,7 +82,7 @@ sha256-*checked* but not hard-blocking on mismatch — it's advisory for an inte
|
|
|
82
82
|
GitHub-hosted runners, no token/Docker/agent:
|
|
83
83
|
|
|
84
84
|
```yaml
|
|
85
|
-
- run: npm i -g "cowork-harness@^4.8.
|
|
85
|
+
- run: npm i -g "cowork-harness@^4.8.1"
|
|
86
86
|
- run: cowork-harness lint scenarios/*.yaml --strict --min-severity WARN
|
|
87
87
|
# no silent false-greens. WITHOUT --strict this
|
|
88
88
|
# step cannot fail on a WARN-class rule (e.g.
|
|
@@ -411,7 +411,7 @@ jobs:
|
|
|
411
411
|
with: { node-version: '24' }
|
|
412
412
|
- uses: actions/setup-python@v5
|
|
413
413
|
with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
|
|
414
|
-
- run: npm i -g "cowork-harness@^4.8.
|
|
414
|
+
- run: npm i -g "cowork-harness@^4.8.1"
|
|
415
415
|
- run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
|
|
416
416
|
- run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
|
|
417
417
|
- run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
|
|
@@ -440,7 +440,7 @@ jobs:
|
|
|
440
440
|
echo "live=true" >> "$GITHUB_OUTPUT"
|
|
441
441
|
fi
|
|
442
442
|
- if: steps.guard.outputs.live == 'true'
|
|
443
|
-
run: npm i -g "cowork-harness@^4.8.
|
|
443
|
+
run: npm i -g "cowork-harness@^4.8.1"
|
|
444
444
|
- if: steps.guard.outputs.live == 'true'
|
|
445
445
|
run: cowork-harness run scenarios/ --output-format json
|
|
446
446
|
env:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Critique — the facts a plugin install can't otherwise reach
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). This is **not** a trim of the full
|
|
4
4
|
[`docs/critique.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/critique.md) (repo-only —
|
|
5
5
|
flags, cost, reading several critiques, known limitations all live there). This file covers exactly what a
|
|
6
6
|
plugin install cannot otherwise discover: the run-dir artifact a harvester actually reads, the report's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Debugging a run
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Read it when a run misbehaved or a green looks wrong: triage, the observability output, and `chat`.
|
|
4
4
|
|
|
5
5
|
## Part III — Debug
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `eval` — paired before/after comparison of a skill edit (EXPERIMENTAL)
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). The full guide is
|
|
4
4
|
[docs/eval.md](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/eval.md); this is the part you
|
|
5
5
|
need while running it.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Fidelity tiers & answer paths
|
|
2
2
|
|
|
3
|
-
Self-contained reference. Tracks `cowork-harness 4.8.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`).
|
|
4
4
|
|
|
5
5
|
> **This page vs. the repo docs.** This is the **offline snapshot** that ships inside the installed
|
|
6
6
|
> plugin — it is self-contained on purpose. The repo carries four other fidelity views, each answering a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Gotchas
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). The full "✓ passed ≠ correct" landmine catalog.
|
|
4
4
|
|
|
5
5
|
## Gotchas — the "✓ passed ≠ correct" landmines
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Recipe 7 — Climb a skill with `/claude-api hillclimb` and the harness as its runner
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). It needs a `cowork-harness` whose
|
|
4
4
|
`hillclimb --help` lists `--skill` (help goes to stderr). This page is the loop's procedure, step by step, in the order of the
|
|
5
5
|
`/claude-api hillclimb` guide. Every mechanic (flags, refusals, the gate, `regrade`, `freeze-ref`, exit codes,
|
|
6
6
|
row keys) is in [`hillclimb.md`](hillclimb.md); the setup and the full list of differences from the guide's own
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `hillclimb` — the runner for a `/claude-api hillclimb` loop
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). It needs a `cowork-harness` whose
|
|
4
4
|
`hillclimb --help` lists `--skill` (help goes to stderr). The command reference is
|
|
5
5
|
[docs/cli.md](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/cli.md); this is the part a loop needs
|
|
6
6
|
while it runs. It covers `run`, `check`, `state-template`, `freeze-ref` and `regrade`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Measurement
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Read it before comparing runs: `--repeat`, `--ablate-skill`, and the hygiene that keeps a batch valid.
|
|
4
4
|
|
|
5
5
|
### Measure — before/after, with/without (`--repeat`, `--ablate-skill`)
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Run, record and lock
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). Read it when running a scenario, recording or placing a cassette, reading verdict signals, checking a background run, or choosing CI lanes.
|
|
4
4
|
|
|
5
5
|
## Part II — RUN, RECORD & LOCK
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Scenario & session schema, replay class, web_fetch, authoring gotchas
|
|
2
2
|
|
|
3
|
-
Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 4.8.
|
|
3
|
+
Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 4.8.1`
|
|
4
4
|
(baseline `desktop-2.31226.1`). If your checkout is newer, prefer the live [`docs/scenario.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/scenario.md),
|
|
5
5
|
[`docs/session.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/session.md), and `SPEC.md`.
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `semantic_matches` — the full semantics
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.8.
|
|
3
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`). The one-line summary is in
|
|
4
4
|
[assertion-catalog-gates-hooks-modifiers.md](assertion-catalog-gates-hooks-modifiers.md); this is the whole contract, split out so the catalog stays within the
|
|
5
5
|
agent's single-read size.
|
|
6
6
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Each recipe composes facts that live scattered across SKILL.md and the other references into one
|
|
4
4
|
decision path. Every one answers a question a real fleet owner had to work out the hard way.
|
|
5
|
-
Tracks `cowork-harness 4.8.
|
|
5
|
+
Tracks `cowork-harness 4.8.1` (baseline `desktop-2.31226.1`), same as SKILL.md's front-matter. Recipe 2's `resolved-tier`/`unverifiable-tier` staleness classes and
|
|
6
6
|
Recipe 3's `init-redact` shipped in 0.24.0 and are part of the current feature set — no version gate
|
|
7
7
|
needed if your CLI meets SKILL.md's version floor.
|
|
8
8
|
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [4.8.1] — 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Upgrade notes
|
|
12
|
+
|
|
13
|
+
- **For maintainers: `npm run preflight` now also fails when the newest baseline has no
|
|
14
|
+
`provenance.observedSpawnEnv`.** A baseline synced with an older cowork-harness must be re-synced before a
|
|
15
|
+
release. The shipped newest baseline already carries the block.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **`sync` records the env key names Desktop's own host-loop spawn carried.** The spawn-env check read only the env
|
|
20
|
+
literals in Desktop's bundle, and Desktop keeps writing to the env after they are built. `sync` now reads the final
|
|
21
|
+
key list Desktop logs for each local host-loop session (`~/Library/Logs/Claude/main*.log`) and records the names as
|
|
22
|
+
`provenance.observedSpawnEnv`. It also scans Desktop's spawn path for direct writes to the env made after it is built.
|
|
23
|
+
Every recorded name must be classified, with a reason, in one registry (`src/sync/spawn-env-registry.ts`). An
|
|
24
|
+
unclassified name that Desktop's own bundle carries refuses the write and is named in sync's local output; any
|
|
25
|
+
other unclassified name (an organisation's managed env, say) is counted as `withheld` and never printed or
|
|
26
|
+
committed. A name the registry says the harness sets, but the harness host loop does not set, also refuses the
|
|
27
|
+
write, as do unreadable dumps, local sessions that logged no dump, a `Starting app` log entry with no version, and
|
|
28
|
+
a bundle that no longer logs the dump.
|
|
29
|
+
- **`npm run preflight` refuses a newest baseline whose observed spawn env is unobserved** (no local host-loop task
|
|
30
|
+
ran on that Desktop build). `--allow-unobserved-spawn-env` downgrades it to a warning for an emergency release.
|
|
31
|
+
When local tasks can no longer run, `sync --spawn-env-unobservable local-lane-removed|no-local-entitlement`
|
|
32
|
+
records that for the build, and the preflight only warns.
|
|
33
|
+
- **`diff <baseline> <baseline>` describes a change to the observed spawn env in a few readable lines**, in the
|
|
34
|
+
default text output, with `--changelog`, and in `sync --diff`. A build that went from observed to unobserved
|
|
35
|
+
prints one line saying so, rather than a removal for every key it no longer records. Keys that moved between
|
|
36
|
+
"every session" and "some sessions" are listed as moved, not as an addition and a removal.
|
|
37
|
+
- **`sync`'s unknown-delta header no longer points at `src/sync/cowork-sync.ts`.** Each delta line now names the
|
|
38
|
+
file to fix.
|
|
39
|
+
|
|
9
40
|
## [4.8.0] — 2026-10-11
|
|
10
41
|
|
|
11
42
|
### Upgrade notes
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ npm ci && npm run build
|
|
|
36
36
|
node dist/cli.js replay examples/replays/example-pdf-skill.cassette.json
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
(Installing globally — `npm install -g "cowork-harness@^4.8.
|
|
39
|
+
(Installing globally — `npm install -g "cowork-harness@^4.8.1"` — gives you the `cowork-harness` CLI for your own
|
|
40
40
|
scenarios and cassettes; the bundled example above also replays from a global install — see the `$(npm root -g)` path below.)
|
|
41
41
|
|
|
42
42
|
Full setup → [Quick start](./docs/cli.md#quick-start).
|
|
@@ -49,8 +49,8 @@ Three ways to use this project. Each row is the whole hook — follow the link f
|
|
|
49
49
|
|
|
50
50
|
| I want to… | Start here | Needs |
|
|
51
51
|
|---|---|---|
|
|
52
|
-
| **Run scenarios myself** from a terminal | **[docs/cli.md](./docs/cli.md)**<br><br>`npm i -g "cowork-harness@^4.8.
|
|
53
|
-
| **Have Claude Code drive it** for me | **[docs/companion-skill.md](./docs/companion-skill.md)**<br><br>`/plugin marketplace add yaniv-golan/cowork-harness`<br>`/plugin install cowork-harness@cowork-harness` | Claude Code. The skill self-bootstraps the CLI via `npx "cowork-harness@^4.8.
|
|
52
|
+
| **Run scenarios myself** from a terminal | **[docs/cli.md](./docs/cli.md)**<br><br>`npm i -g "cowork-harness@^4.8.1"`<br>`cowork-harness replay examples/replays/example-pdf-skill.cassette.json` | Node ≥ 22. The replay demo above is token-free and needs nothing else; live tiers above `protocol` need Docker + a staged agent binary |
|
|
53
|
+
| **Have Claude Code drive it** for me | **[docs/companion-skill.md](./docs/companion-skill.md)**<br><br>`/plugin marketplace add yaniv-golan/cowork-harness`<br>`/plugin install cowork-harness@cowork-harness` | Claude Code. The skill self-bootstraps the CLI via `npx "cowork-harness@^4.8.1"` |
|
|
54
54
|
| **Gate my skill in CI** | **[docs/ci.md](./docs/ci.md)**<br><br>`- uses: yaniv-golan/cowork-harness@v4`<br>` with: { command: replay, path: cassettes/ }` | Nothing for the token-free gate; the live lane needs a self-hosted runner with Docker + an agent binary |
|
|
55
55
|
|
|
56
56
|
Improving a skill round by round with Claude Code's `/claude-api hillclimb` loop? The harness is its runner, with
|
package/RELEASING.md
CHANGED
|
@@ -242,7 +242,10 @@ tagging `1.0.0`, deliberately review and freeze the surfaces with no machine-rea
|
|
|
242
242
|
the push-to-main live suite will be skipped and this release won't be live-validated in CI; warns if a
|
|
243
243
|
ruleset **required status check** names no job in `ci.yml`; fails if the newest baseline's
|
|
244
244
|
`provenance.desktopInitSurface` is unobserved — start one Cowork session and re-run `sync`, or pass
|
|
245
|
-
`--allow-unobserved-init-surface` for an emergency release; fails if
|
|
245
|
+
`--allow-unobserved-init-surface` for an emergency release; fails if its `provenance.observedSpawnEnv`
|
|
246
|
+
is missing, malformed, or unobserved with no declaration — run one local Cowork task on that Desktop and
|
|
247
|
+
re-run `sync`, or pass `--allow-unobserved-spawn-env` for an emergency release (a declared `lane-gone` only
|
|
248
|
+
warns); fails if the covered surface lost or changed
|
|
246
249
|
a leaf since the last release tag and the bumped version is not a major — the same check as
|
|
247
250
|
`npm run check:surface -- --since-tag`, which prints the counts and lists every removed/changed leaf.
|
|
248
251
|
Before the bump it reports the diff without enforcing it).
|
|
@@ -1240,6 +1240,87 @@
|
|
|
1240
1240
|
"toolsSome": []
|
|
1241
1241
|
}
|
|
1242
1242
|
}
|
|
1243
|
+
},
|
|
1244
|
+
"observedSpawnEnv": {
|
|
1245
|
+
"observed": true,
|
|
1246
|
+
"appVersion": "2.31226.1",
|
|
1247
|
+
"nativeAgentVersions": [
|
|
1248
|
+
"2.1.295"
|
|
1249
|
+
],
|
|
1250
|
+
"sessions": 26,
|
|
1251
|
+
"keysAll": [
|
|
1252
|
+
"ANTHROPIC_BASE_URL",
|
|
1253
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
1254
|
+
"API_FORCE_IDLE_TIMEOUT",
|
|
1255
|
+
"API_TIMEOUT_MS",
|
|
1256
|
+
"CLAUDE_AGENT_SDK_MCP_NO_PREFIX",
|
|
1257
|
+
"CLAUDE_CODE_ACCOUNT_TAGGED_ID",
|
|
1258
|
+
"CLAUDE_CODE_ACCOUNT_UUID",
|
|
1259
|
+
"CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD",
|
|
1260
|
+
"CLAUDE_CODE_COWORK_FRAME_ARTIFACTS",
|
|
1261
|
+
"CLAUDE_CODE_DESKTOP_APP_VERSION",
|
|
1262
|
+
"CLAUDE_CODE_DISABLE_AGENTS_FLEET",
|
|
1263
|
+
"CLAUDE_CODE_DISABLE_AUTO_MEMORY",
|
|
1264
|
+
"CLAUDE_CODE_DISABLE_BACKGROUND_TASKS",
|
|
1265
|
+
"CLAUDE_CODE_DISABLE_CRON",
|
|
1266
|
+
"CLAUDE_CODE_DISABLE_TERMINAL_TITLE",
|
|
1267
|
+
"CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES",
|
|
1268
|
+
"CLAUDE_CODE_ENABLE_APPEND_SUBAGENT_PROMPT",
|
|
1269
|
+
"CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL",
|
|
1270
|
+
"CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING",
|
|
1271
|
+
"CLAUDE_CODE_ENABLE_TASKS",
|
|
1272
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
1273
|
+
"CLAUDE_CODE_HOST_PLATFORM",
|
|
1274
|
+
"CLAUDE_CODE_IS_COWORK",
|
|
1275
|
+
"CLAUDE_CODE_OAUTH_SCOPES",
|
|
1276
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
1277
|
+
"CLAUDE_CODE_ORGANIZATION_UUID",
|
|
1278
|
+
"CLAUDE_CODE_PLUGIN_ATTRIBUTION",
|
|
1279
|
+
"CLAUDE_CODE_PROJECT_DIR_NAME",
|
|
1280
|
+
"CLAUDE_CODE_PROMPT_CACHE_TTL",
|
|
1281
|
+
"CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST",
|
|
1282
|
+
"CLAUDE_CODE_RATE_LIMIT_TIER",
|
|
1283
|
+
"CLAUDE_CODE_SIMPLE",
|
|
1284
|
+
"CLAUDE_CODE_SKILL_ATTRIBUTION",
|
|
1285
|
+
"CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL",
|
|
1286
|
+
"CLAUDE_CODE_SUBSCRIPTION_TYPE",
|
|
1287
|
+
"CLAUDE_CODE_TAGS",
|
|
1288
|
+
"CLAUDE_CODE_USER_EMAIL",
|
|
1289
|
+
"CLAUDE_CONFIG_DIR",
|
|
1290
|
+
"CLAUDE_PREVIEW_CLASSIFIER_FLOOR",
|
|
1291
|
+
"DISABLE_AUTOUPDATER",
|
|
1292
|
+
"DISABLE_MICROCOMPACT",
|
|
1293
|
+
"DOCUMENTS_MCP_SCRATCH_ROOT",
|
|
1294
|
+
"ENABLE_PROMPT_CACHING_1H",
|
|
1295
|
+
"GIT_CONFIG_COUNT",
|
|
1296
|
+
"GIT_CONFIG_KEY_0",
|
|
1297
|
+
"GIT_CONFIG_KEY_1",
|
|
1298
|
+
"GIT_CONFIG_KEY_2",
|
|
1299
|
+
"GIT_CONFIG_PARAMETERS",
|
|
1300
|
+
"GIT_CONFIG_VALUE_0",
|
|
1301
|
+
"GIT_CONFIG_VALUE_1",
|
|
1302
|
+
"GIT_CONFIG_VALUE_2",
|
|
1303
|
+
"GIT_DIR",
|
|
1304
|
+
"HOME",
|
|
1305
|
+
"LOGNAME",
|
|
1306
|
+
"MCP_CONNECTION_NONBLOCKING",
|
|
1307
|
+
"MCP_TOOL_TIMEOUT",
|
|
1308
|
+
"NODE_USE_SYSTEM_CA",
|
|
1309
|
+
"OTEL_RESOURCE_ATTRIBUTES",
|
|
1310
|
+
"OTEL_SERVICE_NAME",
|
|
1311
|
+
"PATH",
|
|
1312
|
+
"PYTHONDONTWRITEBYTECODE",
|
|
1313
|
+
"SHELL",
|
|
1314
|
+
"TZ",
|
|
1315
|
+
"USER",
|
|
1316
|
+
"USE_LOCAL_OAUTH",
|
|
1317
|
+
"USE_STAGING_OAUTH"
|
|
1318
|
+
],
|
|
1319
|
+
"keysSome": [
|
|
1320
|
+
"CLAUDE_CODE_HOST_SCHEDULED_RUN",
|
|
1321
|
+
"CLAUDE_CODE_WORKSPACE_HOST_PATHS"
|
|
1322
|
+
],
|
|
1323
|
+
"withheld": 0
|
|
1243
1324
|
}
|
|
1244
1325
|
},
|
|
1245
1326
|
"requireFullVmSandbox": null,
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readFileSync, writeFileSync, mkdirSync, statSync, existsSync, copyFileS
|
|
|
5
5
|
import { join, basename, resolve, isAbsolute, dirname } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { parse as parseYaml, YAMLParseError } from "yaml";
|
|
8
|
-
import { Scenario, Assertion, FIDELITY_TIERS, DesktopInitSurface, } from "./types.js";
|
|
8
|
+
import { Scenario, Assertion, FIDELITY_TIERS, DesktopInitSurface, ObservedSpawnEnv, ObservedSpawnEnvDeclaration, } from "./types.js";
|
|
9
9
|
import { writeAllSync, tildeify, installTerminalScrub, warn } from "./io.js";
|
|
10
10
|
import { SECRET_ENV_KEYS } from "./runtime/host-env.js";
|
|
11
11
|
import { loadBaseline, BASELINES_DIR, cmpVersionStrings, listBaselineNames, sha256File, countStringInFile, cliCapabilitiesOfFile, deriveNativeStagedPath, nativeManifestBuild, buildNextAgentBinary, } from "./baseline.js";
|
|
@@ -17,6 +17,7 @@ import { claudeCliCompleteDecider, isolationRefusal } from "./decide/llm-transpo
|
|
|
17
17
|
import { vmInit, vmDelete, vmStatus, vmPrune, instanceName, vmProvisioned } from "./runtime/lima.js";
|
|
18
18
|
import { resolveVmBaselineArg } from "./runtime/vm-baseline-arg.js";
|
|
19
19
|
import { sync, canonicalizeEnv, syncedNetworkBlock } from "./sync/cowork-sync.js";
|
|
20
|
+
import { checkObservedAgainstHarness, harnessHostLoopKeys } from "./sync/observed-spawn-env.js";
|
|
20
21
|
import { cloudSchemaRefusal, withSyncedCloudBlock } from "./sync/remote-devices.js";
|
|
21
22
|
import { diffBaselines, formatDiffLines, renderChangelog } from "./sync/baseline-diff.js";
|
|
22
23
|
import { runBoundaryChecks, formatBoundary } from "./boundary.js";
|
|
@@ -246,7 +247,7 @@ const HELP = `cowork-harness <command> (v${"$VERSION"})
|
|
|
246
247
|
answer it with 'gates'/'answer' above (the one path here that waits — that is the rehearsal)
|
|
247
248
|
|
|
248
249
|
── Platform admin ─────────────────────────────────────────────────────────────
|
|
249
|
-
sync [--diff] [--allow-empty|--force] derive/refresh a platform baseline from the live Desktop install (macOS only)
|
|
250
|
+
sync [--diff] [--allow-empty|--force] [--spawn-env-unobservable <reason>] derive/refresh a platform baseline from the live Desktop install (macOS only)
|
|
250
251
|
list list available platform baselines
|
|
251
252
|
boundary-check [baseline] [--session <file>] prove the sandbox enforces Cowork's limitations
|
|
252
253
|
vm <init|status|delete|prune> manage the L2 Apple-VZ microVM (fidelity: microvm); macOS arm64 only
|
|
@@ -490,7 +491,7 @@ function hasHelp(args) {
|
|
|
490
491
|
// invocation. Intercept `--help`/`-h` at dispatch and print the command's usage (exit 0). One concise line
|
|
491
492
|
// per command, kept in sync with each command's own bad-invocation `usage:` string.
|
|
492
493
|
const SUBCOMMAND_USAGE = {
|
|
493
|
-
sync: "usage: sync [--diff] [--allow-empty|--force] (re-sync the platform baseline from the installed Cowork app; macOS only)\n --allow-empty (alias --force): write even when the derived egress allowlist is empty",
|
|
494
|
+
sync: "usage: sync [--diff] [--allow-empty|--force] [--spawn-env-unobservable local-lane-removed|no-local-entitlement] (re-sync the platform baseline from the installed Cowork app; macOS only)\n --allow-empty (alias --force): write even when the derived egress allowlist is empty\n --spawn-env-unobservable: no local host-loop task can run on this Desktop build, so record its spawn env as unobservable (refused when any local session or dump exists for the build)",
|
|
494
495
|
list: "usage: list [--output-format text|json] (list available platform baselines)",
|
|
495
496
|
"boundary-check": "usage: boundary-check [<baseline>] [--session <file>] [--output-format text|json]",
|
|
496
497
|
vm: "usage: vm <init|status|delete|prune> [<baseline>] [--output-format text|json] (macOS arm64 only)\n init create the L2 Apple-VZ microVM\n status show running VM state\n delete remove the VM\n prune drop all orphaned VMs\n <baseline> (default: latest) is a baseline name like desktop-<version>, not a VM name — each acts on that baseline's VM",
|
|
@@ -2765,7 +2766,11 @@ async function cmdSync(args) {
|
|
|
2765
2766
|
const normalizedArgs = args.map((a) => (a === "--force" ? "--allow-empty" : a));
|
|
2766
2767
|
let syncParsed;
|
|
2767
2768
|
try {
|
|
2768
|
-
syncParsed = parseArgs(normalizedArgs, withCommandGlobals({
|
|
2769
|
+
syncParsed = parseArgs(normalizedArgs, withCommandGlobals({
|
|
2770
|
+
booleans: ["--diff", "--allow-empty"],
|
|
2771
|
+
values: ["--spawn-env-unobservable"],
|
|
2772
|
+
enums: { "--spawn-env-unobservable": [...ObservedSpawnEnvDeclaration.options] },
|
|
2773
|
+
}));
|
|
2769
2774
|
}
|
|
2770
2775
|
catch (e) {
|
|
2771
2776
|
return fail("sync", "usage", e.message, undefined, isJsonOutput(normalizedArgs));
|
|
@@ -2775,7 +2780,8 @@ async function cmdSync(args) {
|
|
|
2775
2780
|
return fail("sync", "usage", `sync takes no positional arguments (got: ${syncParsed.positionals.join(", ")})`, undefined, isJsonOutput(normalizedArgs));
|
|
2776
2781
|
}
|
|
2777
2782
|
const allowEmpty = !!syncParsed.flags["--allow-empty"];
|
|
2778
|
-
const
|
|
2783
|
+
const declared = syncParsed.options["--spawn-env-unobservable"];
|
|
2784
|
+
const res = sync({ declareUnobservable: declared ? ObservedSpawnEnvDeclaration.parse(declared) : null });
|
|
2779
2785
|
// refuse to write a baseline with empty version fields. An empty appVersion would produce
|
|
2780
2786
|
// `desktop-.json` (invalid filename); an empty agentVersion means resolveAgentBinary will fail.
|
|
2781
2787
|
const versionErrors = [];
|
|
@@ -3009,6 +3015,10 @@ async function cmdSync(args) {
|
|
|
3009
3015
|
if (!initSurfaceCheck.success) {
|
|
3010
3016
|
fail("sync", "runtime", `ERROR: provenance.desktopInitSurface failed its schema — refusing to write baseline (issue paths: ${initSurfaceCheck.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")})`, "This is a reader bug in src/sync/desktop-init-surface.ts, not a Desktop change.", isJsonOutput(normalizedArgs), 1);
|
|
3011
3017
|
}
|
|
3018
|
+
const observedCheck = ObservedSpawnEnv.safeParse(res.observedSpawnEnv);
|
|
3019
|
+
if (!observedCheck.success) {
|
|
3020
|
+
fail("sync", "runtime", `ERROR: provenance.observedSpawnEnv failed its schema — refusing to write baseline (issue paths: ${observedCheck.error.issues.map((i) => i.path.join(".") || "(root)").join(", ")})`, "This is a reader bug in src/sync/observed-spawn-env.ts, not a Desktop change.", isJsonOutput(normalizedArgs), 1);
|
|
3021
|
+
}
|
|
3012
3022
|
// Same rule for the cloud block: names, hashes and counts only. A null block (extraction failed) already
|
|
3013
3023
|
// printed a WARNING note, and is omitted rather than carried forward from the previous release.
|
|
3014
3024
|
if (res.cloud) {
|
|
@@ -3065,8 +3075,17 @@ async function cmdSync(args) {
|
|
|
3065
3075
|
// stamp the previous release's surface with this release's identity. Unobserved is written as
|
|
3066
3076
|
// `observed:false`, which the release preflight refuses to ship.
|
|
3067
3077
|
desktopInitSurface: res.desktopInitSurface,
|
|
3078
|
+
// The env key NAMES Desktop's own host-loop spawn carried on THIS build, from its local log. Carried forward
|
|
3079
|
+
// by the reader only for the same build (log rotation); never from a previous release.
|
|
3080
|
+
observedSpawnEnv: res.observedSpawnEnv,
|
|
3068
3081
|
},
|
|
3069
3082
|
}, res.cloud);
|
|
3083
|
+
// Desktop's observed host-loop env against what the harness host loop spawns for the baseline about to be written.
|
|
3084
|
+
{
|
|
3085
|
+
const cmp = checkObservedAgainstHarness(res.observedSpawnEnv, harnessHostLoopKeys(next));
|
|
3086
|
+
res.unknownDeltas.push(...cmp.deltas);
|
|
3087
|
+
res.notes.push(...cmp.notes);
|
|
3088
|
+
}
|
|
3070
3089
|
const diffFlag = !!syncParsed.flags["--diff"];
|
|
3071
3090
|
if (diffFlag) {
|
|
3072
3091
|
// Diff against `base` (the latest committed baseline `next` was merged onto), NOT a separate read of
|
|
@@ -3092,12 +3111,12 @@ async function cmdSync(args) {
|
|
|
3092
3111
|
log(" - " + n);
|
|
3093
3112
|
}
|
|
3094
3113
|
if (res.unknownDeltas.length) {
|
|
3095
|
-
log("\n⚠ unknown deltas (
|
|
3114
|
+
log("\n⚠ unknown deltas (each line names the file to fix):");
|
|
3096
3115
|
for (const d of res.unknownDeltas)
|
|
3097
3116
|
log(" - " + d);
|
|
3098
3117
|
// unknown deltas block the write unless --diff (diagnosis mode) is active.
|
|
3099
3118
|
if (!diffFlag && !allowEmpty) {
|
|
3100
|
-
fail("sync", "runtime", "Refusing to write baseline with unknown deltas. Fix
|
|
3119
|
+
fail("sync", "runtime", "Refusing to write baseline with unknown deltas. Fix the file each delta names, or pass --allow-empty to force-write.", undefined, isJsonOutput(normalizedArgs), 1);
|
|
3101
3120
|
}
|
|
3102
3121
|
}
|
|
3103
3122
|
if (!diffFlag) {
|
package/dist/runtime/hostloop.js
CHANGED
|
@@ -72,7 +72,7 @@ export const VM_LOOP_TOOL_ALIASES = { WebFetch: "mcp__workspace__web_fetch" };
|
|
|
72
72
|
* 3. the authored `agentEnv` knob, applied last so it always wins.
|
|
73
73
|
*/
|
|
74
74
|
export function buildHostLoopNativeEnv(baseline, opts) {
|
|
75
|
-
const nativeEnv = { ...process.env };
|
|
75
|
+
const nativeEnv = { ...(opts.operatorEnv ?? process.env) };
|
|
76
76
|
for (const k of SCRUBBED_AGENT_ENV_KEYS)
|
|
77
77
|
delete nativeEnv[k];
|
|
78
78
|
delete nativeEnv[AUTO_MEMORY_ENV_KEY]; // owned by hostNativeSpawnEnv's autoMemoryEnv overlay — see AUTO_MEMORY_ENV_KEY
|