cowork-harness 4.9.0 → 4.9.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 +5 -5
- 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 +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 +8 -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 +4 -3
- package/.claude/skills/cowork-harness/references/measurement.md +1 -1
- package/.claude/skills/cowork-harness/references/run-record-replay.md +12 -8
- package/.claude/skills/cowork-harness/references/scenario-schema.md +4 -2
- package/.claude/skills/cowork-harness/references/semantic-judging.md +1 -1
- package/.claude/skills/cowork-harness/references/task-recipes.md +5 -4
- package/.claude/skills/cowork-harness/scripts/scenario.py +79 -28
- package/AGENTS.md +4 -2
- package/CHANGELOG.md +76 -0
- package/CONTRIBUTING.md +1 -1
- package/DESIGN.md +2 -2
- package/README.md +4 -4
- package/RELEASING.md +37 -15
- package/SPEC.md +1 -1
- package/baselines/desktop-2.37337.1.json +1674 -0
- package/baselines/prompts/cowork-system-prompt-fingerprints.json +16 -0
- package/dist/cli.js +13 -11
- package/dist/eval/usage.js +5 -2
- package/dist/hillclimb/cli.js +7 -7
- package/dist/hillclimb/usage.js +38 -1
- package/dist/run/cassette.js +6 -6
- package/dist/run/multi-leg-scenario.js +1 -1
- package/dist/run/regrade-usage.js +2 -2
- package/dist/search-corpus/cli.js +4 -2
- package/dist/search-corpus/harvest.js +48 -8
- package/dist/search-corpus/pool.js +24 -2
- package/dist/sync/cowork-sync.js +212 -30
- package/dist/sync/spawn-env-registry.js +10 -0
- package/dist/types.js +1 -1
- package/docs/ci.md +6 -4
- package/docs/cli.md +21 -20
- package/docs/companion-skill.md +6 -3
- package/docs/debugging.md +7 -0
- package/docs/decider-dir.md +4 -0
- package/docs/eval.md +2 -1
- package/docs/fidelity-gaps.md +2 -1
- package/docs/hillclimb.md +3 -2
- package/docs/maintenance.md +3 -1
- package/docs/scenario.md +17 -11
- package/examples/README.md +3 -1
- package/examples/replays/README.md +1 -1
- package/examples/replays/example-multiselect-gate.cassette.json +1 -1
- package/examples/replays/example-pdf-skill.cassette.json +1 -1
- package/examples/replays/hostloop-computer-links.cassette.json +1 -1
- package/package.json +2 -2
- package/python/test_scenario_lint.py +89 -0
- package/schema/cassette.v18.json +3 -3
- package/schema/critique-compare.json +1 -1
- package/schema/run-result.json +3 -3
- package/schema/scenario-multi-leg.schema.json +1 -1
- package/schema/scenario.schema.json +1 -1
- package/scripts/check-surface.ts +145 -32
- package/scripts/lib/surface.ts +72 -10
|
@@ -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.9.
|
|
7
|
-
tracks-harness: cowork-harness 4.9.
|
|
6
|
+
version: 4.9.1
|
|
7
|
+
tracks-harness: cowork-harness 4.9.1 (baseline desktop-2.37337.1)
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# cowork-harness
|
|
@@ -26,8 +26,8 @@ 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.9.
|
|
30
|
-
> `desktop-2.
|
|
29
|
+
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 4.9.1` (baseline
|
|
30
|
+
> `desktop-2.37337.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
|
|
|
33
33
|
## Preflight — make sure the harness can actually run
|
|
@@ -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, 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.9.
|
|
46
|
+
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 4.9.1**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@^4.9.1" <cmd>` (Node ≥ 22), or install once with `npm i -g "cowork-harness@^4.9.1"`. **Pin `@^4.9.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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 (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
|
|
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`: the LLM decider always picks a button or fails the run); 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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
20
|
+
(e.g. `version: "4.9.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.9.
|
|
85
|
+
- run: npm i -g "cowork-harness@^4.9.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.9.
|
|
414
|
+
- run: npm i -g "cowork-harness@^4.9.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.9.
|
|
443
|
+
run: npm i -g "cowork-harness@^4.9.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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
|
|
|
@@ -35,6 +35,13 @@ received a question or permission request anyway; `events.jsonl` and `control-ou
|
|
|
35
35
|
already does.
|
|
36
36
|
<!-- END triage-canonical -->
|
|
37
37
|
|
|
38
|
+
**A multi-leg run (`legs:`) is read one leg at a time.** `trace`, `verify-run` and `regrade` take
|
|
39
|
+
`--leg <id>` (or the leg dir `<root>/legs/<id>` itself); `inspect` and `fixture export` take the leg dir. A
|
|
40
|
+
multi-leg root without `--leg` is refused, and `trace` / `inspect` name its legs. A host-action leg has no
|
|
41
|
+
agent session and is refused too — its record is `legs/<id>/action.json`. `search-pool build --from <root>` is the
|
|
42
|
+
exception: it reads every prompt leg of the root, each as its own source run. See
|
|
43
|
+
[docs/scenario.md → Multi-leg scenarios](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/scenario.md#multi-leg-scenarios-legs).
|
|
44
|
+
|
|
38
45
|
**`verify-run` never calls the semantic judge**, so it cannot re-grade a `semantic_matches` or `semantic_pairwise` assert. When the
|
|
39
46
|
rubric changed (or you want another judge model) on a run you already paid for, use
|
|
40
47
|
`cowork-harness regrade <run-dir> --scenario <scenario.yaml>` instead: it re-grades those asserts against the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `eval` — paired before/after comparison of a skill edit (EXPERIMENTAL)
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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`.
|
|
@@ -254,8 +254,9 @@ and rewrites each row through the same producer `run` writes it with. Pass the s
|
|
|
254
254
|
`COWORK_HARNESS_RUNS_DIR` the runs were written with, or every row is listed as having no kept run dir. A runs root
|
|
255
255
|
moved to where `--run-dir` now points is read from there; a COPY of it while the original still exists is refused
|
|
256
256
|
row by row (point `--run-dir` at the original root instead). A bare
|
|
257
|
-
`prune` keeps hillclimb-labelled runs; `prune --include-hillclimb`
|
|
258
|
-
|
|
257
|
+
`prune` keeps hillclimb-labelled runs; `prune --include-hillclimb` ranks them with every other run under
|
|
258
|
+
`--keep-last`, so the runs of every flow under the runs root, a loop still running included, can lose the ones
|
|
259
|
+
a later re-grade or reference freeze needs (freezing a reference re-reads its source run). Pass it only once
|
|
259
260
|
every climb there is finished.
|
|
260
261
|
|
|
261
262
|
- **Default:** the row is rebuilt from the scenario as it is now, so a changed value, an added or a removed assert
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Measurement
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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
|
|
|
@@ -229,7 +229,7 @@ Recognize these before "fixing" a non-bug:
|
|
|
229
229
|
`markitdown`/`magika` (`ml_extract`), `cv2` (`cv`), `camelot`/`tabula` (`pdf_tables`), or `wand`
|
|
230
230
|
(`magick`) can trip this even though real Cowork **ships** those (per the rootfs manifest captured at
|
|
231
231
|
Desktop `2.9939.2` — `baselines/provisioning/rootfs-provisioning.json`, which is the dated evidence
|
|
232
|
-
behind that sentence;
|
|
232
|
+
behind that sentence; 9 baselines have shipped since without a re-capture). The message says so ("likely a FALSE
|
|
233
233
|
NEGATIVE (real Cowork ships them)"). Fix: rebuild full parity (`--build-arg COWORK_FULL_PARITY=1`, point
|
|
234
234
|
`COWORK_AGENT_IMAGE` at it), or — if the skill's fallback is genuinely equivalent — assert
|
|
235
235
|
`allow_missing_capability: true`. (Two sources: a skill *observed using* an omitted family, live lane;
|
|
@@ -439,11 +439,13 @@ with the ordinary commands, one scenario at a time:
|
|
|
439
439
|
`trace <root | run-id> --leg <id>`, `inspect <root>/legs/<id>`. A prompt leg is read from its snapshot
|
|
440
440
|
(`legs/<id>/snapshot/`, its files as they were when it ended), so every prompt leg of a kept run re-grades at no
|
|
441
441
|
cost (a host-action leg has no session and is refused). The snapshot is a copy-on-write clone where the filesystem
|
|
442
|
-
supports one (APFS
|
|
443
|
-
|
|
444
|
-
|
|
442
|
+
supports one (macOS APFS through `cp -c`, only when the folder is on the same volume as the run dir; Linux btrfs,
|
|
443
|
+
XFS with reflink), costing almost no disk until a later leg changes a file, and otherwise (ext4, the usual Linux and
|
|
444
|
+
CI disk, or a folder on another volume) a full copy of the leg's outputs and connected folders, per prompt leg;
|
|
445
|
+
`snapshot.json` records which as `method` (`clone`, `copy`, `mixed` — one folder cloned, another copied — or
|
|
446
|
+
`none`), on macOS `fallback` says why a folder was copied rather than cloned, and `leg-post-manifest.json`
|
|
445
447
|
holds the hashes that prove it is the leg's own. A leg over `COWORK_HARNESS_LEG_SNAPSHOT_MAX_BYTES` (default
|
|
446
|
-
512 MiB; a malformed value is refused before the run) gets no snapshot. A leg with no usable snapshot falls back to its session tree and is refused when
|
|
448
|
+
512 MiB; a malformed value is refused before the run) gets no snapshot. A leg with no usable snapshot falls back to its session tree. Under `workdir: persistent` only the last leg of a finished scenario can use it (later legs changed the shared tree); every other leg is refused. That last leg, and every `cowork-task` leg, is refused when its tree changed after the leg ended, or when there is no post-run manifest to check it against. A
|
|
447
449
|
multi-leg root without `--leg` is refused, naming the legs.
|
|
448
450
|
|
|
449
451
|
Full rules: [docs/scenario.md → "Multi-leg scenarios"](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/scenario.md#multi-leg-scenarios-legs).
|
|
@@ -467,8 +469,10 @@ Full rules: [docs/scenario.md → "Multi-leg scenarios"](https://github.com/yani
|
|
|
467
469
|
path unless `--allow-host-paths` (a path into a run dir or a guest session is refused regardless).
|
|
468
470
|
`--session-paths` tokenises this run's own session roots instead of refusing them, and staging writes the new
|
|
469
471
|
run's roots in (hostloop, container, microvm); `--exclude <path>` drops a file.
|
|
470
|
-
- `prune [--keep-last <n>] [--pinned-older-than <N>d]` removes old run dirs (default
|
|
471
|
-
`--dry-run` previews it.
|
|
472
|
+
- `prune [--keep-last <n>] [--pinned-older-than <N>d|h|m] [--include-hillclimb]` removes old run dirs (default
|
|
473
|
+
`--keep-last 5`); `--dry-run` previews it. Hillclimb-labelled runs are kept outside `--keep-last`;
|
|
474
|
+
`--include-hillclimb` ranks them with the rest, so every flow's runs under the runs root (a running climb
|
|
475
|
+
included) can lose the ones `hillclimb regrade` / `freeze-ref` need; pass it only once every climb there is finished.
|
|
472
476
|
|
|
473
477
|
### Place assertions in the right CI lane
|
|
474
478
|
|
|
@@ -1,7 +1,7 @@
|
|
|
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.9.
|
|
4
|
-
(baseline `desktop-2.
|
|
3
|
+
Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 4.9.1`
|
|
4
|
+
(baseline `desktop-2.37337.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
|
|
|
7
7
|
**Minimal scenario** — `prompt` and `fidelity` are required:
|
|
@@ -156,6 +156,8 @@ metrics: # OPTIONAL — numbers read from JSON files
|
|
|
156
156
|
# run as `<script> <events-file> [args]` over this turn's scrubbed slice of
|
|
157
157
|
# events.jsonl; it must print exactly one JSON number. Script + deps: regular,
|
|
158
158
|
# git-tracked files inside the scenario's repo (no symlink, no hard link, <= 1 MiB).
|
|
159
|
+
# `timeout_ms`: the script's wall-clock limit per run (1000-300000, default
|
|
160
|
+
# 30000; command only — refused on an artifact metric), past it command_timeout.
|
|
159
161
|
# RUNS THE REPO'S CODE AS YOU (only repos you would `npm test`). Live runs run it;
|
|
160
162
|
# replay reports the frozen record-time value (cassette v18); verify-run / regrade /
|
|
161
163
|
# hillclimb regrade carry it forward unless --allow-metric-commands. Its own reasons:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `semantic_matches` — the full semantics
|
|
2
2
|
|
|
3
|
-
Tracks `cowork-harness 4.9.
|
|
3
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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.9.
|
|
5
|
+
Tracks `cowork-harness 4.9.1` (baseline `desktop-2.37337.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
|
|
|
@@ -62,7 +62,7 @@ Top-level fields of a `*.cassette.json` (schema [`schema/cassette.v18.json`](htt
|
|
|
62
62
|
|
|
63
63
|
| Field | What it is |
|
|
64
64
|
|---|---|
|
|
65
|
-
| `$schema`, `generator`, `cassetteVersion` | Provenance: schema URL, producing tool, format version — the MINIMUM a reader needs for this scenario, not the recorder's version (current max: 18 — the hash-format epoch floors every stamp at 12, so a fresh recording stamps 12 unless a key lifts it: 13 when its `assert:` uses the object form of `tool_called` / `tool_not_called`, 14 for a v14 key such as `include_fork_results` on `semantic_matches`, 15 for a v15 one such as `gates_all_scripted`, a glob `artifact_json`, an `artifact_json` carrying `schema`, any `artifact_json` on `lane: remote`, or a recording under `answer_channel: none`, 16 for a recording whose `hookFailureBlocks` lists an event or is unknown, 18 for a scenario with `device_bridge: true` (a v17 bridge recording still reads) or one declaring a command metric (its record-time values frozen in `commandMetrics`); the full list is in the cassette docs' version table) |
|
|
65
|
+
| `$schema`, `generator`, `cassetteVersion` | Provenance: schema URL, producing tool, format version — the MINIMUM a reader needs for this scenario, not the recorder's version (current max: 18 — the hash-format epoch floors every stamp at 12, so a fresh recording stamps 12 unless a key lifts it: 13 when its `assert:` uses the object form of `tool_called` / `tool_not_called`, 14 for a v14 key such as `include_fork_results` on `semantic_matches`, 15 for a v15 one such as `gates_all_scripted`, a glob `artifact_json`, an `artifact_json` carrying `schema`, any `artifact_json` on `lane: remote`, or a recording under `answer_channel: none`, 16 for a recording whose `hookFailureBlocks` lists an event or is unknown, 18 for a scenario with `device_bridge: true` (a v17 bridge recording still reads) or one declaring a command metric (its record-time values frozen in `commandMetrics`), and for a multi-leg (`legs:`) scenario; the full list is in the cassette docs' version table) |
|
|
66
66
|
| `scenario` | The embedded scenario snapshot at record time |
|
|
67
67
|
| `events` | The recorded agent event stream (the replay source) |
|
|
68
68
|
| `legs`, `verdict` | Multi-leg form only (a `legs:` scenario, v18), in place of `events`: `legs[]` holds one entry per leg in order — a prompt leg nests its own complete single-prompt cassette under `cassette`, a host action its record under `action`, a leg after a failing one `status: "skipped"` — and `verdict.pass` is the scenario verdict at record time. `replay` and `verify-cassettes` read this form; other cassette readers refuse it by name |
|
|
@@ -521,6 +521,8 @@ cowork-harness search-pool export market-sizing --out ~/market-sizing-pool.json
|
|
|
521
521
|
cowork-harness search-pool import ~/market-sizing-pool.json market-sizing-copy # re-verifies both hashes
|
|
522
522
|
```
|
|
523
523
|
|
|
524
|
+
- **Multi-leg runs.** A multi-leg run under a `--from` dir adds each prompt leg as its own source run (`legs/<id>/`);
|
|
525
|
+
a host-action leg has no searches.
|
|
524
526
|
- **One skill per pool.** A run whose skill differs from the others, or that names no single skill, is refused. Pass
|
|
525
527
|
`--skill <name>` to name the skill for runs that name none, or `--mixed` for a pool of several skills (a measured
|
|
526
528
|
variant, never a baseline).
|
|
@@ -548,5 +550,4 @@ Copy [the resumable-probe scenario](https://github.com/yaniv-golan/cowork-harnes
|
|
|
548
550
|
- Re-grade or read one leg with `--leg <id>`: `verify-run <run-dir> <legs.yaml> --leg <id>`,
|
|
549
551
|
`regrade <run-dir> --scenario <legs.yaml> --leg <id>`, `trace <run-dir | run-id> --leg <id>`. Each prompt leg is read
|
|
550
552
|
from its own snapshot (its files as they stood when it ended; a clone where the filesystem supports one, else a
|
|
551
|
-
full copy, so budget disk on ext4 and CI), so it re-grades at no cost. A leg too large to snapshot falls back to its
|
|
552
|
-
session tree and is refused once a later leg changed it; a host-action leg is refused.
|
|
553
|
+
full copy, so budget disk on ext4 and CI), so it re-grades at no cost. A leg too large to snapshot falls back to its session tree. Under `workdir: persistent` only the last leg of a finished scenario can use it (later legs changed the shared tree); every other leg is refused. That last leg, and every `cowork-task` leg, is refused when its tree changed after the leg ended, or when there is no post-run manifest to check it against; a host-action leg is refused.
|
|
@@ -272,16 +272,24 @@ VERDICT_MODIFIER_KEYS = {
|
|
|
272
272
|
_CLASSIFIED_KEYS = CONTENT_KEYS | GATE_KEYS | MANIFEST_KEYS | LIVE_ONLY_KEYS | VERDICT_MODIFIER_KEYS | {"replay_protocol_fidelity"}
|
|
273
273
|
|
|
274
274
|
|
|
275
|
+
def _str_list(v, name):
|
|
276
|
+
"""`v` as a list of strings, or a TypeError naming the field: a string where a key list belongs would otherwise
|
|
277
|
+
be iterated as characters, and the loaders fall back to their embedded copies on any error."""
|
|
278
|
+
if not isinstance(v, list) or not all(isinstance(x, str) for x in v):
|
|
279
|
+
raise TypeError(f"assertion-keys.json {name} is not a list of strings")
|
|
280
|
+
return v
|
|
281
|
+
|
|
282
|
+
|
|
275
283
|
def _load_assert_keys():
|
|
276
284
|
"""The authoritative `assert:` key set, generated from the Zod Assertion schema into a sibling
|
|
277
285
|
`assertion-keys.json` (so the unknown-key check can't drift). Falls back to the embedded
|
|
278
286
|
`_CLASSIFIED_KEYS` (kept equal to the generated list) with a loud warning if the file is missing."""
|
|
279
287
|
p = Path(__file__).resolve().parent / "assertion-keys.json"
|
|
280
288
|
try:
|
|
281
|
-
return set(json.loads(p.read_text(encoding="utf-8"))["keys"])
|
|
282
|
-
except Exception:
|
|
289
|
+
return set(_str_list(json.loads(p.read_text(encoding="utf-8"))["keys"], "keys"))
|
|
290
|
+
except Exception as e:
|
|
283
291
|
print(
|
|
284
|
-
f"::warning:: assertion-keys.json
|
|
292
|
+
f"::warning:: assertion-keys.json missing or malformed next to scenario.py ({p}): {e} — "
|
|
285
293
|
"using a built-in key list that may be stale (run `npm run schema`).",
|
|
286
294
|
file=sys.stderr,
|
|
287
295
|
)
|
|
@@ -344,22 +352,35 @@ def _load_hook_events():
|
|
|
344
352
|
p = Path(__file__).resolve().parent / "assertion-keys.json"
|
|
345
353
|
try:
|
|
346
354
|
d = json.loads(p.read_text(encoding="utf-8"))
|
|
347
|
-
served
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
355
|
+
served = set(_str_list(d["servedHookEvents"], "servedHookEvents"))
|
|
356
|
+
known = set(_str_list(d["knownHookEvents"], "knownHookEvents"))
|
|
357
|
+
if not (served and known):
|
|
358
|
+
raise ValueError("servedHookEvents or knownHookEvents is empty")
|
|
359
|
+
except Exception as e:
|
|
360
|
+
print(
|
|
361
|
+
f"::warning:: assertion-keys.json missing or malformed next to scenario.py ({p}): {e} — "
|
|
362
|
+
"using built-in hook-event lists that may be stale (run `npm run schema`).",
|
|
363
|
+
file=sys.stderr,
|
|
364
|
+
)
|
|
365
|
+
return (
|
|
366
|
+
set(_FALLBACK_SERVED_HOOK_EVENTS),
|
|
367
|
+
set(_FALLBACK_KNOWN_HOOK_EVENTS),
|
|
368
|
+
set(_FALLBACK_LIVE_VERIFIED_HOOK_EVENTS),
|
|
369
|
+
)
|
|
370
|
+
# Tolerate a sidecar generated before liveVerifiedHookEvents existed: fall back rather than
|
|
371
|
+
# dropping to the embedded KNOWN set wholesale, which would undo the 33-name fix. Membership
|
|
372
|
+
# test, NOT truthiness: an explicitly EMPTY list means the firing receipt was withdrawn, and
|
|
373
|
+
# `or` would silently restore the 3-name claim -- widening a receipt, which is the one direction
|
|
374
|
+
# this constant exists to prevent. For the same reason a MALFORMED list verifies nothing: falling
|
|
375
|
+
# back to the built-in list there would widen a receipt the sidecar does not give.
|
|
376
|
+
if "liveVerifiedHookEvents" not in d:
|
|
377
|
+
return served, known, set(_FALLBACK_LIVE_VERIFIED_HOOK_EVENTS)
|
|
378
|
+
try:
|
|
379
|
+
live = set(_str_list(d["liveVerifiedHookEvents"], "liveVerifiedHookEvents"))
|
|
380
|
+
except TypeError as e:
|
|
381
|
+
print(f"::warning:: {e} ({p}) — treating no hook event as live-verified (run `npm run schema`).", file=sys.stderr)
|
|
382
|
+
live = set()
|
|
383
|
+
return served, known, live
|
|
363
384
|
|
|
364
385
|
|
|
365
386
|
SERVED_HOOK_EVENTS, KNOWN_HOOK_EVENTS, LIVE_VERIFIED_HOOK_EVENTS = _load_hook_events()
|
|
@@ -410,7 +431,7 @@ _EMBEDDED_TOP_LEVEL_KEYS = {
|
|
|
410
431
|
"allow_host_writes",
|
|
411
432
|
"allow_host_hooks", # protocol consent: a staged plugin's hooks run as NATIVE HOST processes # hostloop native-split: consent for a writable connected folder (pre-run gate)
|
|
412
433
|
"workspace_fixture", # a saved outputs tree staged into outputs/ before turn 1 (scenario-file-relative dir)
|
|
413
|
-
"metrics", # numbers read from JSON files the run wrote, reported beside the verdict (RunResult.metrics)
|
|
434
|
+
"metrics", # numbers read from JSON files the run wrote or printed by a command metric's script, reported beside the verdict (RunResult.metrics)
|
|
414
435
|
}
|
|
415
436
|
|
|
416
437
|
|
|
@@ -424,10 +445,10 @@ def _load_top_level_keys():
|
|
|
424
445
|
keys = json.loads(p.read_text(encoding="utf-8")).get("topLevelKeys")
|
|
425
446
|
if not keys:
|
|
426
447
|
raise KeyError("topLevelKeys")
|
|
427
|
-
return set(keys)
|
|
428
|
-
except Exception:
|
|
448
|
+
return set(_str_list(keys, "topLevelKeys"))
|
|
449
|
+
except Exception as e:
|
|
429
450
|
print(
|
|
430
|
-
f"::warning:: assertion-keys.json missing or
|
|
451
|
+
f"::warning:: assertion-keys.json missing, malformed or without topLevelKeys next to scenario.py ({p}): {e} — "
|
|
431
452
|
"using a built-in top-level-key list that may be stale (run `npm run schema`).",
|
|
432
453
|
file=sys.stderr,
|
|
433
454
|
)
|
|
@@ -500,14 +521,26 @@ def _load_enums():
|
|
|
500
521
|
enums = json.loads(p.read_text(encoding="utf-8")).get("enums")
|
|
501
522
|
if not enums:
|
|
502
523
|
raise KeyError("enums")
|
|
503
|
-
|
|
504
|
-
|
|
524
|
+
if not isinstance(enums, dict):
|
|
525
|
+
raise TypeError("assertion-keys.json enums is not a map")
|
|
526
|
+
except Exception as e:
|
|
505
527
|
print(
|
|
506
|
-
f"::warning:: assertion-keys.json missing or
|
|
528
|
+
f"::warning:: assertion-keys.json missing, malformed or without enums next to scenario.py ({p}): {e} — "
|
|
507
529
|
"using a built-in enum-value map that may be stale (run `npm run schema`).",
|
|
508
530
|
file=sys.stderr,
|
|
509
531
|
)
|
|
510
532
|
return dict(_EMBEDDED_ENUMS)
|
|
533
|
+
# Each field's values must be a list of strings: a string value would turn the membership test into a substring
|
|
534
|
+
# test, and anything else would crash the lint. A malformed field falls back to its built-in values alone.
|
|
535
|
+
out = {}
|
|
536
|
+
for field, values in enums.items():
|
|
537
|
+
try:
|
|
538
|
+
out[field] = _str_list(values, f"enums.{field}")
|
|
539
|
+
except TypeError as e:
|
|
540
|
+
print(f"::warning:: {e} ({p}) — using its built-in values (run `npm run schema`).", file=sys.stderr)
|
|
541
|
+
if field in _EMBEDDED_ENUMS:
|
|
542
|
+
out[field] = list(_EMBEDDED_ENUMS[field])
|
|
543
|
+
return out
|
|
511
544
|
|
|
512
545
|
|
|
513
546
|
# field id -> allowed values (generated from the zod schemas; see _load_enums)
|
|
@@ -1484,8 +1517,8 @@ def _hook_block_contradictions(items):
|
|
|
1484
1517
|
|
|
1485
1518
|
# A multi-leg (`legs:`) scenario: these top-level keys, and per-leg prompt/host_action/session/assert. Kept equal to
|
|
1486
1519
|
# MultiLegScenarioObject (src/run/multi-leg-scenario.ts); the harness loader is the authority and runs first under
|
|
1487
|
-
# the `cowork-harness lint` wrapper.
|
|
1488
|
-
|
|
1520
|
+
# the `cowork-harness lint` wrapper. Read from the generated assertion-keys.json at run time; this is its fallback.
|
|
1521
|
+
_EMBEDDED_MULTI_LEG_TOP_KEYS = {
|
|
1489
1522
|
"name",
|
|
1490
1523
|
"baseline",
|
|
1491
1524
|
"session",
|
|
@@ -1502,6 +1535,24 @@ _MULTI_LEG_TOP_KEYS = {
|
|
|
1502
1535
|
"allow_host_hooks",
|
|
1503
1536
|
"workspace_fixture",
|
|
1504
1537
|
}
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def _load_multi_leg_top_keys():
|
|
1541
|
+
"""A multi-leg scenario's top-level keys, from the generated assertion-keys.json (multiLeg.topLevelKeys), so the
|
|
1542
|
+
unknown-key check can't drift; falls back to the embedded copy (kept equal, test-enforced) with a warning."""
|
|
1543
|
+
p = Path(__file__).resolve().parent / "assertion-keys.json"
|
|
1544
|
+
try:
|
|
1545
|
+
return set(_str_list(json.loads(p.read_text(encoding="utf-8"))["multiLeg"]["topLevelKeys"], "multiLeg.topLevelKeys"))
|
|
1546
|
+
except Exception as e:
|
|
1547
|
+
print(
|
|
1548
|
+
f"::warning:: multiLeg.topLevelKeys missing or malformed in {p}: {e} — "
|
|
1549
|
+
"using a built-in key list that may be stale (run `npm run schema`).",
|
|
1550
|
+
file=sys.stderr,
|
|
1551
|
+
)
|
|
1552
|
+
return set(_EMBEDDED_MULTI_LEG_TOP_KEYS)
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
_MULTI_LEG_TOP_KEYS = _load_multi_leg_top_keys()
|
|
1505
1556
|
# The scenario-wide keys a prompt leg runs with (legScenario in src/run/execute.ts).
|
|
1506
1557
|
_LEG_SHARED_KEYS = (
|
|
1507
1558
|
"name",
|
package/AGENTS.md
CHANGED
|
@@ -58,8 +58,10 @@ here**, and the failure is silent. Every one below was hit by an agent working i
|
|
|
58
58
|
hand-maintained** — `cassette.v*.json`, `run-result.json`, `doctor.json`, `protocol.v1.json`,
|
|
59
59
|
`critique-report.json`, `verify-cassettes.json`, `regrade.json`, `schedule-cost.json`. Adding a field to those means editing the TS type **and**
|
|
60
60
|
the JSON by hand; nothing syncs them. Then regenerate the surface baseline (below).
|
|
61
|
-
- **Surface-baseline ordering:** edit the schema → `npm run check:surface` (review the diff
|
|
62
|
-
`+N -0 ~0
|
|
61
|
+
- **Surface-baseline ordering:** edit the schema → `npm run check:surface` (review the diff: a new property or
|
|
62
|
+
an output-schema addition reads `+N -0 ~0`; a tightened constraint on the scenario or session schema reads `~N`
|
|
63
|
+
by design and needs a major, see RELEASING.md; a loosened one is listed as widened) → `npm run gen:surface` →
|
|
64
|
+
`check:surface` again (now `+0 -0 ~0`). Running `gen:surface`
|
|
63
65
|
first hides the very diff you were supposed to review.
|
|
64
66
|
- **A `RunResult` field is a 5-call-site change.** `assembleRunResult` has **5** call sites across
|
|
65
67
|
`assemble-run-result.ts` / `execute.ts` / `cassette.ts` / `chat-result.ts`, plus the TS type, the
|