cowork-harness 0.21.0 → 0.23.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 +45 -29
- package/.claude/skills/cowork-harness/references/ci-recipe.md +64 -9
- package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +4 -3
- package/.claude/skills/cowork-harness/references/scenario-schema.md +37 -15
- package/.claude/skills/cowork-harness/scripts/assertion-keys.json +7 -0
- package/.claude/skills/cowork-harness/scripts/scenario.py +25 -7
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +392 -2
- package/DESIGN.md +5 -3
- package/README.md +83 -20
- package/SPEC.md +88 -18
- package/baselines/desktop-1.11847.5.json +2 -0
- package/baselines/desktop-1.12603.1.json +2 -0
- package/baselines/desktop-1.13576.1.json +3 -1
- package/baselines/desktop-1.14271.0.json +3 -1
- package/baselines/desktop-1.15200.0.json +3 -1
- package/baselines/desktop-1.15962.0.json +3 -1
- package/baselines/desktop-1.15962.1.json +3 -1
- package/baselines/desktop-1.17377.1.json +4 -1
- package/baselines/desktop-1.17377.2.json +5 -1
- package/baselines/desktop-1.18286.0.json +286 -0
- package/baselines/prompts/desktop-1.18286.0/system-prompt-append.md +214 -0
- package/dist/agent/session.js +7 -2
- package/dist/assert.js +88 -0
- package/dist/baseline.js +45 -3
- package/dist/cli-args.js +1 -1
- package/dist/cli.js +981 -73
- package/dist/decide/llm-transport.js +19 -4
- package/dist/errors.js +12 -0
- package/dist/loop-decision.js +0 -3
- package/dist/prompt.js +31 -8
- package/dist/run/cassette.js +177 -47
- package/dist/run/chat.js +42 -5
- package/dist/run/computer-links.js +198 -0
- package/dist/run/diff.js +178 -0
- package/dist/run/display-translate.js +226 -0
- package/dist/run/doctor.js +42 -3
- package/dist/run/envelope.js +32 -9
- package/dist/run/execute.js +89 -8
- package/dist/run/matrix.js +131 -0
- package/dist/run/renderer.js +27 -7
- package/dist/run/repeat.js +89 -0
- package/dist/run/run-index.js +253 -0
- package/dist/run/run.js +15 -2
- package/dist/run/trace-view.js +76 -14
- package/dist/run/verdict.js +8 -6
- package/dist/run/vm-path-ctx-file.js +123 -0
- package/dist/runtime/argv.js +13 -0
- package/dist/runtime/hostloop.js +3 -0
- package/dist/scan.js +31 -9
- package/dist/session.js +36 -0
- package/dist/sync/baseline-diff.js +138 -0
- package/dist/sync/cowork-sync.js +567 -11
- package/dist/types.js +51 -20
- package/dist/vm-paths.js +345 -0
- package/docs/README.md +4 -1
- package/docs/cassette.md +46 -28
- package/docs/chat.md +4 -3
- package/docs/cowork-spawn-contract-1.12603.1.md +1 -1
- package/docs/debugging.md +29 -0
- package/docs/discovery.md +6 -0
- package/docs/fidelity-gaps.md +61 -0
- package/docs/invariants.md +1 -1
- package/docs/maintenance.md +26 -1
- package/docs/protocol.md +136 -0
- package/docs/run-status.md +33 -0
- package/docs/scenario.md +104 -11
- package/docs/session.md +2 -0
- package/docs/stats.md +74 -0
- package/examples/README.md +1 -0
- package/examples/replays/README.md +10 -1
- package/examples/replays/example-multiselect-gate.cassette.json +1 -1
- package/examples/replays/example-pdf-skill-ci-selftest-failing.yaml +30 -0
- package/examples/replays/example-pdf-skill.cassette.json +1 -1
- package/examples/replays/hostloop-computer-links.cassette.json +77 -0
- package/fixtures/protocol/v1/allow-response.json +13 -0
- package/fixtures/protocol/v1/deny-response.json +11 -0
- package/fixtures/protocol/v1/hook-callback.json +27 -0
- package/fixtures/protocol/v1/initialize.json +18 -0
- package/fixtures/protocol/v1/mcp-message.json +42 -0
- package/fixtures/protocol/v1/permission-request.json +13 -0
- package/fixtures/protocol/v1/question-answer-response.json +36 -0
- package/fixtures/protocol/v1/question-request.json +33 -0
- package/llms.txt +4 -1
- package/package.json +3 -3
- package/python/test_scenario_lint.py +21 -0
- package/schema/protocol.v1.json +261 -0
- package/schema/run-result.json +24 -2
- package/schema/scenario.schema.json +37 -1
- package/schema/session.schema.json +3 -0
- package/scripts/check-versions.ts +80 -3
- package/scripts/gen-schema.ts +4 -3
|
@@ -3,8 +3,8 @@ name: cowork-harness
|
|
|
3
3
|
description: Test or debug a Claude Code skill/plugin under Claude Cowork's runtime — sandboxed agent, default-deny egress, the can_use_tool permission/question protocol — using the cowork-harness CLI. Use when validating or regression-testing a skill, authoring or debugging a scenario YAML (prompt + scripted answers + assert:), choosing a fidelity tier, scripting AskUserQuestion / tool-permission answers, or asserting artifacts, egress, or sub-agent dispatch. Especially when a harness run no-ops an assertion, fails on an unanswered gate, false-greens, a steered answer never reaches the model, or a web_fetch is unexpectedly denied or gated. NOT for generic unit testing (pytest/vitest of your own scripts) or non-Cowork CI. Covers the skill / run / chat / record / replay / trace / decide / assertions / scaffold commands and the session-vs-scenario split.
|
|
4
4
|
metadata:
|
|
5
5
|
author: cowork-harness
|
|
6
|
-
version: 0.
|
|
7
|
-
tracks-harness: cowork-harness 0.
|
|
6
|
+
version: 0.23.0
|
|
7
|
+
tracks-harness: cowork-harness 0.23.0 (baseline desktop-1.18286.0)
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# cowork-harness
|
|
@@ -22,8 +22,8 @@ flagged with a loud `::warning::`, not silent — auto-answer a gate, observe an
|
|
|
22
22
|
allowlist). This skill exists mostly to keep you out of those traps — the Gotchas section below is
|
|
23
23
|
the highest-value part. Read it.
|
|
24
24
|
|
|
25
|
-
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.
|
|
26
|
-
> `desktop-1.
|
|
25
|
+
> **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.23.0` (baseline
|
|
26
|
+
> `desktop-1.18286.0`). If your checkout is newer, prefer the live `--help`, `SPEC.md`, and
|
|
27
27
|
> `docs/*.md` over this snapshot, and re-run the bundled linter.
|
|
28
28
|
|
|
29
29
|
## 0. Preflight — make sure the harness can actually run
|
|
@@ -38,7 +38,7 @@ cowork-harness skill ./my-skill "do X" # run the skill once against the sta
|
|
|
38
38
|
Before the first command, confirm the CLI is reachable and **fail loud** (never fake a pass) when a tier's dependencies are missing:
|
|
39
39
|
|
|
40
40
|
- **One-shot check.** Run `cowork-harness doctor [--tier <tier>]` first — a read-only prerequisite check that inspects Docker, the staged agent, the token, and the baseline in one pass. The bullets below explain each thing it checks (and how to fix it).
|
|
41
|
-
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.
|
|
41
|
+
- **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.23.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=0.23.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=0.23.0"`. **Pin `@>=0.23.0`, never `@latest`** — `@latest` can silently fetch an older CLI and the new commands fail as "unknown command", whereas the floor **fails loud** if no compatible version is published. (≥ 0.23.0 is what gates the commands/assertions this skill teaches: `assertions --list`, `scaffold <run-id>`, `trace --view dispatches`, `artifact_json` incl. the `in:` operator, `verify-cassettes`, batch `record <dir>`/`--rerecord-stale`, `record --concurrency <N>`, record-time redaction, multiSelect/`answer:`, `verify-run` answer-coverage, `record --max-artifact-bytes`, live record-time deciders, `verify-cassettes --allow-domain`/`--allow-email`/`--allow-path`/`--allow-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field, and `computer_links_resolve` (new in 0.22.0).)
|
|
42
42
|
- **Agent binary (sandboxed live tiers — `container`/`microvm`/`hostloop`/`cowork`).** The staged Claude Code agent is **bind-mounted** from a local Claude Desktop install, or point `COWORK_AGENT_BINARY` at a `claude-code-vm/<ver>/claude` ELF. Nothing is bundled. `protocol` (L0) and `replay` need no staged agent; for the sandboxed tiers, no agent → no run; report that, don't skip silently.
|
|
43
43
|
- **Docker / Lima.** Only `--fidelity protocol` (L0) runs without them. `container` / `microvm` / `hostloop` / `cowork` need Docker (Lima for L2). If they're absent, drop to `--fidelity protocol` and **say so** — a green that never exercised the sandbox is not a sandbox pass.
|
|
44
44
|
- **Auth.** `CLAUDE_CODE_OAUTH_TOKEN` (preferred) or `ANTHROPIC_API_KEY`, via env or `.env`. Minting an OAuth token needs the **`claude` CLI** (`npm i -g @anthropic-ai/claude-code`, then `claude setup-token`).
|
|
@@ -54,8 +54,13 @@ Before the first command, confirm the CLI is reachable and **fail loud** (never
|
|
|
54
54
|
debugging map).
|
|
55
55
|
|
|
56
56
|
Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
|
|
57
|
-
verify-run · trace · inspect · decide · gates · answer · scaffold · assertions --list · sync ·
|
|
58
|
-
boundary-check · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
|
|
57
|
+
verify-run · trace · inspect · diff · stats · decide · gates · answer · scaffold · assertions --list · sync ·
|
|
58
|
+
list · boundary-check · status · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
|
|
59
|
+
|
|
60
|
+
**Two different `scaffold` tools — don't confuse them.** The native `cowork-harness scaffold <run-id>`
|
|
61
|
+
above turns an already-*recorded* run into a scenario (needs a run to exist first). The bundled
|
|
62
|
+
`scripts/scenario.py scaffold --name … --skill …` in §9 builds a scenario from flags alone, no run
|
|
63
|
+
required. Passing §9's flag set to the native command fails with `unknown flag: --name` (exit 2).
|
|
59
64
|
|
|
60
65
|
## 2. Two files: session vs scenario
|
|
61
66
|
|
|
@@ -94,7 +99,8 @@ the folder basename (collision-resolved); there is no `to:` override. See `refer
|
|
|
94
99
|
| `hostloop` / `cowork` | Production split-exec: the agent loop is a **native process on the host** (no container around the file tools — matching production), with native Bash/WebFetch disabled and routed host-side via the workspace SDK-MCP server into a Docker VM sidecar | Highest-fidelity / parity runs. A writable connected folder needs `allow_host_writes: true` (see scenario-schema.md). |
|
|
95
100
|
|
|
96
101
|
Set the tier in the **scenario's `fidelity:` field**, not a flag — `run` rejects `--fidelity`
|
|
97
|
-
(it's a `skill
|
|
102
|
+
(it's a `skill`/`chat` flag; `run` takes fidelity only from the scenario). See
|
|
103
|
+
`references/fidelity-and-answers.md`.
|
|
98
104
|
|
|
99
105
|
## 5. Choose an answer path (gates: AskUserQuestion + tool-permission)
|
|
100
106
|
|
|
@@ -265,7 +271,7 @@ known-good skeleton (right tier, scripted `answers:` + `on_unanswered: fail`, co
|
|
|
265
271
|
separated from live-only ones, one concern per item) and **self-lints its own output**:
|
|
266
272
|
|
|
267
273
|
```bash
|
|
268
|
-
S="${CLAUDE_PLUGIN_ROOT}/
|
|
274
|
+
S="${CLAUDE_PLUGIN_ROOT}/scripts/scenario.py"
|
|
269
275
|
# Working from a repo checkout instead of an installed plugin?
|
|
270
276
|
# S=".claude/skills/cowork-harness/scripts/scenario.py"
|
|
271
277
|
python3 "$S" scaffold --name report-check --skill ./skills/report-gen \
|
|
@@ -288,7 +294,9 @@ keys). Exit code is non-zero on errors (CI-friendly). `scaffold` auto-upgrades t
|
|
|
288
294
|
for egress on `protocol`, so it never emits a scenario `lint` would reject.
|
|
289
295
|
|
|
290
296
|
CI placement: a **token-free `replay` PR gate** (content/structure only) + a **nightly live `run`**
|
|
291
|
-
(filesystem/egress).
|
|
297
|
+
(filesystem/egress). Fastest setup: `uses: yaniv-golan/cowork-harness@v1` (a packaged GitHub Action with a
|
|
298
|
+
PR job-summary reporter). See `references/ci-recipe.md` for the Action, the manual step-by-step form, and
|
|
299
|
+
the four-stage pipeline.
|
|
292
300
|
|
|
293
301
|
## 10. Debugging with `chat`
|
|
294
302
|
|
|
@@ -326,7 +334,8 @@ are the ones that bite hardest.
|
|
|
326
334
|
`replay` skips filesystem/egress keys (`file_exists`, `user_visible_artifact`, `artifact_json`,
|
|
327
335
|
`egress_*`, `no_delete_in_outputs`, `self_heal_ran`, `transcript_no_host_path`); a *mixed* item like
|
|
328
336
|
`{result, egress_denied}` greens on `result` while its `egress_denied` half is dropped. (`record`
|
|
329
|
-
snapshots an `artifacts` manifest, which makes
|
|
337
|
+
snapshots an `artifacts` manifest, which makes
|
|
338
|
+
`file_exists`/`user_visible_artifact`/`artifact_json`/`computer_links_resolve`
|
|
330
339
|
replay-checkable — but the live-only egress keys stay skipped.) *Fix:* put egress/live-only checks on
|
|
331
340
|
a live gate; keep one concern per `assert:` item; run the linter. The harness warns loudly on skip.
|
|
332
341
|
|
|
@@ -389,23 +398,29 @@ are the ones that bite hardest.
|
|
|
389
398
|
prompt. *Fix:* treat `prompt_asset_missing` as a blocking error in CI by checking the signals
|
|
390
399
|
array.
|
|
391
400
|
13. **`result: success` means the agent didn't error, NOT that the task completed — always assert on
|
|
392
|
-
artifacts/content.**
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
401
|
+
artifacts/content.**
|
|
402
|
+
- A turn that ends on a plain-text re-ask ("which file did you mean?") still reports
|
|
403
|
+
`result: success`.
|
|
404
|
+
- The harness catches this with a **`stalled`** verdict signal: a run that ends on a question and
|
|
405
|
+
did **no productive work after its last gate** — both the no-gate case ("which file?" with no
|
|
406
|
+
tool calls) AND the *answered-gate-then-re-ask* case (the agent answers an `AskUserQuestion`,
|
|
407
|
+
then asks again in plain text and stops). Suppress with `allow_stall: true` if ending on a
|
|
408
|
+
question is intended.
|
|
409
|
+
- The signal is a **tool-position heuristic**, not deliverable detection, so it is imprecise both
|
|
410
|
+
ways:
|
|
411
|
+
- **False negative:** a post-gate tool *call* clears the flag whether it **succeeded or
|
|
412
|
+
errored** — an agent that ran a tool after the gate and still stalled is not caught.
|
|
413
|
+
- **False positive:** a deliverable written *before* a final confirmation gate does **not**
|
|
414
|
+
clear it, so a write-then-confirm-then-question run is flagged — use `allow_stall: true` for a
|
|
415
|
+
deliberate confirm-terminal skill.
|
|
416
|
+
- The broad guard is therefore YOUR assertions — assert the deliverable (`file_exists` /
|
|
417
|
+
`artifact_json` / `transcript_matches`), never just `result: success`.
|
|
418
|
+
- `on_unanswered` governs **unanswered** `AskUserQuestion` gates; the `stalled` signal covers
|
|
419
|
+
stalling *after* one is answered — two different failure modes.
|
|
420
|
+
- **Free-text aside:** a "type-it-in-notes" option has **no scripted deterministic answer** today
|
|
421
|
+
(the `OTHER:` directive works only on the LLM-decider path, not scripted `choose:`; on an
|
|
422
|
+
options-bearing gate a bare out-of-set LLM answer fails loud (exit 2) — see the LLM-decider
|
|
423
|
+
free-text note in `references/fidelity-and-answers.md`).
|
|
409
424
|
14. **A positional `choose` (`first` / index) is order-dependent.** `choose: "2"` survives label drift
|
|
410
425
|
but NOT option *re-ordering* — if the gate presents its options in a different order run-to-run, the
|
|
411
426
|
index lands on a different option (a silent re-record flake). Prefer an exact label when order is
|
|
@@ -444,7 +459,8 @@ and the CI recipe, read the files in `references/`.
|
|
|
444
459
|
- `references/scenario-schema.md` — scenario/session YAML schema, full assertion catalog (with each
|
|
445
460
|
key's replay class), the web_fetch model, and the complete gotcha list.
|
|
446
461
|
- `references/fidelity-and-answers.md` — fidelity tiers, answer paths, the determinism contract.
|
|
447
|
-
- `references/ci-recipe.md` — replay-vs-live lane split and the four-stage
|
|
462
|
+
- `references/ci-recipe.md` — the packaged GitHub Action, replay-vs-live lane split, and the four-stage
|
|
463
|
+
GitHub Actions pipeline.
|
|
448
464
|
- `scripts/scenario.py` — `scaffold` a valid scenario skeleton and `lint` scenarios for the
|
|
449
465
|
no-silent-false-green invariants (both usable as CI steps).
|
|
450
466
|
- `../../../docs/run-status.md` — checking a background run's status without `ps aux`.
|
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
# CI recipe — replay vs live lanes
|
|
2
2
|
|
|
3
|
-
Self-contained reference. Tracks `cowork-harness 0.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 0.23.0` (baseline `desktop-1.18286.0`).
|
|
4
4
|
|
|
5
|
-
**
|
|
6
|
-
|
|
5
|
+
**Fastest path: the packaged Action.** One step gets you `replay`/`lint`/`verify-cassettes` plus a PR
|
|
6
|
+
job-summary reporter (verdict table, staleness findings, cost/turns when available):
|
|
7
7
|
|
|
8
8
|
```yaml
|
|
9
|
-
-
|
|
9
|
+
- uses: yaniv-golan/cowork-harness@v1
|
|
10
|
+
with:
|
|
11
|
+
command: replay
|
|
12
|
+
path: cassettes/
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Reach for the manual multi-step form below only when you need per-step control the Action's inputs don't
|
|
16
|
+
cover (a custom flag combination, a different runner matrix per step, or `lint`/`verify-cassettes` gated
|
|
17
|
+
independently instead of as one action run per command).
|
|
18
|
+
|
|
19
|
+
**Live lane (`command: run`) with the packaged Action — self-hosted runner, you stage the binary.** The
|
|
20
|
+
Action never downloads or provisions the agent ELF itself — by design, not an oversight (see "the
|
|
21
|
+
realistic CI shape" below for why). Stage it yourself as an explicit step in *your own* workflow, then
|
|
22
|
+
point the Action at it:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
jobs:
|
|
26
|
+
live:
|
|
27
|
+
runs-on: [self-hosted, linux, arm64] # needs Docker + this staged ELF; not a stock GitHub-hosted runner
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- name: Stage the agent binary (official channel, sha256-verified — see docs/maintenance.md)
|
|
31
|
+
run: |
|
|
32
|
+
V=2.1.197 # match your scenario's pinned baseline's agentVersion
|
|
33
|
+
curl -fSL "https://downloads.claude.ai/claude-code-releases/$V/linux-arm64/claude" -o "$RUNNER_TEMP/claude-$V"
|
|
34
|
+
chmod +x "$RUNNER_TEMP/claude-$V"
|
|
35
|
+
# verify against the committed baseline's sha256 (baselines/desktop-*.json → agentBinary.sha256)
|
|
36
|
+
# before trusting it — see docs/maintenance.md's "Agent-binary provenance" section.
|
|
37
|
+
echo "COWORK_AGENT_BINARY=$RUNNER_TEMP/claude-$V" >> "$GITHUB_ENV"
|
|
38
|
+
- uses: yaniv-golan/cowork-harness@v1
|
|
39
|
+
with:
|
|
40
|
+
command: run
|
|
41
|
+
path: scenarios/
|
|
42
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Why this is a step *you* write, not an Action input the harness provides for you: pulling Anthropic's
|
|
46
|
+
binary is a call about your own relationship with their distribution terms — keeping it in your own
|
|
47
|
+
version-controlled workflow keeps that decision and its execution yours, auditable, and outside any
|
|
48
|
+
third-party action's code. See the [agent-binary provenance runbook](../../../../docs/maintenance.md) for
|
|
49
|
+
the full recovery/verification story (including why `COWORK_AGENT_BINARY` substitutions are
|
|
50
|
+
sha256-*checked* but not hard-blocking on mismatch — it's advisory for an intentional substitution).
|
|
51
|
+
|
|
52
|
+
**Minimal token-free PR gate (manual form)** — the smallest thing worth committing; runs on stock
|
|
53
|
+
GitHub-hosted runners, no token/Docker/agent:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
- run: npm i -g "cowork-harness@>=0.23.0"
|
|
10
57
|
- run: cowork-harness lint scenarios/*.yaml # no silent false-greens
|
|
11
58
|
- run: cowork-harness verify-cassettes cassettes/ # privacy + staleness
|
|
12
59
|
- run: cowork-harness replay cassettes/ # token-free content/structure
|
|
@@ -22,7 +69,8 @@ The split is not just about tokens — it decides **where each lane can run**:
|
|
|
22
69
|
- **`replay` / `verify-cassettes` (token-free, agent-free).** Replays a recorded cassette
|
|
23
70
|
(`events.jsonl` + `control-out.jsonl`) and lints the committed cassettes. **No model tokens, no
|
|
24
71
|
Docker, no agent binary** — runs on a stock GitHub Actions runner. Evaluates **content** assertions
|
|
25
|
-
only (`transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`, `
|
|
72
|
+
only (`transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`, `skill_triggered`,
|
|
73
|
+
`no_skill_triggered`, `max_cost_usd`, `max_tokens`, `tool_calls_max`, `result`, and the verdict
|
|
26
74
|
modifiers `allow_permissive_auto_allow` / `allow_missing_capability` / `allow_l0_plugin_divergence` /
|
|
27
75
|
`allow_stall` (no-op passes); plus the gate keys `question_asked` / `questions_count_max` /
|
|
28
76
|
`gate_answers_delivered` **if** the cassette has `controlOut`). Filesystem/egress assertions are
|
|
@@ -80,8 +128,9 @@ dollar figures). In a skill repo these cassettes get **committed**. So:
|
|
|
80
128
|
`_KEYS`); empty by default. Redaction is **verdict-preserving** — `record` refuses to write if
|
|
81
129
|
redaction would flip an assertion (a manufactured green). `--no-redact` skips it for known-synthetic
|
|
82
130
|
inputs.
|
|
83
|
-
- **Always-on scan gate** — `verify-cassettes` flags email / currency / bare-domain / local-path
|
|
84
|
-
it finds in the committed cassettes and **exits non-zero**, so "no leak" is
|
|
131
|
+
- **Always-on scan gate** — `verify-cassettes` flags email / currency / bare-domain / local-path /
|
|
132
|
+
machine-inventory matches it finds in the committed cassettes and **exits non-zero**, so "no leak" is
|
|
133
|
+
a gate, not discipline.
|
|
85
134
|
Suppress synthetic / public reference names (NVCA, Cooley GO, …) with `--allow <regex>`. (Multi-word
|
|
86
135
|
proper names are NOT a default class — too noisy to gate on; add a pattern via config if your corpus
|
|
87
136
|
needs it.)
|
|
@@ -110,6 +159,12 @@ A typical skill repo runs four stages, fastest/cheapest first:
|
|
|
110
159
|
|
|
111
160
|
## GitHub Actions sketch
|
|
112
161
|
|
|
162
|
+
The PR gate below is the manual, step-by-step version of what `uses: yaniv-golan/cowork-harness@v1` does
|
|
163
|
+
in one step (see the top of this doc) — reach for this form when you need independent per-command
|
|
164
|
+
gating/annotations rather than one action run per command. The nightly live job has no packaged-Action
|
|
165
|
+
equivalent yet (the Action's `command: run` mode needs a self-hosted runner with Docker + the agent binary
|
|
166
|
+
already provisioned, same as the manual form below).
|
|
167
|
+
|
|
113
168
|
PR gate (token-free — runs on every push):
|
|
114
169
|
|
|
115
170
|
```yaml
|
|
@@ -124,7 +179,7 @@ jobs:
|
|
|
124
179
|
with: { node-version: '20' }
|
|
125
180
|
- uses: actions/setup-python@v5
|
|
126
181
|
with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
|
|
127
|
-
- run: npm i -g cowork-harness@>=0.
|
|
182
|
+
- run: npm i -g "cowork-harness@>=0.23.0"
|
|
128
183
|
- run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
|
|
129
184
|
- run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
|
|
130
185
|
- run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
|
|
@@ -153,7 +208,7 @@ jobs:
|
|
|
153
208
|
echo "live=true" >> "$GITHUB_OUTPUT"
|
|
154
209
|
fi
|
|
155
210
|
- if: steps.guard.outputs.live == 'true'
|
|
156
|
-
run: npm i -g cowork-harness@>=0.
|
|
211
|
+
run: npm i -g "cowork-harness@>=0.23.0"
|
|
157
212
|
- if: steps.guard.outputs.live == 'true'
|
|
158
213
|
run: cowork-harness run scenarios/ --output-format json
|
|
159
214
|
env:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Fidelity tiers & answer paths
|
|
2
2
|
|
|
3
|
-
Self-contained reference. Tracks `cowork-harness 0.
|
|
3
|
+
Self-contained reference. Tracks `cowork-harness 0.23.0` (baseline `desktop-1.18286.0`).
|
|
4
4
|
|
|
5
5
|
## Fidelity tiers (`fidelity:` in the scenario)
|
|
6
6
|
|
|
@@ -187,5 +187,6 @@ and `scaffold` refuse to treat a partial run's half-finished output as a passing
|
|
|
187
187
|
redact from logs (beyond the auth tokens + `ANTHROPIC_CUSTOM_HEADERS`).
|
|
188
188
|
- `COWORK_HARNESS_SOFT_MISSING` — downgrade a missing mount source from hard-error to warn-and-skip.
|
|
189
189
|
- `COWORK_VM_GATEWAY` / `COWORK_VM_PROXY_PORT` / `COWORK_LIMA_INSTANCE` — L2 (microVM) knobs.
|
|
190
|
-
- `COWORK_LOCKDOWN` — default `on`:
|
|
191
|
-
|
|
190
|
+
- `COWORK_LOCKDOWN` — default `on`: gates sandbox hardening on every isolated tier — **aborts loudly**
|
|
191
|
+
if the L2 guest firewall fails to apply (no silent unprotected run), and also gates the
|
|
192
|
+
`container`/`hostloop` Docker hardening. Set `=off` to opt out and run without isolation deliberately.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Scenario & session schema, assertion catalog, web_fetch, full gotchas
|
|
2
2
|
|
|
3
|
-
Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.
|
|
4
|
-
(baseline `desktop-1.
|
|
3
|
+
Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.23.0`
|
|
4
|
+
(baseline `desktop-1.18286.0`). If your checkout is newer, prefer the live `docs/scenario.md`,
|
|
5
5
|
`docs/session.md`, and `SPEC.md`.
|
|
6
6
|
|
|
7
7
|
**Minimal scenario** — `prompt` is the only required field:
|
|
@@ -61,6 +61,12 @@ skills: [report-gen] # OPTIONAL — scope the cassette-staleness
|
|
|
61
61
|
# `skills/<name>` dir under a mounted plugin-root) + the plugin's shared
|
|
62
62
|
# roots. Fail-closed to whole-tree on an unknown name. Omit = whole tree.
|
|
63
63
|
|
|
64
|
+
requires_capabilities: [ocr] # OPTIONAL — declare a capability the skill needs (e.g. office_convert,
|
|
65
|
+
# ocr, pdf_tables, opencv). If the running agent image provably omits
|
|
66
|
+
# one, the harness ABORTS before the paid run (exit 3) — unless the
|
|
67
|
+
# scenario also asserts `allow_missing_capability: true`, which downgrades
|
|
68
|
+
# the abort to a notice and proceeds. Live tiers only.
|
|
69
|
+
|
|
64
70
|
allow_host_writes: true # OPTIONAL — required to run `hostloop` fidelity with a WRITABLE
|
|
65
71
|
# connected folder (session `mode: rw`/`rwd`): with no container
|
|
66
72
|
# around hostloop's native file tools, that combination gives the
|
|
@@ -80,8 +86,10 @@ session = your setup.**
|
|
|
80
86
|
```yaml
|
|
81
87
|
# model & reasoning
|
|
82
88
|
model: claude-opus-4-8 # omit for the agent default
|
|
89
|
+
account_name: my-account # OPTIONAL — which configured account/credential set to run as
|
|
83
90
|
effort: high # low | medium | high | xhigh
|
|
84
91
|
max_thinking_tokens: 31999 # positive int, or per-model map {default, <model>: <n>}; default 31999
|
|
92
|
+
extended_thinking: true # INERT — not a real Cowork toggle; use max_thinking_tokens instead
|
|
85
93
|
permission_mode: default # default | acceptEdits | plan | bypassPermissions
|
|
86
94
|
permission_parity: cowork # cowork (unscripted tool calls allowed) | strict (deny unscripted)
|
|
87
95
|
|
|
@@ -97,8 +105,9 @@ plugins:
|
|
|
97
105
|
marketplaces: [] # plugin_marketplaces (git URLs or local paths)
|
|
98
106
|
local_marketplaces: [] # local marketplace dirs (each has a marketplace.json)
|
|
99
107
|
enabled: [my-skill@local] # enabledPlugins (name@marketplace)
|
|
100
|
-
local_plugins: [./skills/my-skill] # host plugin dirs → mnt/.local-plugins/marketplaces/<
|
|
101
|
-
# (≥1.14271.0
|
|
108
|
+
local_plugins: [./skills/my-skill] # host plugin dirs → mnt/.local-plugins/marketplaces/local-desktop-app-uploads/<plugin>
|
|
109
|
+
# (the marketplace segment is that fixed synthetic name; ≥1.14271.0 —
|
|
110
|
+
# older baselines use mnt/.local-plugins/cache)
|
|
102
111
|
remote_plugins: []
|
|
103
112
|
skills:
|
|
104
113
|
local: [] # extra host skill dirs
|
|
@@ -220,6 +229,12 @@ passes only if every key passes. Keep one concern per item unless you mean conju
|
|
|
220
229
|
| `subagent_dispatched: <regex>` | a sub-agent whose `agentType` **or dispatch description** matches |
|
|
221
230
|
| `subagent_declared_but_unused: <Tool>` | a sub-agent declared the tool but never used **that** tool (even if it used others) |
|
|
222
231
|
| `dispatch_count_max: <N>` | at most N sub-agents dispatched (records only — does NOT enforce a cap) |
|
|
232
|
+
| `skill_triggered: <regex>` | a skill matching the regex (invoked id, e.g. `"plugin:skill"`) was invoked via the `Skill` tool — evidence-unavailable (not a normal fail) if the agent's init tools have no `Skill` tool |
|
|
233
|
+
| `no_skill_triggered: <regex>` | no invoked skill id matched — the negative-control / description-collision catcher; evidence-unavailable (never a vacuous pass) if invocation data is absent or the `Skill` tool is unobservable |
|
|
234
|
+
| `max_cost_usd: <N>` | the run's SDK-reported cost is ≤ N USD — evidence-unavailable if cost telemetry is absent. **Replay asserts the frozen recording's cost, not fresh spend** — a real regression needs a live `run` |
|
|
235
|
+
| `max_tokens: <N>` | `usage.input_tokens + usage.output_tokens` ≤ N (cache tokens excluded) — same replay caveat as `max_cost_usd` |
|
|
236
|
+
| `tool_calls_max: <N>` | total top-level tool calls (sum of `toolCounts`) ≤ N — meaningfully replay-checkable (re-drive recomputes `toolCounts` deterministically) |
|
|
237
|
+
| `max_turns: <N>` | the SDK-reported (or fallback-counted) turn count ≤ N — meaningfully replay-checkable (re-drive recounts turns deterministically, same as `tool_calls_max`) |
|
|
223
238
|
| `question_asked: <regex>` | the agent asked an AskUserQuestion whose text matches |
|
|
224
239
|
| `questions_count_max: <N>` | the agent asked at most N questions |
|
|
225
240
|
| `gate_answers_delivered: true` | every answered gate's answer reached the model (observed `tool_result`; unobserved = fail) |
|
|
@@ -228,10 +243,11 @@ passes only if every key passes. Keep one concern per item unless you mean conju
|
|
|
228
243
|
| `allow_missing_capability: true` | verdict modifier — suppresses the default-fail when the (partial "core") agent image omits a capability the skill used but real Cowork ships (OCR/LibreOffice/markitdown/opencv/PDF-tables). Assert only when the skill's fallback is genuinely equivalent; otherwise rebuild full parity (`--build-arg COWORK_FULL_PARITY=1`). Also opts out of the `requires_capabilities` declared-need check. Live tiers only |
|
|
229
244
|
| `allow_l0_plugin_divergence: true` | verdict modifier — opt into L0/protocol plugin divergence: suppresses the default-fail when a plugin behaves differently at `protocol` (L0) fidelity than under a sandboxed tier. Live tiers only |
|
|
230
245
|
| `allow_stall: true` | verdict modifier — suppresses the `stalled` default-fail when a run ends on a question having done no productive tool work after its last gate (the agent asked for input and stopped — incl. re-asking in plain text after answering an `AskUserQuestion`); assert only when ending on a question is intended, else script the answer (`answer:` / `--answer` / a decider) |
|
|
231
|
-
| `transcript_no_host_path: true` | no host path (`/Users/`, `/opt/cowork/`, `/home/`, `/root/`) leaked into model-visible text — **incompatible with `hostloop`**:
|
|
246
|
+
| `transcript_no_host_path: true` | no host path (`/Users/`, `/opt/cowork/`, `/home/`, `/root/`) leaked into model-visible text — **incompatible with `hostloop` AND `protocol`**: hostloop's native file tools legitimately expose real host paths, and protocol (L0) runs the agent's file tools on the real host cwd with no sealed filesystem, so this fails BY DESIGN on both (the harness warns at run start if asserted anyway); use `container`/`microvm` for this check |
|
|
232
247
|
| `egress_denied: <host>` | the host was blocked by the egress proxy |
|
|
233
248
|
| `egress_allowed: <host>` | the host was allowed through |
|
|
234
249
|
| `artifact_json: {artifact, path, …}` | assert a JSON artifact's contents — `equals`/`gt`/`in`/`exists`/`absent`/`is_null` over a dotted `path` (`in` = membership in a list, for a stochastic/LLM value; `absent` ≠ `is_null`; an unresolved intermediate fails loud) |
|
|
250
|
+
| `computer_links_resolve: true` | every `computer://` link in the model-visible transcript resolves to an artifact that exists in the run's collected outputs/mounts — a dangling link fails, naming which target was checked (a live host path, the collected work tree, or the replay manifest). Zero links in the transcript **passes** (presence-gated separately — pair with `transcript_contains` if you also need a link to show up). **Only `true` is valid** (`false` is rejected by the schema) |
|
|
235
251
|
|
|
236
252
|
`expect_denied: [host, …]` adds one `egress_denied` per host. Run `cowork-harness assertions --list` for this
|
|
237
253
|
table from the live schema. Example: `artifact_json: { artifact: outputs/cap.json, path: me.run_id, equals: "r1" }`.
|
|
@@ -270,21 +286,26 @@ sourcing ≠ evaluation (replay warns when you edit one). `verify-run` is the on
|
|
|
270
286
|
*run dir*; `--assert-from` is the equivalent for a *cassette*.
|
|
271
287
|
|
|
272
288
|
**Evaluated on replay (content):** `transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`,
|
|
273
|
-
`
|
|
274
|
-
`
|
|
289
|
+
`skill_triggered`, `no_skill_triggered`, `max_cost_usd`, `max_tokens`, `tool_calls_max`, `max_turns`, `result`
|
|
290
|
+
(`max_cost_usd`/`max_tokens` assert the frozen recording's spend on replay, not fresh spend). The verdict
|
|
291
|
+
modifiers `allow_permissive_auto_allow` / `allow_missing_capability` / `allow_l0_plugin_divergence` /
|
|
292
|
+
`allow_stall` are also kept on replay, evaluated as no-op passes.
|
|
275
293
|
|
|
276
294
|
**Gate keys — replay only with a `controlOut` cassette:** `question_asked`, `questions_count_max`,
|
|
277
295
|
`gate_answers_delivered`. With `controlOut` present they evaluate; on an old cassette without it, a
|
|
278
296
|
**loud warning** fires and they are **excluded** (not vacuously passed). Re-record to enable them.
|
|
279
297
|
|
|
280
298
|
**Filesystem — replay-checkable WITH an artifact manifest:** `file_exists`, `user_visible_artifact`,
|
|
281
|
-
`artifact_json` run on replay when the cassette carries an `artifacts` snapshot
|
|
282
|
-
`outputs/` + connected folders; `replay` materializes it). `artifact_json` needs the
|
|
283
|
-
inlined; a hash-only entry still satisfies `file_exists`.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
299
|
+
`artifact_json`, `computer_links_resolve` run on replay when the cassette carries an `artifacts` snapshot
|
|
300
|
+
(`record` captures `outputs/` + connected folders; `replay` materializes it). `artifact_json` needs the
|
|
301
|
+
small-file JSON `body` inlined; a hash-only entry still satisfies `file_exists`. `computer_links_resolve`
|
|
302
|
+
resolves a `/sessions/…/mnt/…`-shaped link directly against the manifest, and a host-shaped (hostloop) link
|
|
303
|
+
by first normalizing it to a mount-relative path (recorded connected-folder prefixes + the outputs/uploads
|
|
304
|
+
mounts) — replay has no live filesystem to check a host path against directly (that only happens on a live
|
|
305
|
+
`run`/`verify-run`). Without a manifest (older cassettes) all four are skipped. A green replay re-confirms
|
|
306
|
+
*record-time* artifacts, not that the current skill still produces them — `replay --strict` fails when the
|
|
307
|
+
staleness `fingerprint` shows ANY skill/baseline drift, or `replay --fail-on-skill-drift` only on
|
|
308
|
+
skill-source drift; every replay result also reports it class-tagged in `staleness[]` for a JSON gate.
|
|
288
309
|
|
|
289
310
|
**Egress + other filesystem — still skipped on replay (live-only):** `no_delete_in_outputs`,
|
|
290
311
|
`self_heal_ran`, `transcript_no_host_path`, `egress_*` / `expect_denied`. These run only on a live `run`/`record`.
|
|
@@ -360,7 +381,8 @@ at the top of this file.
|
|
|
360
381
|
the stochastic path flags the run `nonDeterministic`. The LLM decider is one mechanism, two
|
|
361
382
|
spellings: `on_unanswered: llm` (YAML) and `--decider-llm` (CLI). The bare `--on-unanswered llm`
|
|
362
383
|
is rejected (use `--decider-llm`). `agent` is **retired** — `on_unanswered: agent` is rejected by
|
|
363
|
-
the schema. (`src/types.ts:
|
|
384
|
+
the schema. (`src/types.ts:365` — the `on_unanswered` enum; `src/cli.ts:899` — the CLI-side
|
|
385
|
+
`--on-unanswered` value check.)
|
|
364
386
|
|
|
365
387
|
4. **`--on-unanswered first` is non-deterministic too** — it picks option 1 and is flagged
|
|
366
388
|
`nonDeterministic`; not a deterministic substitute for scripted answers.
|
|
@@ -6,22 +6,29 @@
|
|
|
6
6
|
"allow_permissive_auto_allow",
|
|
7
7
|
"allow_stall",
|
|
8
8
|
"artifact_json",
|
|
9
|
+
"computer_links_resolve",
|
|
9
10
|
"dispatch_count_max",
|
|
10
11
|
"egress_allowed",
|
|
11
12
|
"egress_denied",
|
|
12
13
|
"file_exists",
|
|
13
14
|
"gate_answers_delivered",
|
|
15
|
+
"max_cost_usd",
|
|
16
|
+
"max_tokens",
|
|
17
|
+
"max_turns",
|
|
14
18
|
"no_delete_in_outputs",
|
|
19
|
+
"no_skill_triggered",
|
|
15
20
|
"question_asked",
|
|
16
21
|
"questions_count_max",
|
|
17
22
|
"replay_protocol_fidelity",
|
|
18
23
|
"result",
|
|
19
24
|
"self_heal_ran",
|
|
25
|
+
"skill_triggered",
|
|
20
26
|
"subagent_declared_but_unused",
|
|
21
27
|
"subagent_dispatched",
|
|
22
28
|
"subagent_tool_absent",
|
|
23
29
|
"subagent_tool_used",
|
|
24
30
|
"tool_called",
|
|
31
|
+
"tool_calls_max",
|
|
25
32
|
"tool_not_called",
|
|
26
33
|
"tool_result_contains",
|
|
27
34
|
"tool_result_not_contains",
|
|
@@ -59,12 +59,20 @@ CONTENT_KEYS = {
|
|
|
59
59
|
"subagent_dispatched",
|
|
60
60
|
"subagent_declared_but_unused",
|
|
61
61
|
"dispatch_count_max",
|
|
62
|
+
"skill_triggered",
|
|
63
|
+
"no_skill_triggered",
|
|
64
|
+
"max_cost_usd",
|
|
65
|
+
"max_tokens",
|
|
66
|
+
"tool_calls_max",
|
|
67
|
+
"max_turns",
|
|
62
68
|
}
|
|
63
69
|
# content keys, but only evaluated on replay when the cassette carries controlOut
|
|
64
70
|
GATE_KEYS = {"question_asked", "questions_count_max", "gate_answers_delivered"}
|
|
65
71
|
# manifest-backed: replay-checkable when the cassette carries an `artifacts` manifest (record snapshots one);
|
|
66
72
|
# a manifest-less cassette skips them. Since the 0.3.0 artifact-manifest these are NOT always live-only.
|
|
67
|
-
|
|
73
|
+
# computer_links_resolve joins this bucket (not CONTENT_KEYS): resolving a non-empty link set needs
|
|
74
|
+
# either a live filesystem or the cassette's artifacts manifest — see cassette.ts's manifestKeys comment.
|
|
75
|
+
MANIFEST_KEYS = {"file_exists", "user_visible_artifact", "artifact_json", "computer_links_resolve"}
|
|
68
76
|
# live-only: ALWAYS skipped on replay, with a loud warning (no filesystem, no network on the token-free lane)
|
|
69
77
|
LIVE_ONLY_KEYS = {
|
|
70
78
|
"egress_denied",
|
|
@@ -118,8 +126,9 @@ if UNCLASSIFIED_KEYS:
|
|
|
118
126
|
)
|
|
119
127
|
# Embedded fallback for the top-level scenario keys — kept EQUAL to the generated `topLevelKeys`
|
|
120
128
|
# (test-enforced, like _CLASSIFIED_KEYS for assert keys) so a missing assertion-keys.json can't silently
|
|
121
|
-
# reintroduce key drift. `
|
|
122
|
-
#
|
|
129
|
+
# reintroduce key drift. `assertions` is NOT here — it's a hard error handled by its own special-case
|
|
130
|
+
# in the unknown-key check below (`k != "assertions"`). `profile` is retired vocabulary and now falls
|
|
131
|
+
# through to that same unknown-key check like any other typo — no special-case for it.
|
|
123
132
|
_EMBEDDED_TOP_LEVEL_KEYS = {
|
|
124
133
|
"name",
|
|
125
134
|
"baseline",
|
|
@@ -252,7 +261,7 @@ def lint_doc(doc, path, raw_lines):
|
|
|
252
261
|
|
|
253
262
|
# W: unknown top-level keys (typo or hallucinated schema)
|
|
254
263
|
for k in doc:
|
|
255
|
-
if k not in TOP_LEVEL_KEYS and k != "assertions"
|
|
264
|
+
if k not in TOP_LEVEL_KEYS and k != "assertions":
|
|
256
265
|
findings.append(
|
|
257
266
|
Finding(
|
|
258
267
|
"WARN",
|
|
@@ -418,19 +427,28 @@ def lint_doc(doc, path, raw_lines):
|
|
|
418
427
|
_DQ_REGEX_LINE = re.compile(
|
|
419
428
|
r'^\s*-?\s*(' + "|".join(sorted(REGEX_KEYS)) + r')\s*:\s*"([^"]*\\[^"]*)"'
|
|
420
429
|
)
|
|
430
|
+
# A run of an EVEN number of consecutive backslashes in a double-quoted YAML scalar is a properly
|
|
431
|
+
# paired escape (`\\` -> a literal `\`), so e.g. "\\d+ items" decodes to the valid regex `\d+ items` —
|
|
432
|
+
# not a mistake. An ODD run leaves one backslash unpaired, which is the actual footgun (YAML either
|
|
433
|
+
# eats it or errors, depending on what follows). Only flag the odd case.
|
|
434
|
+
_ODD_BACKSLASH_RUN = re.compile(r"\\+")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _has_unpaired_backslash(s):
|
|
438
|
+
return any(len(m.group(0)) % 2 == 1 for m in _ODD_BACKSLASH_RUN.finditer(s))
|
|
421
439
|
|
|
422
440
|
|
|
423
441
|
def _lint_regex_quoting(path, raw_lines):
|
|
424
442
|
out = []
|
|
425
443
|
for i, line in enumerate(raw_lines, start=1):
|
|
426
444
|
m = _DQ_REGEX_LINE.match(line)
|
|
427
|
-
if m:
|
|
445
|
+
if m and _has_unpaired_backslash(m.group(2)):
|
|
428
446
|
out.append(
|
|
429
447
|
Finding(
|
|
430
448
|
"WARN",
|
|
431
449
|
"regex-double-quoted",
|
|
432
|
-
f"`{m.group(1)}` uses a DOUBLE-quoted regex containing
|
|
433
|
-
f'("{m.group(2)}") — YAML strips
|
|
450
|
+
f"`{m.group(1)}` uses a DOUBLE-quoted regex containing an unescaped backslash "
|
|
451
|
+
f'("{m.group(2)}") — YAML strips it, so the regex is wrong.',
|
|
434
452
|
"Single-quote the regex (e.g. '\\d+') or use a block scalar. Use [\\s\\S] not . to span turns.",
|
|
435
453
|
path,
|
|
436
454
|
i,
|
package/AGENTS.md
CHANGED
|
@@ -39,8 +39,8 @@ protocol layer or run-loop bookkeeping in the CLI.
|
|
|
39
39
|
gate-steering silently no-ops (the O7 bug). ELF-verified; a regression test pins it.
|
|
40
40
|
- **"profile" is retired vocabulary.** Synced release ground truth = **`PlatformBaseline`** (`baseline:` /
|
|
41
41
|
`baselines/`); authored setup = **`SessionConfig`** (`session:` / `sessions/`). The `profile:`
|
|
42
|
-
scenario key is retired vocabulary —
|
|
43
|
-
reintroduce the term.
|
|
42
|
+
scenario key is retired vocabulary — the alias is gone and it is now rejected as an unknown key — do
|
|
43
|
+
not reintroduce the term.
|
|
44
44
|
- **A new assertion must pick its replay class.** *Content* assertions (read only `ctx.transcript` / the
|
|
45
45
|
record) go in `src/run/cassette.ts` `contentKeys` so they run on the token-free `replay` PR gate;
|
|
46
46
|
*filesystem / egress* assertions do NOT — they are excluded **by omission** from `contentKeys`, so they
|