cowork-harness 1.12.0 → 1.13.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.
Files changed (35) hide show
  1. package/.claude/skills/cowork-harness/SKILL.md +16 -11
  2. package/.claude/skills/cowork-harness/references/ci-recipe.md +10 -9
  3. package/.claude/skills/cowork-harness/references/critique.md +72 -0
  4. package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +4 -4
  5. package/.claude/skills/cowork-harness/references/scenario-schema.md +8 -6
  6. package/.claude/skills/cowork-harness/references/task-recipes.md +4 -3
  7. package/.claude/skills/cowork-harness/scripts/scenario.py +14 -1
  8. package/AGENTS.md +6 -1
  9. package/CHANGELOG.md +156 -0
  10. package/CONTRIBUTING.md +1 -1
  11. package/README.md +7 -7
  12. package/RELEASING.md +3 -2
  13. package/dist/cli.js +6 -4
  14. package/dist/critique/command.js +156 -27
  15. package/dist/critique/evaluator.js +114 -40
  16. package/dist/critique/limitations.js +4 -4
  17. package/dist/critique/package-evidence.js +474 -78
  18. package/dist/io.js +57 -1
  19. package/dist/run/analyze-skill.js +6 -3
  20. package/dist/run/cassette.js +6 -5
  21. package/dist/run/doctor.js +6 -5
  22. package/dist/run/envelope.js +6 -4
  23. package/dist/run/execute.js +14 -1
  24. package/dist/run/renderer.js +4 -3
  25. package/dist/run/run-index.js +182 -3
  26. package/dist/run/scenario-tool.js +5 -4
  27. package/docs/critique.md +31 -17
  28. package/docs/scenario.md +4 -1
  29. package/docs/stats.md +44 -5
  30. package/examples/replays/README.md +1 -1
  31. package/package.json +2 -1
  32. package/schema/critique-report.json +117 -16
  33. package/scripts/bump-version.ts +3 -0
  34. package/scripts/check-skill-doc-links.ts +105 -0
  35. package/scripts/check-versions.ts +4 -1
@@ -3,8 +3,8 @@ name: cowork-harness
3
3
  description: Test or debug a Claude Code skill/plugin under Claude Cowork's runtime — sandboxed agent, default-deny egress, the can_use_tool permission/question protocol — using the cowork-harness CLI. Use when validating or regression-testing a skill, authoring or debugging a scenario YAML (prompt + scripted answers + assert:), choosing a fidelity tier, scripting AskUserQuestion / tool-permission answers, or asserting artifacts, egress, or sub-agent dispatch. Especially when a harness run no-ops an assertion, fails on an unanswered gate, false-greens, a steered answer never reaches the model, or a web_fetch is unexpectedly denied or gated. 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. 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: 1.12.0
7
- tracks-harness: cowork-harness 1.12.0 (baseline desktop-1.24012.9)
6
+ version: 1.13.0
7
+ tracks-harness: cowork-harness 1.13.0 (baseline desktop-1.24012.9)
8
8
  ---
9
9
 
10
10
  # cowork-harness
@@ -22,7 +22,7 @@ flagged with a loud `::warning::`, not silent — auto-answer a gate, observe an
22
22
  allowlist). This skill exists mostly to keep you out of those traps — the Gotchas section below is
23
23
  the highest-value part. Read it.
24
24
 
25
- > **Version note:** the facts and `file:line` pointers here track `cowork-harness 1.12.0` (baseline
25
+ > **Version note:** the facts and `file:line` pointers here track `cowork-harness 1.13.0` (baseline
26
26
  > `desktop-1.24012.9`). If your checkout is newer, prefer the live `--help` and — in a repo checkout —
27
27
  > `SPEC.md` / `docs/*.md` over this snapshot, and re-run the bundled linter.
28
28
 
@@ -39,7 +39,7 @@ Before the first command, confirm the CLI is reachable and **fail loud** (never
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
41
  - **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>`.
42
- - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 1.12.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=1.12.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=1.12.0"`. **Pin `@>=1.12.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.
42
+ - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 1.13.0**. If it's missing or older, prefix every command with the version floor `npx "cowork-harness@>=1.13.0" <cmd>` (Node ≥ 20), or install once with `npm i -g "cowork-harness@>=1.13.0"`. **Pin `@>=1.13.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.
43
43
 
44
44
  This skill documents the CURRENT surface, not release history. If `cowork-harness --version` is
45
45
  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)
@@ -70,7 +70,7 @@ reproducible regression (Part II), and **debug** a run that misbehaved or greene
70
70
  then `verify-run` to re-check a suspect assertion — all token-free, no Docker, no re-record. This is
71
71
  the loop 0.32.0's observability is built for; the *Triage* and *Inspecting a run's observability
72
72
  output* sections in **Part III — Debug** are the detail (the fuller human-facing map lives in
73
- `docs/debugging.md` — repo-only, not shipped with the installed skill).
73
+ [`docs/debugging.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/debugging.md) — repo-only, not shipped with the installed skill).
74
74
  - **Multi-turn / interactive reproduction** → `cowork-harness chat` (interactive; gates answered at the
75
75
  TTY, **not** an asserted test — see *Debugging with `chat`* in **Part III — Debug**).
76
76
 
@@ -363,7 +363,7 @@ cassette — has its own recipe:
363
363
  skillHash keys the whole MOUNTED plugin, so on a multi-skill plugin the hash alone cross-pairs
364
364
  critiques of DIFFERENT skills — pair by the report's `(gradedSkillHash, gradedSkill)` pair. **On a pre-1.5.0 CLI the `skill` lane emits no `fingerprint.skillHash` at all**, so a
365
365
  pairing step there silently groups on an absent key instead of erroring — check the field is present, or
366
- require ≥ 1.5.0. See `docs/debugging.md`
366
+ require ≥ 1.5.0. See [`docs/debugging.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/debugging.md)
367
367
  (repo-only) for the full loop.
368
368
 
369
369
  #### Interpreting verdict signals
@@ -416,7 +416,7 @@ Recognize these before "fixing" a non-bug:
416
416
  pass or a defect — assert `no_delete_in_outputs` / `transcript_no_host_path` to hard-fail on it instead.
417
417
 
418
418
  The full 14-code signal table (severity + per-signal opt-out) is in
419
- [`references/scenario-schema.md`](./references/scenario-schema.md); `docs/scenario.md` (repo-only) carries
419
+ [`references/scenario-schema.md`](./references/scenario-schema.md); [`docs/scenario.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/scenario.md) (repo-only) carries
420
420
  the fuller narrative.
421
421
 
422
422
  ### Checking whether a background run is alive
@@ -434,7 +434,7 @@ checkable regardless of PID namespace. The harness prints `[status] <outDir>` to
434
434
  run starts, so capture stderr to get the exact directory — but `<dir>` also accepts the run-dir root
435
435
  passed to `--run-dir` (a directory without its own `status.json`): it scans up to two levels down for the
436
436
  newest session's `status.json` and reads that. `--follow` fails loud on a timeout/staleness
437
- rather than hanging forever. (Fuller recipe in `docs/run-status.md` — repo-only, not in the installed
437
+ rather than hanging forever. (Fuller recipe in [`docs/run-status.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/run-status.md) — repo-only, not in the installed
438
438
  payload; `cowork-harness status --help` has the flags.)
439
439
 
440
440
  **A multi-minute `record`/`run` outlives a short-lived wrapper.** Don't launch a long record from a
@@ -457,6 +457,11 @@ afterthought: the run already wrote its evidence, so you **localize the failure
457
457
  re-run and hope. Start at the triage below, then use the observability output and, when you need to
458
458
  reproduce interactively, `chat`.
459
459
 
460
+ > **"Evidence" below means the run's own record** — events, trace, transcript; what `trace` / `inspect` /
461
+ > `diff` / `verify-run` / `replay --explain` read. `critique`'s **evaluator** grades against a separate,
462
+ > narrower record — `critique-evidence-package.txt`, what a grade was actually computed against — none of
463
+ > the five tools above surface it; see `references/critique.md`.
464
+
460
465
  ### Triage — a run misbehaved, or a green looks wrong
461
466
 
462
467
  <!-- BEGIN triage-canonical -->
@@ -479,7 +484,7 @@ already does.
479
484
  **deliberate fidelity gap** — the harness intentionally does *not* reproduce a few real-Cowork behaviors,
480
485
  so a "bug" you see here that real Cowork also has isn't yours to fix. The tier semantics are in
481
486
  `references/fidelity-and-answers.md` (shipped); the specific deltas vs. real Cowork and the sandbox
482
- boundary model live in `docs/fidelity-gaps.md` / `docs/boundary.md` (repo-only, not in the installed
487
+ boundary model live in [`docs/fidelity-gaps.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/fidelity-gaps.md) / [`docs/boundary.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/boundary.md) (repo-only, not in the installed
483
488
  payload). If the behavior is on that gap list, it's expected — stop debugging your skill.
484
489
 
485
490
  ### Inspecting a run's observability output
@@ -749,7 +754,7 @@ repeats the assertion/replay-relevant ones alongside the schema (a scoped subset
749
754
  `resolved-tier` finding (re-record — the recording now exercises the wrong tier); a cassette with no
750
755
  `effectiveFidelity` at all, or an unloadable pinned `baseline:`, reports `unverifiable-tier` instead
751
756
  (couldn't check — also re-record). Both are `fidelity: cowork`-only; an explicit-tier scenario never
752
- produces them. (Details: `docs/cassette.md` § tier staleness — repo-only.)
757
+ produces them. (Details: [`docs/cassette.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/cassette.md) § tier staleness — repo-only.)
753
758
 
754
759
  22. **`lint` floods CI with INFO advisories that don't apply to you.** *Why:* two rules —
755
760
  `manifest-needs-snapshot` and `gate-needs-controlout` — fire on the mere presence of manifest/gate
@@ -785,5 +790,5 @@ assertion/replay-relevant ones).
785
790
  no-silent-false-green invariants (both usable as CI steps), and `resolve-agent-types <plugin-dir>`
786
791
  (validates a pinned `subagent_type` against the plugin's own `plugin.json` + `agents/*.md`).
787
792
  - Checking a background run's status without `ps aux` — covered in *Checking whether a background run is
788
- alive* (Part II) above; the fuller recipe is in `docs/run-status.md` (repo-only, not shipped with the
793
+ alive* (Part II) above; the fuller recipe is in [`docs/run-status.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/run-status.md) (repo-only, not shipped with the
789
794
  installed skill).
@@ -1,6 +1,6 @@
1
1
  # CI recipe — replay vs live lanes
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 1.12.0` (baseline `desktop-1.24012.9`).
3
+ Self-contained reference. Tracks `cowork-harness 1.13.0` (baseline `desktop-1.24012.9`).
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):
@@ -13,7 +13,7 @@ job-summary reporter (verdict table, staleness findings, cost/turns when availab
13
13
  ```
14
14
 
15
15
  The Action's `version` input defaults to `latest` — intentional so a copy-pasted recipe tracks the current
16
- release; pin an exact version (e.g. `version: "1.12.0"`) for reproducible CI.
16
+ release; pin an exact version (e.g. `version: "1.13.0"`) for reproducible CI.
17
17
 
18
18
  Reach for the manual multi-step form below only when you need per-step control the Action's inputs don't
19
19
  cover (a custom flag combination, a different runner matrix per step, or `lint`/`verify-cassettes` gated
@@ -30,13 +30,14 @@ jobs:
30
30
  runs-on: [self-hosted, linux, arm64] # needs Docker + this staged ELF; not a stock GitHub-hosted runner
31
31
  steps:
32
32
  - uses: actions/checkout@v4
33
- - name: Stage the agent binary (official channel, sha256-verified — see docs/maintenance.md)
33
+ - name: Stage the agent binary (official channel, sha256-verified — see https://github.com/yaniv-golan/cowork-harness/blob/main/docs/maintenance.md)
34
34
  run: |
35
35
  V=2.1.219 # match your scenario's pinned baseline's agentVersion
36
36
  curl -fSL "https://downloads.claude.ai/claude-code-releases/$V/linux-arm64/claude" -o "$RUNNER_TEMP/claude-$V"
37
37
  chmod +x "$RUNNER_TEMP/claude-$V"
38
38
  # verify against the committed baseline's sha256 (baselines/desktop-*.json → agentBinary.sha256)
39
- # before trusting it — see docs/maintenance.md's "Agent-binary provenance" section.
39
+ # before trusting it — see the "Agent-binary provenance" section of
40
+ # https://github.com/yaniv-golan/cowork-harness/blob/main/docs/maintenance.md
40
41
  echo "COWORK_AGENT_BINARY=$RUNNER_TEMP/claude-$V" >> "$GITHUB_ENV"
41
42
  - uses: yaniv-golan/cowork-harness@main
42
43
  with:
@@ -48,7 +49,7 @@ jobs:
48
49
  Why this is a step *you* write, not an Action input the harness provides for you: pulling Anthropic's
49
50
  binary is a call about your own relationship with their distribution terms — keeping it in your own
50
51
  version-controlled workflow keeps that decision and its execution yours, auditable, and outside any
51
- third-party action's code. The agent-binary provenance runbook (`docs/maintenance.md` in the repo —
52
+ third-party action's code. The agent-binary provenance runbook ([`docs/maintenance.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/maintenance.md) in the repo —
52
53
  not shipped with the installed skill) has the full recovery/verification story (including why
53
54
  `COWORK_AGENT_BINARY` substitutions are
54
55
  sha256-*checked* but not hard-blocking on mismatch — it's advisory for an intentional substitution).
@@ -57,7 +58,7 @@ sha256-*checked* but not hard-blocking on mismatch — it's advisory for an inte
57
58
  GitHub-hosted runners, no token/Docker/agent:
58
59
 
59
60
  ```yaml
60
- - run: npm i -g "cowork-harness@>=1.12.0"
61
+ - run: npm i -g "cowork-harness@>=1.13.0"
61
62
  - run: cowork-harness lint scenarios/*.yaml # no silent false-greens
62
63
  - run: cowork-harness verify-cassettes cassettes/ # privacy + staleness
63
64
  - run: cowork-harness replay cassettes/ # token-free content/structure
@@ -103,7 +104,7 @@ The split is not just about tokens — it decides **where each lane can run**:
103
104
  `allow_stall` (no-op passes); plus the gate keys `question_asked` / `questions_count_max` /
104
105
  `gate_answers_delivered` **if** the cassette has `controlOut`). Filesystem/egress assertions are
105
106
  skipped on this token-free lane — loudly: replay emits an `::warning::` annotation whenever it drops
106
- one (see docs/cassette.md). This is your **always-on PR gate**. With `--output-format json`, read each
107
+ one (see [docs/cassette.md](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/cassette.md)). This is your **always-on PR gate**. With `--output-format json`, read each
107
108
  `results[].verdict.{pass,signals}` for **per-cassette** pass/fail and the reason (the top-level `ok`
108
109
  collapses the whole batch) — e.g. a `stalled` signal fails a cassette whose assertions all passed.
109
110
  The PR gate evaluates the assertions **frozen in the cassette**; to re-check against an edited on-disk
@@ -221,7 +222,7 @@ jobs:
221
222
  with: { node-version: '20' }
222
223
  - uses: actions/setup-python@v5
223
224
  with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
224
- - run: npm i -g "cowork-harness@>=1.12.0"
225
+ - run: npm i -g "cowork-harness@>=1.13.0"
225
226
  - run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
226
227
  - run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
227
228
  - run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
@@ -250,7 +251,7 @@ jobs:
250
251
  echo "live=true" >> "$GITHUB_OUTPUT"
251
252
  fi
252
253
  - if: steps.guard.outputs.live == 'true'
253
- run: npm i -g "cowork-harness@>=1.12.0"
254
+ run: npm i -g "cowork-harness@>=1.13.0"
254
255
  - if: steps.guard.outputs.live == 'true'
255
256
  run: cowork-harness run scenarios/ --output-format json
256
257
  env:
@@ -0,0 +1,72 @@
1
+ # Critique — the facts a plugin install can't otherwise reach
2
+
3
+ Tracks `cowork-harness 1.13.0` (baseline `desktop-1.24012.9`). This is **not** a trim of the full
4
+ [`docs/critique.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/critique.md) (repo-only —
5
+ flags, cost, reproduction discipline, known limitations all live there). This file covers exactly what a
6
+ plugin install cannot otherwise discover: the run-dir artifact a harvester actually reads, the report's
7
+ real field names, and what the evaluator was and was not shown. See **Recipe 5** in `task-recipes.md` for
8
+ the harvest → reproduce → fix loop this feeds.
9
+
10
+ ## The run-dir artifact a harvester reads
11
+
12
+ Beyond `critique-report.json` (the machine-readable report), a critique writes **`critique-evidence-package.txt`**
13
+ at the run-dir root whenever the evaluator ran — the **armored** corpus the evaluator was actually graded
14
+ against. Re-grade a disputed finding **offline** against this exact file instead of re-running the skill: a
15
+ finding's `evidence` excerpt must resolve verbatim against this package, or it landed in `DROPPED`.
16
+
17
+ | File | When | What |
18
+ |---|---|---|
19
+ | `critique-report.json` | always | the report a harvester parses |
20
+ | `critique-evidence-package.txt` | evaluator ran | the exact armored corpus graded — re-gradeable offline |
21
+ | `critique-salvage.json` | exit 2 only | raw pre-parse evaluator replies, for salvage without console-scraping |
22
+
23
+ ## Cost across critiques — the index, not the reports
24
+
25
+ A critique is FOUR model workloads but only TWO produce a run, so only two produce index rows; the
26
+ evaluator passes produce none. Each critique therefore appends a **roll-up row** (`critiqueRole:"rollup"`)
27
+ carrying `critiqueTotalUsd` — the whole four-workload spend. Its own `costUsd` is the **evaluator passes
28
+ only**, so `sum(costUsd)` over every row is exactly true spend with nothing double-counted or missed. The
29
+ turn rows carry `critiqueRole:"task"` / `"reflection"`.
30
+
31
+ Two things a harvester needs: roll-ups are excluded from `stats` aggregation (they carry no verdict —
32
+ counting them adds a phantom run and drags `passRate` toward 1), so **filter them out of any pass-rate
33
+ computed over raw rows**; and a roll-up with `result:"error"` had an unpriced workload, so its totals
34
+ UNDERCOUNT. The index is the only cost record that survives run-dir pruning.
35
+
36
+ ## The report's item shape — no `title`, no `summary`
37
+
38
+ Each `items[]` entry's prose fields are **`idea`** and **`recommendedAction`** — there is no `title` field
39
+ and no `summary` field. A consumer that parses for either gets silently blank output instead of an error.
40
+ Full required set: `source` (`evaluator`|`self-report`), `idea`, `classification`, `evidence` (the cited
41
+ excerpt, verbatim-checked against the evidence package), `recommendedAction`. The optional
42
+ `findingFingerprint` hashes `idea`+`classification`+`recommendedAction` — high-precision, low-recall: a
43
+ match proves the same finding recurred; a mismatch does NOT prove it didn't (the same finding reworded
44
+ fingerprints differently).
45
+
46
+ ## What the evaluator was actually shown — `evidenceBudget`
47
+
48
+ Skill-authored content (`SKILL.md`, every `references/**` file, `agents/<skill>.md`) ships **WHOLE, not
49
+ rationed** — up to a **512 KiB combined ceiling** across all three together. A breach cuts **loudly**: the
50
+ named file and byte counts are reported, never silent, never refused. The **transcript** is bounded
51
+ separately at **128 KiB**, cut **head+tail with an elided middle**, so a run's setup and its conclusion
52
+ both survive a cut instead of just one end.
53
+
54
+ The report's `evidenceBudget` object says exactly what was shown — read it instead of inferring budgets
55
+ from `dist/` source:
56
+
57
+ | Field | What |
58
+ |---|---|
59
+ | `corpusBytes` | total skill-content bytes found, BEFORE any cut |
60
+ | `corpusCeiling` | the 512 KiB combined ceiling |
61
+ | `corpusCuts` | per-file cut record — empty on every real skill; non-empty only once the ceiling is actually breached |
62
+ | `corpusExcluded` | skill files present on the host but never delivered to the agent (see below) |
63
+ | `trimRecord` | which section the transcript trim shaved, and by how much |
64
+ | `packageTruncated` | `true` if ANY section was cut — check this, not `corpusCuts`, for "was anything trimmed": a transcript-only cut leaves `corpusCuts` empty and would otherwise read as "nothing cut" |
65
+
66
+ ## An untracked skill file is not graded
67
+
68
+ Staging delivers **git-tracked files only** (same rule as a marketplace plugin install) — an untracked
69
+ file under the skill folder was never in the agent's mount, so grading against it would manufacture a
70
+ false `already-covered` verdict. It is named in `corpusExcluded` instead, and an untracked `SKILL.md`
71
+ specifically reports `skillMdStatus: "untracked"`, forcing the mechanical `already-covered` →
72
+ `not-adjudicable` downgrade. `git add` it (or commit before critiquing) if it should count as evidence.
@@ -1,6 +1,6 @@
1
1
  # Fidelity tiers & answer paths
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 1.12.0` (baseline `desktop-1.24012.9`).
3
+ Self-contained reference. Tracks `cowork-harness 1.13.0` (baseline `desktop-1.24012.9`).
4
4
 
5
5
  ## Fidelity tiers (`fidelity:` in the scenario)
6
6
 
@@ -131,7 +131,7 @@ So a helper keys its answer on `questions[N].question`, enumerates choices from
131
131
  The `reply_with` field is a literal fill-in template for the reply shape (array placeholder when
132
132
  multiSelect). Notes: `options` is *optional* — a free-text / header-only gate arrives with no
133
133
  `options`; key off `question` (falling back to `header`). This is the shared wire model for both
134
- channels — `docs/decider-dir.md` shows the same shape, and the Python `serve_decider(fn)` adapter
134
+ channels — [`docs/decider-dir.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/decider-dir.md) shows the same shape, and the Python `serve_decider(fn)` adapter
135
135
  hands `fn` exactly this dict.
136
136
 
137
137
  ### `--on-unanswered` accepted values (precise)
@@ -207,7 +207,7 @@ up often enough to spell out:
207
207
  Everything else that *is* evaluated is checked against the **recording**, not fresh behavior — a
208
208
  green replay says the skill produced these events when it was recorded, not that it still does
209
209
  (`staleness[]` flags skill/baseline drift as a hint; only a live `run` re-confirms current
210
- behavior). See `docs/cassette.md` § "Still skipped on replay" and `docs/scenario.md` § "Which
210
+ behavior). See [`docs/cassette.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/cassette.md) § "Still skipped on replay" and [`docs/scenario.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/scenario.md) § "Which
211
211
  assertions survive replay."
212
212
  - **Container-only assertions can't verify off the `container` tier.** `no_scratchpad_leak` and
213
213
  `present_files_called` check the `present_files` delivery path, which is served **only** on
@@ -222,7 +222,7 @@ up often enough to spell out:
222
222
  class of Cowork bug (a client-side write-back to a relative URL that resolves-but-fails against
223
223
  Cowork's own origin, or a broken blob-download fallback) is invisible to any live run, however
224
224
  faithfully sandboxed, because it only manifests in a rendered DOM a human is driving. See
225
- `docs/fidelity-gaps.md` § "Browser↔webview↔human-interaction boundary."
225
+ [`docs/fidelity-gaps.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/fidelity-gaps.md) § "Browser↔webview↔human-interaction boundary."
226
226
 
227
227
  ## Relevant environment variables
228
228
 
@@ -1,8 +1,8 @@
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 1.12.0`
4
- (baseline `desktop-1.24012.9`). If your checkout is newer, prefer the live `docs/scenario.md`,
5
- `docs/session.md`, and `SPEC.md`.
3
+ Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 1.13.0`
4
+ (baseline `desktop-1.24012.9`). If your checkout is newer, prefer the live [`docs/scenario.md`](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/scenario.md),
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` is the only required field:
8
8
 
@@ -94,10 +94,12 @@ session = your setup.**
94
94
  # model & reasoning
95
95
  model: claude-opus-4-8 # omit for the agent default
96
96
  account_name: my-account # OPTIONAL — display name rendered into {{accountName}} / the prompt's
97
- # "User name:" line; NOT a credential/identity selector (see src/prompt.ts, docs/session.md)
97
+ # "User name:" line; NOT a credential/identity selector (see src/prompt.ts,
98
+ # https://github.com/yaniv-golan/cowork-harness/blob/main/docs/session.md)
98
99
  effort: high # low | medium | high | xhigh | max (+ extra, normalized to xhigh); validated against
99
- # the resolved model's offered levels (docs/session.md); omit for Cowork's medium
100
- # fallback — real Cowork always emits --effort, never omits it
100
+ # the resolved model's offered levels
101
+ # (https://github.com/yaniv-golan/cowork-harness/blob/main/docs/session.md);
102
+ # omit for Cowork's medium fallback — real Cowork always emits --effort, never omits it
101
103
  extended_thinking: true # real Cowork on/off toggle; default true (ON) -> --max-thinking-tokens 31999,
102
104
  # or --thinking disabled when false (no arbitrary budget in real Cowork)
103
105
  agent_max_turns: 500 # optional turn ceiling -> agent --max-turns; omit for the agent default
@@ -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 1.12.0` (baseline `desktop-1.24012.9`), same as SKILL.md's front-matter. Recipe 2's `resolved-tier`/`unverifiable-tier` staleness classes and
5
+ Tracks `cowork-harness 1.13.0` (baseline `desktop-1.24012.9`), 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
 
@@ -178,13 +178,14 @@ Hardening a skill is a loop: run → read what it did → fix → run again. Two
178
178
  `--resume` (both pin one run dir) and `--decider-cmd`/`--decider-dir` (a driving agent x N is not a
179
179
  measurement).
180
180
  The full loop (harvest -> reproduce -> fix -> prove freshness -> compare) is written out end-to-end in
181
- docs/debugging.md under "The whole loop, end to end". The harness now SHIPS a grader — `cowork-harness critique <skill-folder> --prompt "<probe>"` runs the
181
+ [docs/debugging.md](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/debugging.md) under "The whole loop, end to end". The harness now SHIPS a grader — `cowork-harness critique <skill-folder> --prompt "<probe>"` runs the
182
182
  skill, asks the agent what confused it, and grades that self-report against a frozen record of the run
183
183
  (blinded evaluator + mechanical citation checking). **Critiquing a document-analysis skill?** The probe
184
184
  attaches nothing on its own — pass `--upload <path>` (repeatable) or `--folder <dir>` exactly as you
185
185
  would to `skill`, or the graded run has no file and "there was no file attached" is the correct finding,
186
186
  not a skill defect. Source flags reach both spawned turns automatically — see SKILL.md's
187
- floor list. See docs/critique.md for the full flag table, cost and limits. If you
187
+ floor list. See [docs/critique.md](https://github.com/yaniv-golan/cowork-harness/blob/main/docs/critique.md) for the full flag table, cost and limits (or the shipped
188
+ [`references/critique.md`](./critique.md) for the report/evidence-package shapes without leaving the plugin payload). If you
188
189
  prefer to build your own grader, the substrate is still here:
189
190
  - `result.json` → `finalMessage` (the skill's own answer/critique) + `toolResults[]` (tool outputs).
190
191
  - `cowork-harness trace <run-dir> --output-format json` → the tool-call stream. Add `--full-results` so
@@ -1400,7 +1400,11 @@ def main(argv=None):
1400
1400
  )
1401
1401
  # The wrapper (`cowork-harness lint`) sets COWORK_HARNESS_PROG so usage/error lines name a command the
1402
1402
  # user can actually run. Default = the script's own basename, because scenario.py is ALSO documented as
1403
- # directly runnable (SKILL.md, docs/gotchas.md, docs/plugin-root.md, docs/subagents.md) — hardcoding
1403
+ # directly runnable (SKILL.md;
1404
+ # https://github.com/yaniv-golan/cowork-harness/blob/main/docs/gotchas.md,
1405
+ # https://github.com/yaniv-golan/cowork-harness/blob/main/docs/plugin-root.md,
1406
+ # https://github.com/yaniv-golan/cowork-harness/blob/main/docs/subagents.md — repo-only, not
1407
+ # shipped with the installed skill) — hardcoding
1404
1408
  # "cowork-harness lint" would lie on that path.
1405
1409
  prog = os.environ.get("COWORK_HARNESS_PROG")
1406
1410
  sub = ap.add_subparsers(dest="command", required=True)
@@ -1505,6 +1509,15 @@ def main(argv=None):
1505
1509
  args, extras = ap.parse_known_args(argv)
1506
1510
  if extras:
1507
1511
  target = sub.choices.get(getattr(args, "command", "") or "")
1512
+ # `--min-severity` is `lint`-only (it filters lint's severity-classed findings; `lint-skill`'s two
1513
+ # footgun checks have no severity ladder to filter). Passing it to `lint-skill` used to fall through
1514
+ # to the bare "unrecognized arguments" message below and leave the user hunting for a flag that DOES
1515
+ # exist, just on the sibling command — name it explicitly, same as the `--output-format` translation
1516
+ # `runLintLike` (src/run/scenario-tool.ts) already does for the other lint/lint-skill flag mismatch.
1517
+ if getattr(args, "command", None) == "lint-skill" and any(
1518
+ e == "--min-severity" or e.startswith("--min-severity=") for e in extras
1519
+ ):
1520
+ (target or ap).error("unrecognized arguments: " + " ".join(extras) + " (--min-severity is a `lint` flag, not `lint-skill` — rerun with `cowork-harness lint` instead)")
1508
1521
  (target or ap).error("unrecognized arguments: " + " ".join(extras))
1509
1522
  return args.func(args)
1510
1523
 
package/AGENTS.md CHANGED
@@ -136,7 +136,12 @@ the narrow case of advancing a branch nobody has checked out — it just cannot
136
136
  `main` itself, so the base can still move under an in-flight rebase. Separate worktrees fix that half.
137
137
 
138
138
  **Two one-time costs per worktree**, both benign: no `./.env` (use `--dotenv <primary>/.env` — `doctor`
139
- detects this and prints the remedy, see `docs/gotchas.md`), and no `node_modules` (`npm ci` before tests).
139
+ detects this and prints the remedy, see `docs/gotchas.md`), and no `node_modules` — run **`npm ci`**.
140
+
141
+ **Do NOT symlink `node_modules` from the primary** to skip that install. `.gitignore`'s first line is
142
+ `node_modules/` — with a trailing slash, so it matches a *directory* only. A symlink is not a directory to
143
+ git, so it shows up as `?? node_modules` and breaks every clean-tree gate, `npm run preflight` included.
144
+ Verified both ways: a real `node_modules/` directory is correctly ignored; a symlink is not.
140
145
 
141
146
  **Structural safety, not just etiquette:** git refuses to check out `main` in a second worktree
142
147
  (`fatal: 'main' is already used by worktree at …`), so the integration branch cannot be double-held by
package/CHANGELOG.md CHANGED
@@ -6,6 +6,162 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.13.0] — 2026-07-28
10
+
11
+ **Upgrade notes.**
12
+
13
+ - **`critique` verdicts are not comparable across this release — but the counts still are, and that
14
+ distinction decides what to do with your history.** The evaluator now sees materially more of your skill,
15
+ so a *per-item* verdict may flip for reasons that have nothing to do with your edits: do not diff
16
+ individual findings across the boundary, and re-baseline anything that tracks a skill's progress
17
+ report-to-report. **Do NOT discard your pre-upgrade reports.** Aggregate counts — above all the
18
+ `not-adjudicable` count on identical prompts — remain the right way to measure what this release did,
19
+ and your archived reports are the only "before" that exists. Pair that with the
20
+ `citationResolved:false` (DROPPED) rate, which is the guard in the other direction: a much larger corpus
21
+ could make the evaluator's citations sloppier, and nothing else would show it.
22
+ - **`skillMdTruncated` is gone from `critique-report.json`**, replaced by `evidenceBudget`
23
+ (`corpusBytes` / `corpusCeiling` / `corpusCuts` / `corpusExcluded` / `trimRecord`). A harvester reading
24
+ the old boolean should read `evidenceBudget.corpusCuts` instead — it is empty on every real skill.
25
+ - **An untracked skill file is no longer graded**, and the two cases behave differently — do not read one
26
+ as the other:
27
+ - **Untracked `references/**` or `agents/<skill>.md`** are simply excluded, named in
28
+ `evidenceBudget.corpusExcluded`, and announced with a `::warning::`. **This does NOT force
29
+ `not-adjudicable`** — and it should not: the agent never received those files either, so the
30
+ evaluator's view now MATCHES the agent's. A finding like "the skill never explains X" against an
31
+ excluded reference is correctly `grounded`; before this release the evaluator could see the file the
32
+ agent never got and mark that true finding `already-covered`.
33
+ - **An untracked `SKILL.md`** previously had its content **graded** (the packager read the host directory
34
+ raw). It now reports `skillMdStatus: "untracked"`, withholds the content, and **does** force the same
35
+ `not-adjudicable` downgrade as an unreadable one — coverage claims cannot be judged with no skill source
36
+ at all.
37
+ Either way, `git add` before critiquing to grade as-published.
38
+
39
+ ### Fixed
40
+
41
+ - **Every `docs/*.md` pointer in the shipped skill was dead for a plugin install.** A plugin install
42
+ materializes only `.claude/skills/<name>/**`; `docs/` ships in the npm tarball and not in that payload.
43
+ The skill referenced `docs/` pages **25 times across 13 targets** anyway, so a consumer following one
44
+ found nothing. This cost real time: `critique-evidence-package.txt` — the corpus the evaluator actually
45
+ graded — was documented only in `docs/critique.md` and named nowhere a plugin user could see, so its
46
+ behaviour got reverse-engineered from compiled `dist/` instead. All 25 are now repo permalinks that
47
+ resolve for tarball and plugin users alike, a trimmed `references/critique.md` ships inside the payload
48
+ covering what a plugin install cannot otherwise reach, and **`npm run check:skill-doc-links` fails CI on
49
+ any new dangling reference** — the durable half, without which the next release re-creates them.
50
+ Reported by a consumer.
51
+ - **`lint-skill --min-severity` exited 2 with no explanation.** The flag belongs to `lint`; the error now
52
+ names the sibling command instead of leaving you to diff two help texts.
53
+ - **`index.jsonl` under-reported a critique's cost by ~39%.** A critique is four model workloads, but only
54
+ the two graded turns produce a run and therefore a row — the two evaluator passes are direct API calls
55
+ that produce neither. Anything summing the index missed them entirely (measured: $10.17 indexed against
56
+ $16.67 actual across three runs), and the index is the only cost record that survives run-dir pruning, so
57
+ spend trends built from it were systematically light. Each critique now writes one roll-up row carrying
58
+ `critiqueTotalUsd`, and `--reindex` reconstructs it from the run dir's `critique-report.json` so a lost
59
+ index recovers critique costs too. The roll-up's own `costUsd` is the **evaluator passes only**, so
60
+ `sum(costUsd)` across all rows is exactly true spend — neither double-counted nor short. Roll-ups are
61
+ excluded from `stats` aggregation, where they would otherwise add a phantom run and inflate `passRate`.
62
+ Reported by a consumer.
63
+
64
+ **Sharing a runs root across CLI versions:** an *older* CLI's `--reindex` deletes these roll-up rows —
65
+ its supersede clause drops any prior row without a `turn`, which a roll-up is. Re-run `--reindex` with a
66
+ current CLI to restore them from the run dirs.
67
+ - **A critique was indistinguishable from a plain `skill` run in the index**, which recorded the *inner*
68
+ command for both turns and carried no `skill` field despite `--skill`. With three concurrent critiques of
69
+ three skills against one plugin, every row read `scenario: skill-<plugin>` and the only way to tell them
70
+ apart was opening each run dir. Rows now carry `critiqueRole` (`task` / `reflection` / `rollup`) and the roll-up carries
71
+ `skill`. These are **additive-optional fields, not a new `command` value**: the row validator hard-codes
72
+ the command allowlist, so widening it would make an older CLI quarantine every critique row and drop it
73
+ from `stats`. The role derives from critique's own session-id shape, so `--reindex` marks rebuilt rows too.
74
+ - **`critique`'s task-turn timeout was 10 minutes**, which a sub-agent-dispatching skill routinely exceeds.
75
+ The turn is killed *after* its model spend, so the consumer paid for a graded run and received an
76
+ instrument failure instead of a critique — a reported case burned $11.05 that way. Raised to 30 minutes,
77
+ matching the evaluator transport; `--timeout` still raises it further, and the byte cap and process-group
78
+ kill remain the real runaway guards.
79
+
80
+
81
+ - **`critique` graded skills against a fraction of their own references.** The evidence package shared one
82
+ 8 KiB budget across ALL `references/**` files, filled in filename-sort order — so the alphabetically
83
+ first file took what it needed and every later file was dropped whole. Measured across nine real runs on
84
+ a six-skill plugin: the same first file was the sole survivor in 9 of 9, and **11 of 13 distinct
85
+ reference files had never reached an evaluator in any run**, including a scoring rubric a sub-agent had
86
+ opened in order to do the scoring. Skill-authored content (SKILL.md, every `references/**` file,
87
+ `agents/<skill>.md`) now ships **whole** — bounded only by a **512 KiB sanity ceiling across all three
88
+ combined**, which no real skill approaches (the largest measured is ~164 KB, about a third of it). A skill
89
+ that does breach it is cut **loudly and by name** in `evidenceBudget.corpusCuts` — never silently, and
90
+ never by refusing the run. The transcript is bounded separately at 128 KiB. Reported by a consumer.
91
+ - **The evidence corpus could contain files the agent never received.** Staging delivers git-tracked files
92
+ only, but the packager read the host directory directly — so an uncommitted reference was absent from
93
+ the mount and present in the evaluator's evidence. An agent saying "the skill never explains X" could be
94
+ marked `already-covered` against a file it was never given. The packager now applies staging's own
95
+ tracked-set filter and reports what it excluded.
96
+ - **`references/**` traversal was neither recursive nor symlink-aware**, so nested and symlinked reference
97
+ files were dropped silently, with no omission marker.
98
+ - **The overall package trim did not converge.** It shaved a section by exactly the overflow and then
99
+ appended a truncation marker, leaving the package marker-length over; it then shaved the next section by
100
+ that amount and re-added the same marker, cascading through every section and exiting still over cap
101
+ with the whole document mangled. It remains a belt-and-suspenders path that should never fire — the
102
+ per-section budgets still sum under the cap, now pinned by a test — which is exactly why it had to be
103
+ correct: nothing exercises it until something else has already gone wrong.
104
+ - **The overall trim destroyed the run record first.** It shaved from the last section backwards, which is
105
+ the transcript — so a breach caused by oversized *skill* content was paid for by deleting *run*
106
+ evidence. Trim priority is now explicit and independent of render order.
107
+ - **`writeSync` short writes and `EAGAIN` were unhandled at every stdout/stderr sink.** `verify-cassettes … | tail`
108
+ died with `EAGAIN` instead of printing its verdict, and a short write on a pipe silently dropped the
109
+ remainder of a JSON envelope. Every `writeSync` output site in `src/` now goes through `writeAllSync`, including critique's own JSON report — the largest single payload the tool emits, and the one its own code comment already flagged as at risk when piped to `jq`. (Sites already using async `process.stderr.write`, and two dev-only sinks under `scripts/`, are unchanged — the failure was specific to the synchronous idiom.)
110
+
111
+ ### Documentation
112
+
113
+ - **`turn` vs `turns` in `index.jsonl` is now explained.** The names invite reading `turn: 2, turns: 1` as
114
+ "turn 2 of 1". They are unrelated: `turn` is the position within a resumed session, `turns` is the count
115
+ of agent turns *inside* that one run, and neither bounds the other. A critique roll-up carries neither,
116
+ since it accounts for workloads that are not runs. Documented rather than renamed — a rename breaks
117
+ `stats` and would need a schema version bump for no behavioural gain. Reported by a consumer.
118
+
119
+ **Cost note.** Sending the whole corpus costs more per critique, not less: roughly **+5% to +18%** on a
120
+ ~$5.60 run, scaling with how much of your skill was previously being cut (a skill whose SKILL.md sat under
121
+ the old 64 KiB cap and had few references sees almost nothing; one that was heavily truncated sees the top
122
+ of the range). Evidence input was only ~14% of an evaluator pass's cost to begin with, which is why the old
123
+ caps were rationing the cheap term — but it is an increase, and a batch budget should carry it.
124
+
125
+ ### Added
126
+
127
+ - **`session.json` now identifies its own run.** The resume manifest (written for `--session-id`/`--resume`
128
+ runs) held only opaque ids, so the file whose name makes it the first thing you open in a run dir
129
+ answered nothing — with several concurrent runs against one plugin there was no way to tell which was
130
+ which without opening each turn's `result.json`. It now also carries `scenario` and `prompt`. These are
131
+ **additive-optional**: nothing validates them, and a manifest written before this still resumes.
132
+ `result.json` remains authoritative for identity; this is a signpost, not a second source of truth.
133
+ Reported by a consumer.
134
+ - **`critique` reports progress.** Four `::notice::` lines on stderr at the phase boundaries (task turn →
135
+ reflection turn → evaluator pass 1 → evaluator pass 2). Previously four model calls over 10–20 minutes
136
+ produced no output at all until the finished report appeared, so a working run and a hung one were
137
+ indistinguishable. stdout remains the machine channel.
138
+ - **`costUsd` now reports the evaluator passes' token split** (`evaluatorPass1Tokens` /
139
+ `evaluatorPass2Tokens`, each `{input, output, cacheRead}`). The transport always handed the harness the
140
+ full usage object and it was summed to a dollar figure and discarded — so the report said what a pass
141
+ COST and never why, and "is this money evidence or thinking?" was unanswerable from any artifact the tool
142
+ produced. That is the question that decides whether sending more evidence is cheap, which makes it the
143
+ number to check first when budgeting a batch under the whole-corpus change above.
144
+ - **`evidenceBudget` in `critique-report.json`** — what the evaluator was actually shown, so the budgets
145
+ are discoverable without reading compiled source. Includes `packageTruncated`, which is what carries the
146
+ transcript's head+tail elision: that elision is what adds the evaluator's truncation caveat, and without
147
+ the flag an elided package was indistinguishable from a clean one (`corpusCuts` is empty in that case and
148
+ would otherwise imply nothing had been cut).
149
+ - **A no-reads signal** (`noSkillFilesRead`). When the graded turn Read no `references/` or `scripts/`
150
+ file at all — neither the main agent nor any sub-agent — the report says so. Worded observationally on
151
+ purpose: the underlying predicate counts the `Read` tool only, so `Grep` or `assets/` use produces an
152
+ empty set having demonstrably reached the material, and calling that "progressive disclosure never
153
+ fired" would be a false accusation about someone's skill.
154
+
155
+ ### Changed
156
+
157
+ - **The overall evidence-package cap is 144 KiB → 768 KiB** (`critique --help` reports it). It stays a
158
+ belt-and-suspenders bound: the per-section budgets deliberately sum under it, pinned by a test.
159
+ - **The transcript bound is 32 KiB → 128 KiB and now keeps head *and* tail** with an elided middle.
160
+ A tail-only cut is the worst shape for a procedural skill, which puts its workflow steps last.
161
+ - **`critique`'s evaluator passes get a 30-minute transport timeout** (was the decider's 600 s default).
162
+ That timeout is enforced with SIGKILL and no retry, so a kill during pass 1 discarded the whole critique
163
+ *after* the graded turns had already been paid for.
164
+
9
165
  ## [1.12.0] — 2026-07-25
10
166
 
11
167
  **Upgrade notes.**
package/CONTRIBUTING.md CHANGED
@@ -27,7 +27,7 @@ Extra prerequisites for specific stages:
27
27
  - **Lima (`limactl`, macOS arm64)** — required only for the **L2 `microvm`** tier and the `vm` commands; the guest runs on Apple Virtualization.framework (`vmType: vz`). `microvm` does **not** use Docker. (`cowork-harness doctor --tier microvm` checks for Lima, not Docker.)
28
28
 
29
29
  CI Stage 1 (the `build` job in `.github/workflows/ci.yml`) does **not** invoke `npm run ci`. It runs the
30
- gate steps individually — e.g. `check:versions`, `format:check`, `typecheck`, `build`, a CLI smoke
30
+ gate steps individually — e.g. `check:versions`, `check:skill-doc-links`, `format:check`, `typecheck`, `build`, a CLI smoke
31
31
  (`node dist/cli.js list`), three token-free `replay` gate fixtures, `verify-cassettes`, `lint`, and
32
32
  source-guard checks — while the unit suite runs separately as the 4-shard `test` job (see `ci.yml` for
33
33
  the authoritative list). Only `release.yml` calls `npm run ci`. For a copy-paste GitHub Actions setup,