cowork-harness 4.8.1 → 4.9.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 +12 -12
- 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 +2 -2
- package/.claude/skills/cowork-harness/references/assertion-catalog-outcome-files-tools.md +3 -3
- package/.claude/skills/cowork-harness/references/assertion-catalog.md +20 -5
- package/.claude/skills/cowork-harness/references/assertions-guide.md +1 -1
- package/.claude/skills/cowork-harness/references/authoring.md +36 -9
- package/.claude/skills/cowork-harness/references/ci-recipe.md +5 -5
- package/.claude/skills/cowork-harness/references/critique.md +4 -1
- package/.claude/skills/cowork-harness/references/debugging.md +4 -2
- package/.claude/skills/cowork-harness/references/eval.md +40 -9
- package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +3 -2
- package/.claude/skills/cowork-harness/references/gotchas.md +7 -6
- package/.claude/skills/cowork-harness/references/hillclimb-recipe.md +17 -5
- package/.claude/skills/cowork-harness/references/hillclimb.md +22 -5
- package/.claude/skills/cowork-harness/references/measurement.md +2 -2
- package/.claude/skills/cowork-harness/references/run-record-replay.md +55 -4
- package/.claude/skills/cowork-harness/references/scenario-schema.md +95 -15
- package/.claude/skills/cowork-harness/references/semantic-judging.md +1 -1
- package/.claude/skills/cowork-harness/references/task-recipes.md +59 -4
- package/.claude/skills/cowork-harness/scripts/assertion-keys.json +41 -0
- package/.claude/skills/cowork-harness/scripts/scenario.py +115 -5
- package/CHANGELOG.md +283 -0
- package/CONTRIBUTING.md +6 -4
- package/README.md +17 -4
- package/RELEASING.md +14 -9
- package/SPEC.md +52 -8
- package/baselines/desktop-2.31226.1.json +39 -6
- package/dist/baseline.js +1 -0
- package/dist/bounded-spawn.js +146 -0
- package/dist/cli.js +210 -25
- package/dist/critique/cluster-lexical.js +126 -0
- package/dist/critique/command.js +30 -76
- package/dist/critique/compare.js +73 -4
- package/dist/decide/decider.js +91 -14
- package/dist/errors.js +26 -1
- package/dist/eval/classify.js +58 -0
- package/dist/eval/command.js +52 -23
- package/dist/eval/families.js +79 -0
- package/dist/eval/plan-history.js +4 -0
- package/dist/eval/plan.js +39 -13
- package/dist/eval/report.js +356 -11
- package/dist/eval/runs.js +12 -0
- package/dist/eval/stats.js +401 -8
- package/dist/eval/usage.js +8 -5
- package/dist/fixture/export.js +23 -4
- package/dist/fixture/workspace.js +5 -2
- package/dist/hillclimb/cases.js +2 -2
- package/dist/hillclimb/check.js +63 -0
- package/dist/hillclimb/cli.js +7 -4
- package/dist/hillclimb/command.js +5 -1
- package/dist/hillclimb/freeze-ref.js +6 -3
- package/dist/hillclimb/grade-keys.js +2 -7
- package/dist/hillclimb/metric-keys.js +14 -0
- package/dist/hillclimb/regrade.js +110 -16
- package/dist/hillclimb/run-command.js +12 -1
- package/dist/hillclimb/state-template.js +6 -2
- package/dist/hillclimb/usage.js +4 -1
- package/dist/hostloop/remote-devices-handler.js +66 -1
- package/dist/metric-command/interpreters.js +50 -0
- package/dist/metric-command/live.js +177 -0
- package/dist/metric-command/output.js +23 -0
- package/dist/metric-command/resolve.js +129 -0
- package/dist/metric-command/run.js +183 -0
- package/dist/metric-command/slice.js +41 -0
- package/dist/metric-sig.js +28 -0
- package/dist/metrics.js +38 -10
- package/dist/refs/compose.js +5 -1
- package/dist/run/cassette.js +379 -20
- package/dist/run/doctor.js +19 -0
- package/dist/run/envelope.js +12 -8
- package/dist/run/execute.js +236 -45
- package/dist/run/host-action-runners.js +186 -0
- package/dist/run/host-action.js +250 -0
- package/dist/run/inspect-view.js +9 -3
- package/dist/run/latest-run.js +9 -0
- package/dist/run/leg-manifest.js +66 -0
- package/dist/run/leg-snapshot.js +264 -0
- package/dist/run/lint-load.js +28 -9
- package/dist/run/migrate-run-dir.js +9 -0
- package/dist/run/multi-leg-cassette.js +458 -0
- package/dist/run/multi-leg-scenario.js +194 -0
- package/dist/run/multi-leg.js +563 -0
- package/dist/run/regrade-usage.js +5 -3
- package/dist/run/regrade.js +35 -5
- package/dist/run/run-dir-identity.js +6 -2
- package/dist/run/run-index.js +32 -1
- package/dist/run/run-labels.js +6 -1
- package/dist/run/run.js +11 -2
- package/dist/run/runs-gc.js +12 -2
- package/dist/run/scenario-result.js +41 -0
- package/dist/run/tier-vacuous-tools.js +30 -7
- package/dist/run/trace-view.js +36 -7
- package/dist/run/turn-layout.js +134 -3
- package/dist/run/verdict.js +36 -1
- package/dist/run/verify-context.js +24 -2
- package/dist/runtime/argv.js +18 -1
- package/dist/runtime/container.js +21 -18
- package/dist/runtime/device-container.js +408 -66
- package/dist/runtime/device-image.js +73 -0
- package/dist/runtime/device-tree.js +144 -0
- package/dist/runtime/microvm.js +3 -10
- package/dist/runtime/protocol.js +9 -3
- package/dist/runtime/stage.js +27 -2
- package/dist/search-corpus/cli.js +142 -0
- package/dist/search-corpus/harvest.js +186 -0
- package/dist/search-corpus/pool.js +361 -0
- package/dist/search-corpus/render.js +117 -0
- package/dist/search-corpus/retrieval.js +161 -0
- package/dist/session.js +11 -2
- package/dist/sync/agent-search-gates.js +98 -0
- package/dist/sync/capture-org.js +128 -0
- package/dist/sync/cowork-sync.js +25 -2
- package/dist/sync/observed-spawn-env.js +3 -3
- package/dist/sync/search-anchors.js +139 -0
- package/dist/types.js +138 -5
- package/docker/device/Dockerfile +20 -0
- package/docker/device/ld.so.preload +1 -0
- package/docker/device/nodelete.c +517 -0
- package/docs/README.md +4 -2
- package/docs/boundary.md +4 -3
- package/docs/cassette.md +41 -1
- package/docs/ci.md +1 -1
- package/docs/cli.md +68 -16
- package/docs/companion-skill.md +2 -2
- package/docs/critique.md +33 -1
- package/docs/eval.md +112 -12
- package/docs/fidelity-gaps.md +65 -15
- package/docs/hillclimb.md +18 -7
- package/docs/invariants.md +4 -0
- package/docs/maintenance.md +44 -2
- package/docs/scenario.md +247 -10
- package/docs/stats.md +7 -1
- package/docs/subagents.md +1 -1
- package/examples/README.md +2 -0
- package/examples/replays/README.md +1 -1
- package/examples/scenarios/csv-metrics-command.yaml +53 -0
- package/examples/scenarios/metrics/count_bash_calls.py +36 -0
- package/examples/sessions/resumable-probe.yaml +10 -0
- package/llms.txt +5 -1
- package/package.json +2 -2
- package/python/metric-runner.py +127 -0
- package/python/test_scenario_lint.py +51 -1
- package/schema/cassette.v18.json +594 -0
- package/schema/critique-compare.json +58 -1
- package/schema/regrade.json +2 -1
- package/schema/run-result.json +47 -5
- package/schema/scenario-multi-leg.schema.json +287 -0
- package/schema/scenario.schema.json +110 -7
- package/scripts/check-surface.ts +36 -0
- package/scripts/check-versions.ts +2 -1
- package/scripts/eval-mds-table.py +29 -0
- package/scripts/gen-schema.ts +34 -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, 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.
|
|
7
|
-
tracks-harness: cowork-harness 4.
|
|
6
|
+
version: 4.9.0
|
|
7
|
+
tracks-harness: cowork-harness 4.9.0 (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.
|
|
29
|
+
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 4.9.0` (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
|
|
|
@@ -35,15 +35,15 @@ Read them.
|
|
|
35
35
|
The 10-second inner loop, once the CLI is on PATH:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
cowork-harness doctor # prerequisites OK? (Docker, agent, token, baseline)
|
|
38
|
+
cowork-harness doctor # prerequisites OK? (Docker, agent, token, baseline, device image)
|
|
39
39
|
cowork-harness skill ./my-skill "do X" --model claude-sonnet-5 # run once (or set COWORK_HARNESS_MODEL)
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Before the first command, confirm the CLI is reachable and **fail loud** (never fake a pass) when a tier's dependencies are missing:
|
|
43
43
|
|
|
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
|
|
44
|
+
- **One-shot check.** Run `cowork-harness doctor [--tier <tier>]` first — a read-only prerequisite check that inspects Docker, the staged agent, the token, the baseline and the `device_bridge` image (built, or built on the first bridge run) in one pass. The bullets below explain each thing it checks (and how to fix it).
|
|
45
45
|
- **Replay-only? Skip `doctor`.** Replaying committed cassettes needs no Docker, no staged agent, and no token — and every tier's `doctor` validates the auth token (the live tiers also Docker + the staged agent), so a ✗ there is expected, not a blocker. Go straight to `cowork-harness replay <cassette>`.
|
|
46
|
-
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 4.
|
|
46
|
+
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 4.9.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@^4.9.0" <cmd>` (Node ≥ 22), or install once with `npm i -g "cowork-harness@^4.9.0"`. **Pin `@^4.9.0`, never `@latest`** — `@latest` can silently fetch an older CLI and the new commands fail as "unknown command", whereas the floor **fails loud** if no compatible version is published.
|
|
47
47
|
|
|
48
48
|
This skill documents the CURRENT surface, not release history. If `cowork-harness --version` is
|
|
49
49
|
OLDER than the floor, the per-release record of what you are missing is [CHANGELOG.md](https://github.com/yaniv-golan/cowork-harness/blob/main/CHANGELOG.md)
|
|
@@ -106,7 +106,7 @@ CI-grade scenario, and the post-hoc debug loop; the rest are narrower tools that
|
|
|
106
106
|
|
|
107
107
|
Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · migrate-run-dir · lint ·
|
|
108
108
|
lint-skill · analyze-skill · probe-dispatch ·
|
|
109
|
-
verify-run · regrade · fixture · ref <freeze|verify> · trace · inspect · diff · critique · eval · eval report · hillclimb <run|check|state-template|freeze-ref|regrade> · stats · decide · gates · answer · scaffold · assertions --list · sync ·
|
|
109
|
+
verify-run · regrade · fixture · search-pool <build|export|import> · ref <freeze|verify> · trace · inspect · diff · critique · eval · eval report · hillclimb <run|check|state-template|freeze-ref|regrade> · stats · decide · gates · answer · scaffold · assertions --list · sync ·
|
|
110
110
|
list · boundary-check · status · vm <init|status|delete|prune> · doctor · init-redact`. Always check `cowork-harness <cmd> --help`.
|
|
111
111
|
|
|
112
112
|
## Invariants — how a green run lies
|
|
@@ -155,23 +155,23 @@ behind each, is [`references/gotchas.md`](references/gotchas.md).
|
|
|
155
155
|
|
|
156
156
|
| File | Read it for |
|
|
157
157
|
|---|---|
|
|
158
|
-
| [`references/authoring.md`](references/authoring.md) | session vs scenario, discovery, fidelity tier, the answer-channel decision tree, `web_fetch`, scaffold + lint |
|
|
158
|
+
| [`references/authoring.md`](references/authoring.md) | session vs scenario, discovery, fidelity tier, the answer-channel decision tree, `web_fetch`, a skill that resumes in a later session (`legs:`), scaffold + lint |
|
|
159
159
|
| [`references/assertions-guide.md`](references/assertions-guide.md) | the two assertion axes, the goal → key map |
|
|
160
|
-
| [`references/run-record-replay.md`](references/run-record-replay.md) | run / `verify-run`, recording and cassette placement, real-document validation, verdict signals (incl. where a relative or `outputs/` path lands per tier), background-run liveness, CI lanes |
|
|
160
|
+
| [`references/run-record-replay.md`](references/run-record-replay.md) | run / `verify-run`, multi-leg runs and `--leg`, recording and cassette placement, real-document validation, verdict signals (incl. where a relative or `outputs/` path lands per tier), background-run liveness, CI lanes |
|
|
161
161
|
| [`references/measurement.md`](references/measurement.md) | `--repeat`, `--ablate-skill`, measurement hygiene |
|
|
162
162
|
| [`references/debugging.md`](references/debugging.md) | triage, `result.json` fields and `trace` views, `chat` |
|
|
163
163
|
| [`references/gotchas.md`](references/gotchas.md) | the full "✓ passed ≠ correct" landmine catalog |
|
|
164
|
-
| [`references/task-recipes.md`](references/task-recipes.md) | start here for "how do I X": evolve `assert:`, audit tier drift, redaction, budgets, answer quality, and goals with no flag (force a compaction, ablate a section, a form reply, resume in a new conversation, hook JSON decisions, schema checks, unattended runs) |
|
|
164
|
+
| [`references/task-recipes.md`](references/task-recipes.md) | start here for "how do I X": evolve `assert:`, audit tier drift, redaction, budgets, answer quality, and goals with no flag (force a compaction, ablate a section, a form reply, resume in a new conversation, hook JSON decisions, schema checks, unattended runs), keeping a skill's web searches (`search-pool`), and a skill that parks and resumes in a later session (`legs:`) |
|
|
165
165
|
| [`references/assertion-catalog.md`](references/assertion-catalog.md) | the assertion catalog's index: conventions shared by every key, the verdict-signal table, and links to the per-family files below |
|
|
166
166
|
| [`references/assertion-catalog-outcome-files-tools.md`](references/assertion-catalog-outcome-files-tools.md) | per-key rows: outcome, transcript, files and artifacts, tools |
|
|
167
167
|
| [`references/assertion-catalog-agents-skills-budgets.md`](references/assertion-catalog-agents-skills-budgets.md) | per-key rows: sub-agents, skills and connectors, budgets, tasks, delivery |
|
|
168
168
|
| [`references/assertion-catalog-gates-hooks-modifiers.md`](references/assertion-catalog-gates-hooks-modifiers.md) | per-key rows: gates, hooks, path denial, verdict modifiers, egress, judged keys |
|
|
169
169
|
| [`references/semantic-judging.md`](references/semantic-judging.md) | `semantic_matches` in full: what the judge reads, fork results, refusal reasons, provenance |
|
|
170
|
-
| [`references/scenario-schema.md`](references/scenario-schema.md) | every YAML field, which keys survive `replay`, the `web_fetch` model |
|
|
170
|
+
| [`references/scenario-schema.md`](references/scenario-schema.md) | every YAML field, the multi-leg (`legs:`, `host_action`, `workdir`) form, which keys survive `replay`, the `web_fetch` model |
|
|
171
171
|
| [`references/fidelity-and-answers.md`](references/fidelity-and-answers.md) | tier semantics, answer paths, the determinism contract |
|
|
172
172
|
| [`references/ci-recipe.md`](references/ci-recipe.md) | the GitHub Action, replay-vs-live lanes, the four-stage pipeline |
|
|
173
173
|
| [`references/critique.md`](references/critique.md) | `critique` report and evidence-package shapes |
|
|
174
|
-
| [`references/eval.md`](references/eval.md) | `eval`: paired before/after of two plugin versions — labels, refusals, exit codes, files |
|
|
174
|
+
| [`references/eval.md`](references/eval.md) | `eval`: paired before/after of two plugin versions — labels, metric rows, refusals, exit codes, files |
|
|
175
175
|
| [`references/hillclimb.md`](references/hillclimb.md) | `hillclimb run` / `check` / `state-template` / `freeze-ref` / `regrade` for a `/claude-api hillclimb` loop — pass `--flow` to each, the harness gate, resume, pairwise references, re-grading, refusals, exit codes |
|
|
176
176
|
| [`references/hillclimb-recipe.md`](references/hillclimb-recipe.md) | Recipe 7: the `/claude-api hillclimb` loop with the harness as its runner, step by step — the runner command, Step 0.5 checks, metrics, spend, rounds, stalls |
|
|
177
177
|
| `scripts/scenario.py` | `scaffold`, `lint`, `lint-skill`, `resolve-agent-types <plugin-dir>` (validates a pinned `subagent_type` against `plugin.json` + `agents/*.md`) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertion catalog: sub-agents, skills and connectors, budgets, tasks, delivery
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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
|
|
|
@@ -17,7 +17,7 @@ refused at load: no question reaches the harness to grade.
|
|
|
17
17
|
| `gate_answers_delivered: true` | every answered gate's answer reached the model (observed `tool_result`; unobserved = fail); **zero gates fired passes vacuously** — pair with `gate_answer_count_min: >= 1` to also require a gate, or drop this key and declare `questions_count_max: 0` in a scenario that expects none |
|
|
18
18
|
| `gate_answers_delivered: false` | asserts at least one answered gate's answer was **confirmed not delivered** (an observed delivery failure); an unobserved/null delivery does **not** satisfy this — for negative-path delivery tests. Requires a gate, so **mutually exclusive** with `questions_count_max: 0` (refused by `run`/`skill`/`record`) |
|
|
19
19
|
| `gate_answer_count_min: <N>` | at least N AskUserQuestion gates fired AND were delivered non-error — presence companion to `gate_answers_delivered`'s vacuous-pass. **`: 0` asserts nothing** and does not satisfy that pairing; `>= 1` is **mutually exclusive** with `questions_count_max: 0` (refused by `run`/`skill`/`record`) |
|
|
20
|
-
| `gates_all_scripted: true \| {include_permissions: true}` | every AskUserQuestion gate that fired was answered by a scripted rule (`answers:` / `--answer`) — not the LLM decider, `first`, or an external or human decider; a failure names each such gate and who answered it. A gate asked but never answered fails. **Evidence:** the run's decisions — a result with no decisions record, or an answer with no recognisable source, fails evidence-unavailable, never passes. On replay each gate is re-classified against the cassette's frozen `answers:`; that is evidence-unavailable when those answers were redacted, or when the cassette records that a live decider answered a gate yet its frozen rules cover every gate (they are not the rules it recorded with). **`{include_permissions: true}`** also checks tool permissions: a scripted `when_tool` rule or a fixed rule counts (a Read/Glob/Grep registry allow, a strict-parity deny, the harness's own path-gate deny, the fail-closed deny when nothing answered live — on replay that row means the recording held no answer, evidence-unavailable); a web_fetch deny that no answer source decided fails (`answered by abstain-fallback`), because real Cowork asks the user; cowork parity's permissive off-registry auto-allow never does, and a replayed web_fetch permission cannot be attributed (evidence-unavailable). Zero gates passes (nothing needed a person), so pair it with `gate_answer_count_min: >= 1` or `questions_count_max` to state whether gates were expected (`lint` warns `unpaired-gates-all-scripted`). |
|
|
20
|
+
| `gates_all_scripted: true \| {include_permissions: true}` | every AskUserQuestion gate that fired was answered by a scripted rule (`answers:` / `--answer`) — not the LLM decider, `first`, or an external or human decider; a failure names each such gate and who answered it. A gate asked but never answered fails. **Evidence:** the run's decisions — a result with no decisions record, or an answer with no recognisable source, fails evidence-unavailable, never passes. On replay each gate is re-classified against the cassette's frozen `answers:`; that is evidence-unavailable when those answers were redacted, or when the cassette records that a live decider answered a gate yet its frozen rules cover every gate (they are not the rules it recorded with). **`{include_permissions: true}`** also checks tool permissions: a scripted `when_tool` rule or a fixed rule counts (a Read/Glob/Grep registry allow, a strict-parity deny, the harness's own path-gate deny, the fail-closed deny when nothing answered live — on replay that row means the recording held no answer, evidence-unavailable); a web_fetch deny that no answer source decided fails (`answered by abstain-fallback`), because real Cowork asks the user (under the default `on_unanswered: fail` such a fetch ends the run as an unanswered gate before any deny, so this arises only under `first` or `llm`); cowork parity's permissive off-registry auto-allow never does, and a replayed web_fetch permission cannot be attributed (evidence-unavailable). Zero gates passes (nothing needed a person), so pair it with `gate_answer_count_min: >= 1` or `questions_count_max` to state whether gates were expected (`lint` warns `unpaired-gates-all-scripted`). |
|
|
21
21
|
| `hook_blocked: <regex>` | one of the harness's own PreToolUse hook callbacks blocked a tool whose name matches the regex (`RunResult.hookEvents`; a plugin's hook never reaches it: use `hook_event_blocked`) — evidence-unavailable if hook telemetry is absent. Replay: needs a `controlOut` cassette (a custom hook's decision lives only there, not the recorded stream). **Does not see the agent's own refusal**: on `hostloop` against Desktop 2.7032.0+ a relative `Read`/`Write`/`Edit` is denied by the agent's permission rules before any hook runs, so neither this key nor `path_denied` records it — assert it with `tool_result_contains: "denied by your permission settings"` |
|
|
22
22
|
| `no_hook_blocked: true` | no tool was blocked by the harness's own hook callbacks (a plugin's hook never reaches it: use `no_hook_event_blocked`) (distinguishes a real tool crash from an intentional hook block) — evidence-unavailable if hook telemetry is absent. Replay: needs a `controlOut` cassette. **Only `true` is valid** |
|
|
23
23
|
| `hook_event_fired: <HookEvent>` | a **command hook** for this event (a plugin's `hooks/hooks.json` or manifest hook — `Stop`, `SessionStart`, `PostToolUse`, …) ran: a `hook_response` system frame with that `hook_event` was recorded (`RunResult.contextEvents`). Any outcome counts. The harness passes `--include-hook-events` whenever a staged plugin declares hooks — that is what puts events other than SessionStart/Setup on the stream — so a recording made without it reports "never fired". Content-class, grades on replay. Recorded end-to-end for `Stop` ([stop-hook-probe.scenario.yaml](https://github.com/yaniv-golan/cowork-harness/blob/main/examples/probes/stop-hook-probe.scenario.yaml)) and for `PreToolUse` at `container` (deciding by JSON and by exit 2); the other names match the same frame but have not each been recorded |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertion catalog: outcome, transcript, files and artifacts, tools, tool results
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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
|
|
|
@@ -14,7 +14,7 @@ keys is an AND; glob vs regex name matching) and the verdict-signal table are in
|
|
|
14
14
|
| `file_exists: <path>` | the path exists under the run's `work/` (anchored at `mnt/`, e.g. `outputs/x.md`). For a user-facing deliverable prefer `user_visible_artifact` — with a connected folder the file lands in `mnt/<folder>` (= `{{workspaceFolder}}`), not `mnt/outputs`, so `file_exists: outputs/x.md` misses it. Object form `{path, authored}`: `authored: true` also requires that THIS run created or rewrote the file (an untouched pre-run file — e.g. from a `workspace_fixture` — fails; no pre-run manifest ⇒ evidence-unavailable); `authored: false` states that inheriting it is fine. On a `workspace_fixture` path one of the two is REQUIRED (refused at load otherwise). On `lane: remote` plain `file_exists` is the proxy for a written path; with `authored: true` it is rejected at load. Under `device_bridge`, a path in a connected folder is refused: the folders live in the device container; assert the `device_bash` call instead. |
|
|
15
15
|
| `user_visible_artifact: <path>` | exists **and** under a user-visible root (`outputs/` + each connected folder's mount name) — the right primitive for a workspace deliverable when a folder is connected. Takes the same `{path, authored}` object form as `file_exists` (and `artifact_text`/`artifact_json` take an `authored` field) |
|
|
16
16
|
| `no_delete_in_outputs: true` | no delete op touched `mnt/outputs` — **only `true` is valid**; `false` is rejected. Checks on **every** baseline. Omitting it: on a baseline recording outputs `rw` (Desktop before 2.16120.0) a detected delete still fails via the `outputs_delete` verdict signal, which fires *because* the key was not authored (`allow_outputs_delete: true` accepts an intended one); on `rwd` (2.16120.0+, including `latest`, where Cowork allows deletes in outputs) nothing checks outputs deletes, so author this key to keep the check. Detection is a post-run bash-command scan plus a per-turn filesystem diff of `outputs/`, not mount enforcement, so a green means none was *detected*. Fails when the diff proves the delete, when a delete in command/call position has an `outputs/` path as its own operand, or when the diff could not verify; a hit resting only on the detector's inference (e.g. a Python variable named `rm`) with a clean diff passes, and the `outputs_delete_unconfirmed` warn is still raised in the run output — see that code for the classes of real delete that land there |
|
|
17
|
-
| `no_delete_in_mounts: true` | no delete op touched `outputs` or any `rw` connected folder, except mounts waived by `allow_delete_in`. Covers `outputs` on **every** baseline, including those where Cowork allows an outputs delete (unless outputs is waived, authoring it arms the outputs check, so a filesystem-proven delete fails as the `outputs_delete` signal, which `allow_outputs_delete` waives). Production denies `unlink`/`rmdir` on a `rw` connected folder until approval, so `no_delete_in_outputs` covers only part of the rule. Under `device_bridge` it
|
|
17
|
+
| `no_delete_in_mounts: true` | no delete op touched `outputs` or any `rw` connected folder, except mounts waived by `allow_delete_in`. Covers `outputs` on **every** baseline, including those where Cowork allows an outputs delete (unless outputs is waived, authoring it arms the outputs check, so a filesystem-proven delete fails as the `outputs_delete` signal, which `allow_outputs_delete` waives). Production denies `unlink`/`rmdir` on a `rw` connected folder until approval, so `no_delete_in_outputs` covers only part of the rule. Under `device_bridge` it compares the device copy of each `rw` folder before and after the run (a delete the user allowed through the delete card counts), and reads `device_bash` commands only on a run whose device did not refuse deletes (`deleteRefusal: "reported"`). **only `true` is valid** |
|
|
18
18
|
| `no_unexpected_files: [<glob>, …]` | every **newly created** file under a user-visible root matches ≥1 glob (workRoot-relative paths; `**` = whole path segment for any depth — use `outputs/handoff/**` for per-run subdirs); `[]` = no new files; **new-files-only** — overwriting a pre-existing file in place is invisible (use content-level producer stamping); live/verify-run without a pre-run manifest ⇒ evidence-unavailable hard-fail (live runs capture the baseline only when this key is asserted; recordings always capture); an **incomplete post-run filesystem walk** (an unreadable subtree — a permission/I-O error) also fails evidence-unavailable rather than reporting "no strays" over a partial tree — distinct from the missing-manifest case (a `--resume` run), which fails for a different reason; captured on every live sandbox tier including microvm (its outputs are snapshotted from the VM into the run dir); replay needs `cassette.preRunPaths` (≥0.24 recordings) — cassettes without it **exclude** the key with a loud warning. ⚠️ **Not a stand-in for "file X must not exist":** there is no negative-existence key, and inverting this one is a different claim — it is new-files-only (a pre-existing file is invisible to it) and needs a pre-run manifest. Assert the content-level consequence instead (`tool_not_called`, or the absent `artifact_json`) rather than widening an allowlist for every incidental lock/temp file. Rejected at load on `lane: remote`. |
|
|
19
19
|
| `input_unmodified: <glob> \| [<glob>, …]` | a single glob or a list; every **pre-existing** file (incl. uploaded files under `uploads/**`) whose workRoot-relative path matches ≥1 glob keeps an unchanged content hash after the run — the in-place-mutation companion to `no_unexpected_files`'s new-files check (`[]` is rejected by the schema — list at least one glob); a glob that matches **no** pre-run path fails loud (a typo or renamed mount would otherwise verify zero files and pass vacuously); a matched file that was deleted counts as a content change (fails); live/verify-run without a pre-run hash manifest ⇒ evidence-unavailable hard-fail (a `--resume` run); captured on every live sandbox tier including microvm; replay needs `cassette.preRunHashes` — cassettes without it **exclude** the key with a loud warning; on replay it compares against the manifest's recorded `sha256`, never a re-hash of the materialized tree. On `lane: remote` it still reads the local stand-in for the inputs, which is right there: an edit to a staged copy inside the cloud container does not reach the device. Under `device_bridge`, a glob on a connected folder's path is refused (the folders live in the device container). |
|
|
20
20
|
| `self_heal_ran: <bool>` | a bash command the model wrote did (not) name a plugin under `/sessions/<id>/mnt/.local-plugins/` or `/sessions/<id>/mnt/.remote-plugins/` — the plugin-root self-heal path. It reads the command as the model wrote it, so a host plugin path that `hostloop`'s bash tool rewrote to the VM mount does not count |
|
|
@@ -26,7 +26,7 @@ keys is an AND; glob vs regex name matching) and the verdict-signal table are in
|
|
|
26
26
|
| `tool_called: {tool: <glob> \| [..], input?, input_any?, result?, scope?, subagent_type?, count?}` | **object form** — asserts what a call CARRIED, where it RAN and what its paired result SAID, which no name glob and no `transcript_*` key can see (`transcript_*` reads top-level prose only, so it passes when the agent merely *says* it ran a command). `tool`: a glob or a list of globs (any-of; list both shells, `[Bash, mcp__workspace__bash]`, for a claim that must hold at hostloop too). `input: {<field>: <regex>}`: every named top-level input field must match (case-insensitive, unanchored; a missing field is no match; a non-string value is matched as JSON). `input_any: <regex>`: some top-level field matches. `result: {matches?, not_matches?, is_error?}`: predicates on the call's paired `tool_result` (by `toolUseId`, 10,240 characters of text — 32,768 for a top-level `Skill` result). `scope`: `main` (default — the main agent, including a `Skill`'s or `Agent(fork)`'s children, the set the string form reads), `subagent` (the parent is a sub-agent dispatch this run recorded, at any depth), or `any` (also calls whose parent is not a recorded dispatch, e.g. a `Skill` invoked inside a sub-agent). `subagent_type` (with `scope: subagent`): regex over the IMMEDIATE parent dispatch's type or description. `count: {min?, max?}` (default `min: 1`). **Fails closed:** an unpaired call never satisfies `result`; a truncated result or input that cannot settle a predicate, or a result.json without `toolCalls`, reports *evidence unavailable*, never a pass or a plain "not called". A red lists the calls it considered and names matches in another scope ("1 matching call in scope subagent — set `scope: any`"). `{tool: X}` alone is exactly `tool_called: X`. A cassette using this form stamps **v13**. |
|
|
27
27
|
| `tool_not_called: {tool: <glob> \| [..], input?, input_any?, result?, scope?, subagent_type?}` | **object form** — no call in scope satisfies every predicate (no `count`). Same fields and fail-closed rules as above: a candidate whose result is unpaired or truncated, or whose input was truncated, is *evidence unavailable*, never a pass. Refused at load when EVERY listed tool is one the tier does not serve. ⚠️ **Redaction hazard — this is the dangerous direction.** A committed cassette is redacted, which rewrites the recorded inputs: an `input` regex naming a literal the policy rewrites (a home path, an email) cannot see its target on replay. On replay, any candidate call whose field (or paired result, for `result.matches`) carries a redaction token is *evidence unavailable*, whatever the regex — the evaluator cannot know what the token replaced — so a negative check never passes over rewritten bytes. `record` warns — naming the redacted call and the ways out (narrow `scope`/`tool`, the string form, or live-only) — and refuses the write (the exact guard), and `lint` flags a regex naming a redactable literal (`tool-input-regex-redactable`) — a heuristic that reads `.cowork-redact.json` from the current and scenario directories only, not the cassette's. Match a part of the input redaction leaves alone (the verb and flags, a workspace-relative path), or keep the check on a live gate. Note also that the default `scope: main` does not see a sub-agent's call — use `scope: any` for "nothing anywhere ran X". |
|
|
28
28
|
| **legacy tool names** | The agent binary canonicalizes legacy spellings (`Task`→`Agent`, `KillShell`/`KillBash`→`TaskStop`, …) while the spawn tool list still declares the LEGACY one — so the init inventory shows `Task` and every call is emitted as `Agent`. `tool_called`/`tool_not_called`/`subagent_tool_used`/`subagent_tool_absent` match EITHER spelling, globs included. Before this, `tool_called: "Task"` could never pass and `tool_not_called: "Task"` always did. |
|
|
29
|
-
| **tier vacuity — now REFUSED** | `tool_not_called` **and `subagent_tool_absent`** naming a tool the tier does not serve is rejected at scenario load (`Bash`/`WebFetch`/`NotebookEdit` at `hostloop`; `mcp__workspace__bash` at `container`/`microvm`; on those three tiers, `mcp__remote-devices__device_bash`/`get_device_info` without `device_bridge: true`, and every other `mcp__remote-devices__*` tool the harness never serves, bridge or not), because it could never be violated. `scenario.py lint` WARNs on the same set before any run. The table is CLOSED to tools the harness itself removes or registers — `--tools` gates only the built-in set while every tier separately passes `--mcp-config`, so a session-MCP tool name is offered without appearing in any tool list and is never refused. Globs are never refused. `subagent_tool_absent` is covered because it reads the tools sub-agents actually USED, not a per-dispatch declared list. Under-approximates on purpose: `REPL` at hostloop is vacuous too and is not caught, which is the right side to err on for a hard refusal. |
|
|
29
|
+
| **tier vacuity — now REFUSED** | `tool_not_called` **and `subagent_tool_absent`** naming a tool the tier does not serve is rejected at scenario load (`Bash`/`WebFetch`/`NotebookEdit` at `hostloop`; `mcp__workspace__bash` at `container`/`microvm`; on those three tiers, `mcp__remote-devices__device_bash`/`get_device_info`/`device_request_delete_permission` without `device_bridge: true`, and every other `mcp__remote-devices__*` tool the harness never serves, bridge or not; and `mcp__visualize__read_me`/`show_widget`, production's skill-argument form tools, which the harness never serves unless the session's own MCP config declares a `visualize` server), because it could never be violated. `scenario.py lint` WARNs on the same set before any run. The table is CLOSED to tools the harness itself removes or registers — `--tools` gates only the built-in set while every tier separately passes `--mcp-config`, so a session-MCP tool name is offered without appearing in any tool list and is never refused. Globs are never refused. `subagent_tool_absent` is covered because it reads the tools sub-agents actually USED, not a per-dispatch declared list. Under-approximates on purpose: `REPL` at hostloop is vacuous too and is not caught, which is the right side to err on for a hard refusal. |
|
|
30
30
|
| `reference_read: <regex>` | a skill `references/`/`scripts/` file whose path matches this **regex** was ACCESSED — main agent or sub-agents, via `Read`, `Grep`/`Glob` (`path` input), or a `Bash`/`mcp__workspace__bash` command naming the path. Regex is **unanchored + case-insensitive** (the shared helper every regex key uses). **Under-approximates by design:** the path must be rooted in the mounted plugin, so a `cd` into the skill dir then a bare `cat references/x.md`, a heredoc body, and a `$VAR`-built path are invisible. Fails **evidence unavailable** when the run recorded no observable tool stream. Replay-capable (cassettes freeze whole tool inputs) |
|
|
31
31
|
| `no_observed_reference_access: <regex>` | no OBSERVED access matched the regex — the progressive-disclosure check: a reference the skill's routing never reaches. Named `observed` because detection under-approximates (see above), so it is **not proof the file went unread** — an agent that `cd`s and `cat`s it passes. Fails **evidence unavailable** rather than passing vacuously when no observable tool stream was recorded |
|
|
32
32
|
| `tool_result_contains: <str>` | a tool result includes the literal string (content / replay-checkable — substring match) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertion catalog
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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
|
|
|
@@ -23,6 +23,18 @@ The per-key table is split by assertion family so each file stays well within wh
|
|
|
23
23
|
- [`assertion-catalog-agents-skills-budgets.md`](./assertion-catalog-agents-skills-budgets.md): subagent_*, dispatch, skills and connectors, cost / token / turn / tool-error budgets, tasks, scratchpad and present_files delivery.
|
|
24
24
|
- [`assertion-catalog-gates-hooks-modifiers.md`](./assertion-catalog-gates-hooks-modifiers.md): AskUserQuestion gates, hooks, path denial, allow_* verdict modifiers, host-path leaks, egress, MCP, RSS, semantic judging, artifact_json, computer:// links.
|
|
25
25
|
|
|
26
|
+
**A multi-leg scenario's `host_action` leg** takes its own keys plus the file keys only (it has no transcript):
|
|
27
|
+
|
|
28
|
+
| key | passes when |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `exit_code: <0-255>` | the action exited with that code (with no `exit_code` item, 0 is expected) |
|
|
31
|
+
| `stdout_matches: <regex>` | the action's stdout (capped, secret-scrubbed) matches; a no-hit on a truncated stdout is evidence-unavailable, not a fail |
|
|
32
|
+
| `stdout_not_matches: <regex>` | the action's stdout does not match |
|
|
33
|
+
| `file_exists`, `file_absent`, `artifact_json`, `artifact_text` | as below, against the leg's tree after the action |
|
|
34
|
+
|
|
35
|
+
A prompt leg takes every key in the per-family files. The generated lists are `multiLeg.*` in
|
|
36
|
+
`scripts/assertion-keys.json`; the form is in [`scenario-schema.md`](./scenario-schema.md#multi-leg-scenario-yaml-legs).
|
|
37
|
+
|
|
26
38
|
**Name-matching styles differ by key (don't mix them up):** `tool_called`, `tool_not_called`,
|
|
27
39
|
`subagent_tool_used`, `subagent_tool_absent` are **glob** (anchored, case-sensitive, `*`/`?`). `tool_available`,
|
|
28
40
|
`skill_triggered`/`no_skill_triggered`, `skill_available`, `connector_available`, `skill_tool_used`,
|
|
@@ -38,9 +50,9 @@ paraphrases (that re-records red). Structured JSON → assert it in YAML with **
|
|
|
38
50
|
`path` + operator, or `schema:` for a whole value's shape); use the pytest lane (`assert_artifact_json`) only for
|
|
39
51
|
predicates neither can express.
|
|
40
52
|
|
|
41
|
-
**VerdictSignals in `result.verdict.signals`:** `computeVerdict` pushes signals into `result.verdict.signals`;
|
|
53
|
+
**VerdictSignals in `result.verdict.signals`:** `computeVerdict` pushes signals into `result.verdict.signals`; twelve
|
|
42
54
|
are **fail**-severity (they flip the run's pass/exit code even though `result.result` itself stays
|
|
43
|
-
`"success"`) and
|
|
55
|
+
`"success"`) and fifteen are **warn**-severity (informational, never flip pass/fail). All twenty-seven signal
|
|
44
56
|
codes (`VerdictSignal["code"]` in `src/run/verdict.ts`):
|
|
45
57
|
|
|
46
58
|
| Code | Severity | Meaning |
|
|
@@ -53,7 +65,10 @@ codes (`VerdictSignal["code"]` in `src/run/verdict.ts`):
|
|
|
53
65
|
| `outputs_delete` | fail | An unauthorized delete touched `mnt/outputs` on a baseline that records outputs `rw` (not raised on `rwd`, Desktop 2.16120.0+, unless `no_delete_in_outputs` or `no_delete_in_mounts` (outputs not waived) is authored — with `no_delete_in_outputs` that assertion fails instead; with `no_delete_in_mounts` this signal itself fires, and its message names that key), confirmed: the per-turn filesystem diff proves it, a delete in command/call position has an `outputs/` path as its own operand, or the diff could not verify the turn. Authoring `no_delete_in_outputs` moves it into that assertion; `allow_outputs_delete` waives it |
|
|
54
66
|
| `outputs_delete_unconfirmed` | warn | A delete-shaped command near `mnt/outputs` nothing confirms: no output present at turn start was deleted and no flagged delete has an outputs path as its own operand (e.g. a Python variable named `rm` next to an outputs path, quoted prose, a `sed`/`grep` pattern). A *statement* is a fragment split on newline/`;`/`&&`/`\|\|`, quote-blind, so some real deletes also land here — a loop body whose operand is the loop variable (`for f in …; do rm "$f"; done`), a `cd` then a relative path, chained variables (`A=…; B=$A/x; rm "$B"`), a Python path held in a variable set on another line (`p = …` then `os.remove(p)`, or `for p in …:` then `p.unlink()`), wrappers with flag combinations the classifier does not model (`sudo -Hu user rm`, `git -C dir rm`), and calls outside the modelled set such as Node's `fs.promises.rm(…)` — read the command. Raised even when `no_delete_in_outputs` is authored (that assertion passes on it); not raised on an `rwd` baseline (Desktop 2.16120.0+) unless that key, or `no_delete_in_mounts` with outputs not waived, is authored. Kept false positives that still fail `outputs_delete`: quoted text in which a delete command with an outputs operand follows a shell separator, subshell or keyword — the classifier does not track quotes (`echo 'note; rm mnt/outputs/x'`, `echo "a & rm …/outputs/x"`), and a heredoc that *writes* a script rather than running it (`cat <<EOF > clean.sh` with an `rm …/outputs/x` line). Waive: `allow_outputs_delete` |
|
|
55
67
|
| `outputs_diff_unavailable` | warn | The per-turn filesystem diff of `outputs/` could not verify this turn and the text scan flagged nothing — a delete made without a bash command would have gone undetected. With a text hit the turn fails `outputs_delete` instead. Like `outputs_delete_unconfirmed`, not raised on an `rwd` baseline unless `no_delete_in_outputs` or `no_delete_in_mounts` (outputs not waived) is authored |
|
|
56
|
-
| `mount_delete` | warn | A delete touched a delete-denied mount other than `outputs` (a `rw` connected folder). Production denies `unlink`/`rmdir` there until per-mount approval, so the run diverged. Warn, not fail: the harness detects post-hoc what production enforces. Author `no_delete_in_mounts` to hard-fail, or `allow_delete_in` to waive |
|
|
68
|
+
| `mount_delete` | warn | A delete touched a delete-denied mount other than `outputs` (a `rw` connected folder). Production denies `unlink`/`rmdir` there until per-mount approval, so the run diverged. Warn, not fail: the harness detects post-hoc what production enforces. Author `no_delete_in_mounts` to hard-fail, or `allow_delete_in` to waive. Not raised under `device_bridge` once the device refuses deletes (`deleteRefusal: "enforced"`): a delete left there is one the user allowed |
|
|
69
|
+
| `device_delete_bypassed` | fail | `device_bridge`: a file is gone from a connected folder deletion was never allowed in, past the device's refusal (a static binary or raw system call; `deviceBridge.bypassedDeletes` names them). Production would have refused it. Not waivable by `allow_delete_in`. Where the filesystem can read a rename followed by an edit as a delete (`rename-reads-as-delete`) it is `device_delete_unconfirmed` instead |
|
|
70
|
+
| `device_delete_unconfirmed` | warn | `device_bridge`, on a filesystem that can give a new file a deleted file's identity (`rename-reads-as-delete`): files are gone from a connected folder deletion was never allowed in, either deleted past the device's refusal or renamed and then edited, which that filesystem cannot tell apart |
|
|
71
|
+
| `device_audit_unreadable` | warn | `device_bridge`: the shim's delete log was replaced by the device shell (a link, a FIFO), so the number of refused deletes, and deletes of files the session made after a grant, are unknown (`deviceBridge.auditUnreadable`; `refusedDeletes` is left out). The before-and-after comparison of the folders still ran |
|
|
57
72
|
| `host_path_leak` | fail | A host path leaked into model-visible text (opt out: author `transcript_no_host_path`). A path copied verbatim from the scenario's own uploads, connected folders, prompt, or declared plugins' or local skills' files is not a leak |
|
|
58
73
|
| `l0_host_config_contamination` | fail | `protocol` ran against the operator's REAL config dir, so host-installed plugins/skills/MCP servers may have answered instead of the thing under test — the run did not necessarily measure it (opt out: `allow_l0_host_config_contamination`). Name predates the meaning: delivery at L0 works, contamination is what this reports |
|
|
59
74
|
| `missing_capability` | fail | A `requires_capabilities` need was unmet, or the skill used a capability the image omits (opt out: `allow_missing_capability`, or `skill --allow-missing-capability` on an open-ended run) |
|
|
@@ -72,4 +87,4 @@ codes (`VerdictSignal["code"]` in `src/run/verdict.ts`):
|
|
|
72
87
|
|
|
73
88
|
A **fail**-severity signal does not change `result.result` (still `"success"`), but it DOES fail the
|
|
74
89
|
overall run verdict and exit code — `assert result: success` alone won't catch it; check
|
|
75
|
-
`result.verdict.signals[].severity` or the run's exit code. Only the
|
|
90
|
+
`result.verdict.signals[].severity` or the run's exit code. Only the fifteen **warn** codes are truly benign.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Assertions guide
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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
|
|
|
@@ -79,12 +79,24 @@ computer, `mcp__remote-devices__device_bash`, from a second container holding CO
|
|
|
79
79
|
`$HOME/mnt/<folder-name>`; the agent's own container gets no folder. Assert on that call, never on the folder's
|
|
80
80
|
files: `tool_called: {tool: mcp__remote-devices__device_bash, result: {matches: <text>}}`, plus `transcript_matches`.
|
|
81
81
|
`file_exists` / `input_unmodified` on a connected folder's path are refused under the bridge (they would read the
|
|
82
|
-
agent's container). `
|
|
83
|
-
`
|
|
82
|
+
agent's container). A delete in an `rw` folder is refused until the user allows it through the delete card,
|
|
83
|
+
`mcp__remote-devices__device_request_delete_permission`, as in production: a skill that deletes there needs a
|
|
84
|
+
scripted answer for the card (`when_tool:` it, `decide: allow` or `deny`) or the run fails as unanswered; an `rwd`
|
|
85
|
+
folder allows deletes from the start. A move between connected folders copies and cannot remove its source, and git
|
|
86
|
+
leaves its lock files so the next commit fails on `HEAD.lock`, as in production; a rename within one folder
|
|
87
|
+
succeeds. `no_delete_in_mounts` covers the folders, an allowed delete included. A delete
|
|
88
|
+
that gets past the refusal fails the run (`device_delete_bypassed`). The first bridge run builds the device image
|
|
89
|
+
locally (the agent image plus the delete library; needs Docker Hub and apt, takes a few minutes) and rebuilds it when
|
|
90
|
+
the agent image changes.
|
|
91
|
+
`tool_not_called` on `device_bash`/`get_device_info`/`device_request_delete_permission` without the bridge, or on a device tool the harness never serves
|
|
84
92
|
(with it or without), is refused as vacuous. Egress follows the session allowlist;
|
|
85
93
|
`egress.unrestricted: true` matches production's pass-through. With no `permission_mode` in the session the agent
|
|
86
|
-
runs in `acceptEdits` (a production cloud task's mode under Manual); a mode you set wins. The
|
|
87
|
-
|
|
94
|
+
runs in `acceptEdits` (a production cloud task's mode under Manual); a mode you set wins. The delete card
|
|
95
|
+
(`mcp__remote-devices__device_request_delete_permission`) is served and never answered by the harness: script it with
|
|
96
|
+
`when_tool: … decide: allow|deny`, or an unscripted card fails the run as an unanswered gate (`first` declines it).
|
|
97
|
+
Under `permission_mode: bypassPermissions` a delete is granted at once with no card (`answeredBy: bypassPermissions`).
|
|
98
|
+
A `--decider-cmd` reply to the card is `{behavior}` only; a `grant` scope in it fails the run. The
|
|
99
|
+
run's `deviceBridge` lists every known difference from production, and `grants` who allowed what.
|
|
88
100
|
|
|
89
101
|
### Choose an answer path (gates: AskUserQuestion + tool-permission)
|
|
90
102
|
|
|
@@ -204,7 +216,7 @@ The `"first"` shorthand remains active only for the built-in `--on-unanswered fi
|
|
|
204
216
|
write an external helper, return a label name or option index — never the bare word `"first"` unless
|
|
205
217
|
your gate actually has a label called `"first"`.
|
|
206
218
|
|
|
207
|
-
### web_fetch (
|
|
219
|
+
### web_fetch (never auto-allowed, two-path)
|
|
208
220
|
|
|
209
221
|
`web_fetch` behaves unlike `curl`. A URL is gated by **provenance**, not the egress allowlist:
|
|
210
222
|
|
|
@@ -212,14 +224,29 @@ your gate actually has a label called `"first"`.
|
|
|
212
224
|
make a fetch succeed, put the URL in the prompt.
|
|
213
225
|
- **Provenanced** → fetches (still SSRF-guarded per redirect hop); the egress hostname allowlist is
|
|
214
226
|
**not consulted**.
|
|
215
|
-
- **Not provenanced** → raises a per-domain approval gate (`webfetch:<domain>`) that is
|
|
216
|
-
|
|
217
|
-
|
|
227
|
+
- **Not provenanced** → raises a per-domain approval gate (`webfetch:<domain>`) that is never
|
|
228
|
+
auto-allowed. Unanswered, it **ends the run as an unanswered gate** under `on_unanswered: fail`
|
|
229
|
+
(the `run` and `record` default), naming the domain, the URL and the rule to add; `--on-unanswered first` denies
|
|
230
|
+
it, and `llm` picks one of its three buttons (`Allow once`, `Allow all for website`, `Deny`). Answer it with a
|
|
231
|
+
scripted rule (`when_tool: "webfetch:<domain>"` + `decide: allow` and `grant: once|domain`, or `decide: deny`), a
|
|
232
|
+
session
|
|
218
233
|
`web_fetch.approved_domains`, or a live decider.
|
|
219
234
|
|
|
220
235
|
Surprise to remember: adding a host to `egress.extra_allow` is a **no-op** for a provenanced fetch.
|
|
221
236
|
Full model in `references/scenario-schema.md`.
|
|
222
237
|
|
|
238
|
+
### A skill that parks and resumes in a later session: `legs:`
|
|
239
|
+
|
|
240
|
+
When the skill stops (a question, a gate, a status file) and a NEW session resumes it, author ONE multi-leg scenario
|
|
241
|
+
instead of two scenarios glued together: `legs:` in place of `prompt:`, each leg a `prompt` (a fresh agent session)
|
|
242
|
+
or a `host_action` (an argv command in the previous leg's runtime, asserted with `exit_code`, `stdout_matches`,
|
|
243
|
+
`stdout_not_matches` and the file keys). Pick `workdir: persistent` (one tree across legs — a persistent workspace,
|
|
244
|
+
not Cowork) or `workdir: cowork-task` (a new tree per prompt leg, folders carried — a model of a new Cowork task).
|
|
245
|
+
Judge completion from the skill's own status file and ledger (`artifact_json`), never from the agent's prose. The
|
|
246
|
+
YAML is in [`scenario-schema.md`](./scenario-schema.md#multi-leg-scenario-yaml-legs), the walkthrough is Recipe 10 in
|
|
247
|
+
[`task-recipes.md`](./task-recipes.md), and the rules are in [docs/scenario.md → "Multi-leg scenarios"](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/scenario.md#multi-leg-scenarios-legs).
|
|
248
|
+
`scaffold` does not generate the multi-leg form; start from the resumable-probe example Recipe 10 links.
|
|
249
|
+
|
|
223
250
|
### Scaffold a valid scenario, then lint before you push
|
|
224
251
|
|
|
225
252
|
Don't hand-write the YAML from memory — that's how invented keys (`assertions:` vs `assert:`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CI recipe — replay vs live lanes
|
|
2
2
|
|
|
3
|
-
Self-contained reference. Tracks `cowork-harness 4.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 4.9.0` (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.
|
|
20
|
+
(e.g. `version: "4.9.0"`) instead when you want byte-reproducible CI.
|
|
21
21
|
|
|
22
22
|
Reach for the manual multi-step form below only when you need per-step control the Action's inputs don't
|
|
23
23
|
cover (a custom flag combination, a different runner matrix per step, or `lint`/`verify-cassettes` gated
|
|
@@ -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.
|
|
85
|
+
- run: npm i -g "cowork-harness@^4.9.0"
|
|
86
86
|
- run: cowork-harness lint scenarios/*.yaml --strict --min-severity WARN
|
|
87
87
|
# no silent false-greens. WITHOUT --strict this
|
|
88
88
|
# step cannot fail on a WARN-class rule (e.g.
|
|
@@ -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.
|
|
414
|
+
- run: npm i -g "cowork-harness@^4.9.0"
|
|
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.
|
|
443
|
+
run: npm i -g "cowork-harness@^4.9.0"
|
|
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.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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
|
|
@@ -224,6 +224,9 @@ harness majors, mixed `corpusHash` within a group, the same critique twice, a su
|
|
|
224
224
|
field, a pre-4.8 report. Marked, and refused
|
|
225
225
|
by `--strict`: mixed evaluator models / hash bases / skill trees / probes, a pass-1-only member. Marked only:
|
|
226
226
|
`mixedPackagedCorpus`, `probeUnverified` (no prompt hash — the summary default).
|
|
227
|
+
`--cluster lexical` (default `none`) adds `candidateGroups`: citation-resolved findings with text, from full reports, whose wording is close, from at least two reports
|
|
228
|
+
(complete linkage, every pair at or above a fixed cut of 0.20). A judgement from word overlap, not a match: no k/N, input order, a
|
|
229
|
+
finding in no group may still have recurred in other words. Local, nothing sent; `--cluster judge` is refused.
|
|
227
230
|
Its output carries finding text — not public-safe. Details: [`docs/critique.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/critique.md) (repo-only).
|
|
228
231
|
|
|
229
232
|
## An untracked skill file is not graded
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Debugging a run
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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
|
|
|
@@ -109,7 +109,9 @@ decide which assertions from *Assertions: two orthogonal axes* in `assertions-gu
|
|
|
109
109
|
to check a skill's delivery survives the cloud lane, where new Pro and Max tasks run from 2026-10-06. Orthogonal to `fidelity` — a `lane: remote`
|
|
110
110
|
scenario still runs locally. Add `device_bridge: true` (with `fidelity: container`) to serve `device_bash`, the
|
|
111
111
|
cloud task's shell on the user's computer; a red there reads from the `device_bash` tool calls and the run's
|
|
112
|
-
`deviceBridge` record, and a `device-container` / `device-exec` infra error is that second container.
|
|
112
|
+
`deviceBridge` record, and a `device-container` / `device-exec` infra error is that second container (`device-shim`: the device image could not be prepared, or the bridge's probe delete was not refused). The bridge
|
|
113
|
+
also serves the delete card (`device_request_delete_permission`): an unscripted one ends the run as an unanswered
|
|
114
|
+
gate, so script it (`when_tool: … decide: allow|deny`).
|
|
113
115
|
- **`cowork-harness stats [--metric <m>]`** — aggregate across the run index: `cost`, `duration`,
|
|
114
116
|
`tokens`, `cache-tokens`, `model-cost`, `turns`, `pass-rate`. Filters: `--since`/`--baseline`/`--branch`,
|
|
115
117
|
plus `--skill-hash <prefix>`/`--label <tag>` to narrow to ONE skill generation and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `eval` — paired before/after comparison of a skill edit (EXPERIMENTAL)
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (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
|
|
|
@@ -20,8 +20,9 @@ cowork-harness eval report <eval-dir> # rebuild the report from the eval dir
|
|
|
20
20
|
not the `remote_plugins` path a UI-installed plugin has (`mnt/.remote-plugins/plugin_<id>`). A skill that
|
|
21
21
|
locates its own files at runtime sees a different path under each, so the comparison holds for the
|
|
22
22
|
`local_plugins` layout only.
|
|
23
|
-
-
|
|
24
|
-
compared
|
|
23
|
+
- Pass rows are a pass or a fail per rep, and `eval` compares pass rates. Each number a scenario declares under
|
|
24
|
+
`metrics:` is a metric row, compared with an exact rank test in its own correction family (see
|
|
25
|
+
[Metric rows](#metric-rows)).
|
|
25
26
|
- scenarios × 2 × `--reps` live runs (10 per scenario at the default `--reps 5`), interleaved, plus one judge
|
|
26
27
|
call per `semantic_matches` assert per run. The start-up line prints the job count.
|
|
27
28
|
- Before an A/B, plan it: `eval … --dry-run --target-effect 30pp` runs nothing and prints, from the runs
|
|
@@ -35,7 +36,8 @@ cowork-harness eval report <eval-dir> # rebuild the report from the eval dir
|
|
|
35
36
|
- Run an A/A first (`--allow-identical-arms`, the same source twice) to see your scenarios' noise.
|
|
36
37
|
- A directory arm's snapshot leaves out untracked files unless you pass `--include-untracked` (not with a
|
|
37
38
|
`git:` arm). `--correction bh|holm` picks the multiple-comparison correction behind `confirmed` (default
|
|
38
|
-
`bh` at q = 0.10; `holm` is stricter).
|
|
39
|
+
`bh` at q = 0.10; `holm` is stricter). Metric rows default to holm at `--alpha` split across the section's
|
|
40
|
+
families (0.025 beside pass rows); an explicit `--correction bh` grades them with BH at q = 0.10, unsplit.
|
|
39
41
|
|
|
40
42
|
## Reading the labels
|
|
41
43
|
|
|
@@ -47,6 +49,9 @@ cowork-harness eval report <eval-dir> # rebuild the report from the eval dir
|
|
|
47
49
|
| `underpowered` | no outcome at these sizes could reach `--alpha` — NOT "no change" |
|
|
48
50
|
| `insufficient` | too few valid reps in an arm (4 of 5 needed by default) |
|
|
49
51
|
|
|
52
|
+
Metric rows say `confirmed/possible worse|better` (read through the metric's `better`), plus `unresolved`,
|
|
53
|
+
`insufficient_unmeasured` and `not compared` ([below](#metric-rows)).
|
|
54
|
+
|
|
50
55
|
A drop is a signal to investigate, not proof: open the run dirs the report links for that row. In order,
|
|
51
56
|
first match wins: an infrastructure failure is excluded and reported — including a usage limit the run lane
|
|
52
57
|
named (the main loop's or a sub-agent's, rule `kind_usage_limit`, even on a rep that otherwise timed out or hit
|
|
@@ -70,10 +75,11 @@ names the arm, scenario, dominant error and a matching hint (`Every rep of arm <
|
|
|
70
75
|
- a model or judge that is an alias (`opus`, `best`) rather than a concrete id;
|
|
71
76
|
- an eval dir inside any git work tree (the snapshots would mount empty);
|
|
72
77
|
- identical arms (unless `--allow-identical-arms`);
|
|
73
|
-
- an arm that contains the eval's own scenario or session files
|
|
74
|
-
`evals.json`, or a symlink resolving outside it;
|
|
78
|
+
- an arm that contains the eval's own scenario or session files, or a command metric's script or deps (by location,
|
|
79
|
+
copy or symlink), an `evals.json`, or a symlink resolving outside it;
|
|
75
80
|
- a scenario input a run would refuse (a missing path, a `tool_not_called` the tier can never violate);
|
|
76
|
-
- `--fail-on confirmed` when no
|
|
81
|
+
- `--fail-on confirmed` when no family (pass rows or metric rows, in any section) could reach `confirmed` at this
|
|
82
|
+
`--reps`; when only some can, the start-up line names the families the gate can fire on;
|
|
77
83
|
- no usable agent credential for a scenario's tier — the same check as `doctor --tier <tier>`'s `token` row,
|
|
78
84
|
with its fix. A Keychain login or a `.credentials.json` in the config dir, without an env/.env token,
|
|
79
85
|
passes only at `protocol`;
|
|
@@ -81,10 +87,35 @@ names the arm, scenario, dominant error and a matching hint (`Every rep of arm <
|
|
|
81
87
|
one `local_plugins` entry). Workaround: eval a copy of the session that declares the same directory under
|
|
82
88
|
`local_plugins`, and check the `remote_plugins` path handling with an ordinary `run`.
|
|
83
89
|
|
|
90
|
+
## Metric rows
|
|
91
|
+
|
|
92
|
+
- One row per (scenario, declared metric). The test is exact two-sided Wilcoxon–Mann–Whitney, so ties and skewed
|
|
93
|
+
numbers are fine. Each row also shows the Hodges–Lehmann shift with its ACHIEVED coverage (96.8% at 5 v 5), each
|
|
94
|
+
arm's SD and the minimum detectable shift (`mds`).
|
|
95
|
+
- **At 5 reps a metric row reliably flags only a shift of about 2.4 SD** (2.0 at 6 reps): near-complete separation.
|
|
96
|
+
Every metric row is `underpowered` at `--reps 3`; use `--reps 5`, since at `--reps 4` one lost rep silences it.
|
|
97
|
+
A metric whose every value is equal is `underpowered`, never "no change".
|
|
98
|
+
- A rep where the agent failed, or the skill did not produce the number (`missing_artifact`, `missing_path`,
|
|
99
|
+
`not_json`, `not_a_number`, `pre_run`), ranks WORST, below every measured value. A rep whose number is missing
|
|
100
|
+
because of the scorer, the capture or a cap (`size`, `readonly`, `remote`, and a command metric's
|
|
101
|
+
`command_failed` / `command_timeout` / `command_output` / `nondeterministic` / `no_events`) is EXCLUDED and
|
|
102
|
+
guarded: no verdict may rest on it, so a row that would flag only while those reps stay unknown is `unresolved`
|
|
103
|
+
(never gates). The candidate's ≥ 2 extra excluded reps that took a row below the threshold: `insufficient_unmeasured`
|
|
104
|
+
(`--fail-on possible` gates). `pruned` / `no_manifest`, a completed rep with no entry for the id
|
|
105
|
+
(`metric_unrecorded`), or a command metric's `command_changed` / `command_missing` / `command_not_allowed` (a rep
|
|
106
|
+
scored by another script than the manifest froze): `not compared`, exit 1.
|
|
107
|
+
- At 5 reps a metric can be `confirmed` beside pass rows only when the section declares 3 metrics or fewer; 4–11
|
|
108
|
+
need `--reps 6`. Every report states each family's correction, row count and A/A false-confirm rate.
|
|
109
|
+
- Worth it for count metrics on a targeted defect fix (a ×3.3 rate change is `possible` at 5 reps, ×4.1
|
|
110
|
+
confirms). Otherwise run an A/A first and read each row's `sd` and `mds`: if `MDS possible` (in the metric's units)
|
|
111
|
+
is larger than the change you hope for, raise `--reps` or change the metric.
|
|
112
|
+
- An eval dir written before metrics were graded reports them as not recorded (exit code unchanged).
|
|
113
|
+
|
|
84
114
|
## Exit codes
|
|
85
115
|
|
|
86
|
-
`0` completed — no drop fails the eval unless you pass `--fail-on`. `1` a drop at the `--fail-on`
|
|
87
|
-
every row `insufficient`, a scenario that compared nothing,
|
|
116
|
+
`0` completed — no drop fails the eval unless you pass `--fail-on`. `1` a drop or a worse metric at the `--fail-on`
|
|
117
|
+
level, every row `insufficient`, a scenario that compared nothing, a metric row `not compared`, or the judge model
|
|
118
|
+
differed across reps (an A/A run under `--fail-on possible`
|
|
88
119
|
can exit 1 on noise). `2` usage or a refusal. `3` an arm snapshot could not be copied or staged.
|
|
89
120
|
|
|
90
121
|
## Files
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Fidelity tiers & answer paths
|
|
2
2
|
|
|
3
|
-
Self-contained reference. Tracks `cowork-harness 4.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 4.9.0` (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
|
|
@@ -243,7 +243,8 @@ prevent, so use it to unblock once, never in CI.
|
|
|
243
243
|
at the cost of non-determinism (one model call per gate). Correct, but flaky for skills whose gates
|
|
244
244
|
appear stochastically.
|
|
245
245
|
- `first` — picks option 1 and warns loudly. **Flagged `nonDeterministic`** — not a deterministic
|
|
246
|
-
substitute for scripted answers. (For a web_fetch approval gate it abstains
|
|
246
|
+
substitute for scripted answers. (For a web_fetch approval gate it abstains, and the fetch is denied; for the device bridge's delete card it picks
|
|
247
|
+
`Decline`.)
|
|
247
248
|
- `prompt` — asks at the TTY (`skill` only).
|
|
248
249
|
- `llm` — a model answers; flagged `nonDeterministic`.
|
|
249
250
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Gotchas
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.
|
|
3
|
+
Tracks `cowork-harness 4.9.0` (baseline `desktop-2.31226.1`). The full "✓ passed ≠ correct" landmine catalog.
|
|
4
4
|
|
|
5
5
|
## Gotchas — the "✓ passed ≠ correct" landmines
|
|
6
6
|
|
|
@@ -69,9 +69,10 @@ authorable). Reach for this list when debugging a run's behavior, that one while
|
|
|
69
69
|
7. **`protocol` is rejected (not silently passed) if the scenario asserts egress** — boundary
|
|
70
70
|
assertions need a sandboxed tier (`container`+). Good: this one fails loud by design.
|
|
71
71
|
|
|
72
|
-
8. **Read-only mounts are enforced; delete-deny is
|
|
73
|
-
Production enforces it on `rw` mounts; which mounts are `rw` is below. `mode:r` mounts get a real `:ro` bind (a write fails in-guest).
|
|
74
|
-
(write-but-no-delete) is *not* mount-enforced
|
|
72
|
+
8. **Read-only mounts are enforced; delete-deny is enforced only under `device_bridge` — production enforces it everywhere.**
|
|
73
|
+
Production enforces it on `rw` mounts; which mounts are `rw` is below. `mode:r` mounts get a real `:ro` bind (a write fails in-guest). On the local lane
|
|
74
|
+
(every tier without `device_bridge`) `rw` vs `rwd` (write-but-no-delete) is *not* mount-enforced — `rm` succeeds
|
|
75
|
+
and is only caught
|
|
75
76
|
post-hoc. **Real Cowork enforces it live on a `rw` mount:** a FUSE mount where `unlink`/`rmdir` fail
|
|
76
77
|
`Operation not permitted`; a skill must request approval via `allow_cowork_file_delete` (which
|
|
77
78
|
re-mounts the folder `rwd` mid-session) to delete. **Which mounts are `rw` depends on the release:** a
|
|
@@ -87,8 +88,8 @@ authorable). Reach for this list when debugging a run's behavior, that one while
|
|
|
87
88
|
(`EXDEV`, then `EPERM` on the copy-then-unlink fallback), so that stays a delete; on an `rwd` outputs
|
|
88
89
|
mount the fallback's unlink should succeed, but that is not probed. Consequences on a `rw` mount: a
|
|
89
90
|
skill should not stage disposable scratch there (cleanup costs an approval prompt), and a skill's
|
|
90
|
-
"catch-EPERM-then-request-approval" branch
|
|
91
|
-
succeeds
|
|
91
|
+
"catch-EPERM-then-request-approval" branch can be exercised only under `device_bridge`, where the device refuses
|
|
92
|
+
the delete and serves the delete card; on the local-lane tiers the `rm` just succeeds. On an older `rw` baseline, if a scenario's deletion IS intended, assert
|
|
92
93
|
`allow_outputs_delete: true` rather than dropping `no_delete_in_outputs` (on `rwd` it is a no-op unless
|
|
93
94
|
`no_delete_in_mounts` arms the outputs check, where it waives that check as on `rw`).
|
|
94
95
|
|