Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
f1b6abb
feat(dataset): 1/3 — add Dataset.split_field and the --split row filter
uipreliga Aug 12, 2026
1aee44e
feat(plugin): 2/3 — add the optimize-skill skill and split-label the …
uipreliga Aug 12, 2026
015ee43
docs: 3/3 — add the skill-optimization tutorial, and fix the reachabi…
uipreliga Aug 12, 2026
fdae123
style: apply ruff format to the reachability lint assertion
uipreliga Aug 12, 2026
2dd5e14
docs(harness): record the all-skipped-run-exits-0 gap found while add…
uipreliga Aug 12, 2026
601ed3e
fix: code review fixes for the split-field / optimize-skill plan
uipreliga Aug 12, 2026
4c7481c
feat(plugin): promote a measured `analyze` description, and close the…
uipreliga Aug 12, 2026
276bbda
docs(tutorial): Stage C completed — the analyze promotion is confirme…
uipreliga Aug 12, 2026
6edda25
docs: record the bare-name collision hazard, and mark the plan complete
uipreliga Aug 12, 2026
4e153d4
feat(plugin): add an execution track to optimize-skill — optimize the…
uipreliga Aug 12, 2026
9ac72f6
fix(plugin): repairs from live-testing optimize-skill with cold agents
uipreliga Aug 12, 2026
728c1dc
docs(tutorial): conform 08 to the tutorial front-matter and title con…
uipreliga Aug 13, 2026
2ebc522
refactor(dataset): rename the split values tune/holdout to train/test…
uipreliga Aug 13, 2026
f1a684d
docs(tutorial): use blockquotes, not mkdocs admonitions, in tutorial 08
uipreliga Aug 13, 2026
bdd6815
fix(plugin): 1/8 — correct optimize-skill's execution track and cost …
uipreliga Aug 13, 2026
7092f26
feat(plugin): 2/8 — ship the outcome-suite template for the execution…
uipreliga Aug 13, 2026
ca870df
fix(docs): 3/8 — repair the stale skill counts, and the sensor that m…
uipreliga Aug 13, 2026
12c8237
feat(tasks): 4/8 — add the ci-outcome sample, the execution track's w…
uipreliga Aug 13, 2026
4225e8e
feat(lint): 8/8 — CE035 catches the partly-labelled split dataset
uipreliga Aug 13, 2026
e5aa230
fix(plugin): 5/8 — execution-track A/B returns a NULL result, and cor…
uipreliga Aug 13, 2026
2bed770
docs(tutorial): 6/8 — add tutorial 09, the content track, reported as…
uipreliga Aug 13, 2026
e3444c7
docs(tutorial): 7/8 — make tutorial 08's A/B reproducible, and untang…
uipreliga Aug 13, 2026
6dedff6
fix: code review fixes for the optimize-skill plan
uipreliga Aug 13, 2026
4781a2a
test(lint): CE036 — a row's prompt must not contain what its criteria…
uipreliga Aug 13, 2026
278e4a6
docs(harness): defer three gaps found reviewing the optimize-skill plan
uipreliga Aug 13, 2026
59019dd
fix(criteria): an errored Skill call is not engagement
uipreliga Aug 13, 2026
5f5ad04
docs: correct tutorial 09 — the round's null had a different cause th…
uipreliga Aug 13, 2026
635c0ae
docs(tutorial): 09 gets the real baseline — a verified ceiling, 1.000…
uipreliga Aug 13, 2026
67eaf02
fix(tasks): gate engagement, in the artifacts that call it a gate
uipreliga Aug 13, 2026
4dd6e69
docs(tasks): make the ci-outcome sample runnable in two lines, and re…
uipreliga Aug 13, 2026
ad6bb13
chore: close the last loose ends from the optimize-skill plan
uipreliga Aug 13, 2026
04a999a
docs: the task guide documented the old skill_triggered detection rule
uipreliga Aug 13, 2026
2355d0f
Merge remote-tracking branch 'origin/main' into feat/plugin-optimize-…
uipreliga Aug 13, 2026
e16fb38
fix(criteria): 1/8 — skill_triggered's live and final verdicts agree …
uipreliga Aug 14, 2026
2e30c68
fix(dataset): 2/8 — a --split that matches nothing aborts instead of …
uipreliga Aug 14, 2026
a6b641d
feat(plugin): 3/8 — the activation template caps and isolates, as its…
uipreliga Aug 14, 2026
8f56940
test(lint): 4/8 — CE036 gets unit fixtures, and exempts skill_name
uipreliga Aug 14, 2026
ffafed8
fix(dataset): 5/8 — validate every row id, not just the selected ones
uipreliga Aug 14, 2026
75e4c20
feat(cli): 6/8 — plan expands datasets and takes --split
uipreliga Aug 14, 2026
59f7556
docs: 7/8 — state the method's caveats, and make tutorial 08's number…
uipreliga Aug 14, 2026
d1973a9
docs(plugin): 8/8 — extract the optimize method, and fix three seams
uipreliga Aug 14, 2026
c31bd80
docs(plugin): the cost table's Stage B row priced only the activation…
uipreliga Aug 14, 2026
70d043c
docs(tutorials): audit all nine, and measure what tutorial 08 could o…
uipreliga Aug 14, 2026
c553893
docs(tutorials): name 08 for what it optimizes — a skill's description
uipreliga Aug 14, 2026
6659c81
docs(tutorials): re-run Part 2's Stage A, and let it stop at Stage A
uipreliga Aug 14, 2026
b306a99
feat(stats): 1/9 — one F1 implementation, and the gate's two primitives
uipreliga Aug 14, 2026
3fc36ba
feat(optimize): 2/9 — the activation gate, as a library the skill drives
uipreliga Aug 14, 2026
0329258
feat(optimize): 3/9 — price the noise floor, and guard cost and latency
uipreliga Aug 14, 2026
62f13d0
docs(plugin): 4/9 — the skill's gate is the one the tool computes
uipreliga Aug 14, 2026
1d88ca2
docs(plugin): 5/9 — the proposal is a shape, not an improvisation
uipreliga Aug 14, 2026
f347d8c
feat(optimize): 6/9 — measurements get a schema, the ledger keeps its…
uipreliga Aug 14, 2026
e85207b
docs(plugin): 7/9 — establish the body does anything before optimizin…
uipreliga Aug 14, 2026
e0e0631
feat(optimize): 8/9 — the shape of a round, not just its average
uipreliga Aug 14, 2026
82ba733
docs(plugin): 9/9 — halving, priced honestly
uipreliga Aug 14, 2026
45f7307
fix: code review fixes for the optimize-skill gate corrections
uipreliga Aug 14, 2026
e4d6e2f
test(lint): CE038 — extra="forbid" must reach the models it appears t…
uipreliga Aug 14, 2026
5142cd5
fix(stats): 1/6 — the bootstrap p is (b+1)/(m+1), not a clamped count
uipreliga Aug 14, 2026
61e35ef
feat(optimize): 2/6 — a gate that refuses rather than overclaiming
uipreliga Aug 14, 2026
e6f6383
test(lint): 3/6 — CE039, prose claims checked by computing them
uipreliga Aug 14, 2026
a4e9f00
feat(optimize): 4/6 — the execution track gets a floor, on its own me…
uipreliga Aug 14, 2026
86274ad
feat(optimize): 5/6 — the merge shortlist is a different set from the…
uipreliga Aug 14, 2026
1260425
feat(optimize): 6/6 — cost as a second axis of the shortlist, never a…
uipreliga Aug 14, 2026
9089fa8
test(lint): CE040 — the bootstrap's p-floor is derived in one place
uipreliga Aug 14, 2026
7f2a18c
fix(plugin): the control-arm command needs a file Step 9 authors
uipreliga Aug 14, 2026
12eee85
fix: cross-phase review findings for the optimize-gate plan
uipreliga Aug 14, 2026
884f1a3
fix(optimize): cost-front coverage is a set test, not a count
uipreliga Aug 14, 2026
d2742e0
docs(optimize): 1/6 — the sentence CE039 cannot see
uipreliga Aug 14, 2026
05f4e04
feat(optimize): 2/6 — a refusal whose remedy is true
uipreliga Aug 14, 2026
2abcc08
docs(optimize): 3/6 — the sizing rule, before the money is spent
uipreliga Aug 14, 2026
5b9116e
fix(optimize): review findings on 2/6 — two false claims about the lever
uipreliga Aug 14, 2026
a536841
feat(optimize): 4/6 — the regression corpus gets a reader
uipreliga Aug 14, 2026
00d014e
feat(optimize): 5/6 — the execution track gets a computed, corrected …
uipreliga Aug 14, 2026
96cfb47
feat(optimize): 6/6 — the sibling guardrail stops being opt-in
uipreliga Aug 14, 2026
271c170
docs(harness): two deferred gaps from the optimize-gate review
uipreliga Aug 14, 2026
aa8fabb
fix: code review fixes for the optimize-gate v8/v2/v3/v5/v1/v4/v6 plan
uipreliga Aug 14, 2026
a534e95
docs(harness): two more deferred gaps from the optimize-gate final re…
uipreliga Aug 14, 2026
9db6326
feat(optimize): 1/3 — a search loop that bounds nothing, and two poin…
uipreliga Aug 14, 2026
a88310a
feat(optimize): 2/3 — an anti-memorization check that does not cry wolf
uipreliga Aug 14, 2026
f30a323
feat(optimize): 3/3 — the gold solution, and failures with names
uipreliga Aug 14, 2026
da5dd71
fix: code review fixes for the ReAPO optimize-skill plan
uipreliga Aug 14, 2026
763d959
docs(harness): two deferred gaps from the ReAPO optimize-skill review
uipreliga Aug 14, 2026
608e49e
docs(harness): the search loop's arithmetic belongs in a tested function
uipreliga Aug 14, 2026
c7e395c
feat(optimize): the search loop's decision becomes tested code
uipreliga Aug 14, 2026
e79f525
docs: bring the optimize surfaces back in line with the gate
uipreliga Aug 14, 2026
bb2a254
feat(optimize): 1/4 — a zero-variance execution verdict is not a result
uipreliga Aug 15, 2026
12f738a
feat(optimize): 2/4 — the execution gate fails closed on a degenerate…
uipreliga Aug 15, 2026
4197f64
feat(optimize): 3/4 — a mistyped verdict field raises instead of vani…
uipreliga Aug 15, 2026
622023c
feat(optimize): 4/4 — the coverage front learns the guard its sibling…
uipreliga Aug 15, 2026
b6198fb
fix: code review fixes for the optimize-gate promotion-correctness plan
uipreliga Aug 15, 2026
40f71f9
test(optimize): a fixture that builds twice under one tmp_path now fa…
uipreliga Aug 15, 2026
e2e3fbe
fix(optimize): close the three gaps this plan deferred
uipreliga Aug 15, 2026
df6f88a
test(optimize): 1/6 — pin the six rendered blocks before anything moves
uipreliga Aug 15, 2026
2de420d
feat(optimize): 2/6 — the replicate-padding seam, one glob, and CE042
uipreliga Aug 15, 2026
045c5a4
feat(optimize): 3/6 — one declaration for the trim and the shared Hol…
uipreliga Aug 15, 2026
6486d0c
feat(optimize): 4/6 — extract the worst complexity blocks
uipreliga Aug 15, 2026
8ea3847
feat(optimize): 5/6 — SearchComparison.accepted becomes a derived pro…
uipreliga Aug 15, 2026
b2dbc93
feat(optimize): 6/6 — split the presentation and sidecar halves out o…
uipreliga Aug 15, 2026
880a81f
docs(harness): two deferred guardrails from the optimize-gate module …
uipreliga Aug 15, 2026
33f103e
test(cli): 1/6 — the row selectors get a test that goes through the CLI
uipreliga Aug 16, 2026
d5301a6
feat(models): 2/6 — one RowSelection, recorded in run.json and rendered
uipreliga Aug 16, 2026
f24ef26
feat(cli): 3/6 — plan previews what run actually executes
uipreliga Aug 16, 2026
dce0621
feat(optimize): 4/6 — the gate stops pairing a train run against a te…
uipreliga Aug 16, 2026
07679ce
feat(criteria): 5/6 — one label constant, and both activation suites …
uipreliga Aug 16, 2026
ecbb5d0
docs(ci): 6/6 — the ci skill learns about splits, and the docs catch up
uipreliga Aug 16, 2026
dd89061
test(lint): CE038 reaches BatchRunConfig, the container that motivate…
uipreliga Aug 16, 2026
941944b
docs(harness): the fingerprint-diff blind spot a moved config key opens
uipreliga Aug 16, 2026
a355d30
fix: cross-phase review findings for the row-selection plan
uipreliga Aug 16, 2026
3f9af81
docs(harness): the sampler half of the cross-split refusal
uipreliga Aug 16, 2026
3b5d18a
test(lint): 1/6 — CE044 and CE045, the two sensors that could not see
uipreliga Aug 16, 2026
76cca36
test(lint): 2/6 — CE046 and CE047, the CLI's two undocumented surfaces
uipreliga Aug 16, 2026
4bec17f
refactor(models): 3/6 — copy_with closes the update-key hole, CE048 h…
uipreliga Aug 16, 2026
aef73b3
test(optimize): 4/6 — three prose claims that were not true
uipreliga Aug 16, 2026
4076e0d
test(lint): 5/6 — the method file's sign sentence is bound to the gate
uipreliga Aug 16, 2026
ad3149c
ci(stats): 6/6 — a rendered statistic that steps must say why
uipreliga Aug 16, 2026
8098bc9
fix: cross-phase review findings for the harness-sensors plan
uipreliga Aug 16, 2026
fa69cdf
test(lint): guard CE id collisions the runner's assert cannot see
uipreliga Aug 16, 2026
555841a
fix(early-stop): 1/9 — the decide_within budget stops expiring mid-call
uipreliga Aug 16, 2026
5f9179d
feat(optimize): 2/9 + 3/9 — the index gets a lower bound, the guardra…
uipreliga Aug 16, 2026
1cababb
feat(optimize): 4/9 — the gate stops trusting a run.json about a tree…
uipreliga Aug 16, 2026
1d48914
feat(cli): 5/9 — untrusted text stops being read as Rich markup, and …
uipreliga Aug 16, 2026
bc29b77
test(lint): 6/9 + 7/9 — four import rules stop failing open, and C90 …
uipreliga Aug 16, 2026
6967316
test(lint): 8/9 — the ci-outcome fixture loads, and CE052 makes sure …
uipreliga Aug 16, 2026
0788a1b
feat(orchestration): 9/9 — BatchRunConfig accepts the flat row select…
uipreliga Aug 16, 2026
cb7e10b
docs(harness): two deferred guards the top-10 review-fixes run surfaced
uipreliga Aug 16, 2026
83bbe2b
fix: cross-phase review findings for the top-10 review-fixes plan
uipreliga Aug 16, 2026
2bf6edf
docs(stats): the ledger row also accounts for the pins' re-encoding
uipreliga Aug 16, 2026
95122a2
fix: adversarial review findings — two guards that did not guard
uipreliga Aug 16, 2026
5b27242
docs(optimize-skill): findings from the xlsx execution-track dogfood run
uipreliga Aug 17, 2026
265299e
fix(criteria): 1/3 — allowlist parity for skill_triggered's file-read…
uipreliga Aug 17, 2026
fb6c470
fix(criteria): 2/3 — reject an off-kind agent_judge judge config at load
uipreliga Aug 17, 2026
17e35c1
feat(criteria): 3/3 — name the non-engagement reason in skill_trigger…
uipreliga Aug 17, 2026
96c0b42
fix: code review fixes for the Plan A scoring-correctness fixes
uipreliga Aug 17, 2026
b5cae81
feat(lint): CE054 — a criterion compares result_status in exactly one…
uipreliga Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
446 changes: 446 additions & 0 deletions .claude/harness-candidates.md

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions .claude/shared/run-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,82 @@ runs/<run_id>/<variant_id>/<task_id>/<NN>/{task.json, task.log, artifacts/}
- `task.json.malformed` — present only on the docker degrade path: when an existing `task.json` fails to parse (schema skew from a stale `:latest` image, or a truncated/torn write), the docker runner moves the unparseable original aside to this sidecar and writes a synthetic `final_status=ERROR` `task.json` in its place. Diagnostic-only; `rglob("task.json")` consumers do not match it.
- `task.log` — the human-readable task log; `artifacts/` — files the agent produced.

## Suite rollups (dataset-backed tasks only)

A task carrying `dataset:` fans out into one row-task per row and additionally writes a
per-suite rollup:

```
runs/<run_id>/<variant_id>/<suite_id>/{suite.json, suite.md}
```

`<suite_id>` is the original (pre-fan-out) `task_id`. Nothing is written for a task
without `dataset:`.

`suite.json` carries the suite's pass counts plus `criterion_aggregates[]` — one entry per
criterion that opted into across-row aggregation, each with:

- `criterion_type`, and `description` (set when a task stacks several criteria of the same
type, e.g. one `skill_triggered` per skill — that is what distinguishes them);
- `rows_total` and `rows_excluded` — the denominator and what was dropped from it. A row
that errored before criteria ran (a timeout, say) is **excluded** rather than scored, so
metrics are computed over `rows_total - rows_excluded`;
- `metrics` — a **flat** name → float map. Classification-style criteria emit
`accuracy`, `macro_f1`, and per-label `precision.<label>` / `recall.<label>` /
`f1.<label>` (for `skill_triggered` the labels are `yes` / `no`). It also carries
`completion_rate` — the surviving fraction of the denominator above — which is an
ordinary metric, so `suite_thresholds: {completion_rate: 1.0}` gates a run whose sample
eroded;
- `details` — `labels`, `per_label`, `confusion`, `total_pairs`;
- `threshold_checks` + `passed`, from the criterion's `suite_thresholds`.

Alongside the aggregates, `failed_samples[]` lists failed/errored rows **by `row_id`** with
their failure reasons, capped at a fixed limit. It is the only place in `suite.json` that
carries row identity — `metrics` and `details` are counts only — so a consumer that needs
to know *which* rows failed reads `failed_samples`, then falls back to the per-row
`<variant_id>/<suite_id>/<row_id>/<NN>/task.json` for anything past the cap.

**Read `metrics`; never recompute a metric from `details.confusion`.** The criterion layer
owns that arithmetic, including its division-by-zero convention, and a consumer that
re-derives F1 will disagree with the gate the run already applied.

**Rollups pool replicates.** The grouping key is `(variant_id, suite_id)` — the replicate
index is *not* part of it. So `--repeats N` over an M-row suite writes **one** `suite.json`
per variant, with `rows_total: N × M` and a single pooled confusion matrix. There is no
per-replicate metric in that file. A consumer that needs replicate-to-replicate spread must
invoke the run N times and read N run directories; `--repeats` cannot serve that purpose.
(Contrast `experiment.md`'s `## Paired Comparison` block, which averages replicates per row
before pairing — there `--repeats` is exactly the right tool.)

**Row-selection provenance.** `run.json` carries `row_selection` — the `split` / `max_rows` /
`sample_per_stratum` this run executed under (`--split` / `--sample` / `--sample-per-stratum`).
Its **absence** means the run predates the field, which is deliberately NOT the same as a
recorded `{"split": null, ...}`: the first says nothing about what was selected, the second
says no selector was passed on the command line.

It records what was **requested on the CLI**, not the effective row set. A task's own
`dataset.sample_per_stratum` still narrows the run without appearing here — and that draw is
re-drawn every invocation unless `dataset.sample_seed` is pinned — so a recorded all-`null`
selection is *not* a promise that every row ran. Read the task YAML for that half.

A consumer comparing two runs must not pair different `split` values — a train run and a test
run of one suite are two different row sets, and reporting their difference as one measurement
is the failure this field exists to prevent. `optimize_gate.activation_gate` enforces exactly
that: it REFUSES a pair whose arms recorded different splits, and NOTES an unrecorded one rather
than assuming it matched. The refusal leaves `promoted` at `null` like every other activation
verdict — `holm_promote` is what forces it to `false` and what makes the block headline
`NOT A RESULT`; before that the headline is `UNDECIDED` and the reason prints on its own line. `measure_noise_floor` refuses to
measure across them at all, and a floor measured over any unrecorded run dir is never cached.

One caveat on `--resume`: the field records the selection of the invocation that WROTE the
summary, so a resumed run stamps its own selectors over results produced by an earlier one. The
run-config fingerprint warns about that drift separately; the provenance field itself does not
distinguish the two.

Scope: only `split` is compared by the gate. `max_rows` / `sample_per_stratum` are recorded but
not gate-checked — a mismatch there shows up as a small `rows_paired` beside a large
`rows_excluded` rather than as a refusal.

**Scope-marker files** (used to detect what a given path represents):

- `run.json` at the run root → **run scope**. If `experiment.json` (+ `experiment.md`) is also present → multi-variant experiment.
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,37 @@ jobs:
working-directory: evalboard
run: pnpm verify

estimator-protocol:
# A rendered statistic can step for IDENTICAL data when an estimator or a resample
# count changes, and nothing in a run artifact distinguishes that from a real change
# in the thing being measured. So a PR touching a watched statistical constant or a
# rendered-number snapshot fixture must add a row to `## Estimator changes` in
# docs/REPORT_SCHEMA.md. Diff-based, so it cannot live in `make verify` — a working
# tree has no base ref — and `pull_request`-only, because without a base ref
# $GITHUB_BASE_REF is empty and the fetch below would turn every push red for the
# wrong reason. Installs nothing: the checker is stdlib-only by construction.
name: Estimator Change Protocol (statistical constants ↔ ledger)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Set up Python 3.13
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"

# No `git fetch`: `fetch-depth: 0` already populates refs/remotes/origin/*, and re-fetching
# would advance origin/BASE past the merge commit this run checked out — which is how a
# racing PR's ledger row gets credited to this one. The checker resolves the merge base
# itself, so both sides of the row count share one baseline.
- name: Check the estimator ledger
run: python -m tests.lint.estimator_ledger "origin/$GITHUB_BASE_REF"

plugin-validate:
# Proves the Claude Code plugin marketplace is installable and that the suite
# `check-skill` scaffolds is real: the manifests pass strict validation, and the
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,8 @@ refs/

# SkillsBench tasks for testing
/resources/

# /coder-eval:optimize-skill working tree: candidate skill snapshots,
# per-stage experiment files and the round ledger. A snapshot is a full plugin
# root copied per arm, so a round writes several megabytes of duplicated skills.
.optimize-skill/
20 changes: 14 additions & 6 deletions CLAUDE.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ jobs that execute PR-supplied code (`quality-gate`, `no-uipath-extra`, `evalboar
fall back to it **for fork PRs only**, since this repo is public and untrusted code
should not run on the shared pool image — any new job running PR-supplied code needs
the same carve-out. `action-dogfood` always uses it, because it is the executable proof
behind the published Action and must exercise the image integrators actually use.
behind the published Action and must exercise the image integrators actually use, and
`plugin-validate` and `estimator-protocol` always use it too — the safe
direction, taken unconditionally rather than only on fork PRs. (`estimator-protocol`
installs nothing at all; `plugin-validate` installs the Claude Code CLI and the
package, and still takes the stock image rather than the shared pool.)

## Adding Tasks or Criteria

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ results — runs inside the agent:
/plugin install coder-eval@coder-eval
```

That adds six slash commands: `/coder-eval:init`, `/coder-eval:check-skill`,
`/coder-eval:task`, `/coder-eval:lint-tasks`, `/coder-eval:analyze` and
`/coder-eval:ci`. They drive the `coder-eval` CLI, so install it too
That adds seven slash commands: `/coder-eval:init`, `/coder-eval:check-skill`,
`/coder-eval:optimize-skill`, `/coder-eval:task`, `/coder-eval:lint-tasks`,
`/coder-eval:analyze` and `/coder-eval:ci`. They drive the `coder-eval` CLI, so install it too
(`uv tool install coder-eval`). See [Claude Code Plugin](docs/PLUGIN.md).

## Use as a GitHub Action
Expand Down
1 change: 1 addition & 0 deletions docs/AB_EXPERIMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ if any listed metric is below its minimum.
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `-e, --experiment <path\|name>` | Experiment YAML. Bare name → `experiments/<name>.yaml`. |
| `--sample N` | For dataset-backed tasks, use a fixed-seed random N-row sample (reproducible, unbiased across paths; cheap smoke test). |
| `--split NAME` | For dataset-backed tasks, keep only rows whose `dataset.split_field` value matches (e.g. `train` / `test`). Applied before `--sample`. Unlabelled tasks unaffected. |
| `--repeats N` | Run each `(task, variant)` N times; overrides YAML `repeats`. |
| `--driver tempdir\|docker` | Override sandbox driver for all tasks. |
| `-j, --max-parallel N` | Run up to N tasks concurrently. |
Expand Down
85 changes: 80 additions & 5 deletions docs/DATASETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,52 @@ Load-time errors, with their message shapes:
| Two rows share an id | `Duplicate dataset row id for task '<task_id>': '<x>'` |
| Both/neither `rows` and `paths` | `Dataset must specify either 'paths' or 'rows'` / `... only one of ...` |
| `paths: []` | `Dataset.paths must be a non-empty list` |

## Sampling a subset

A full dataset is expensive. Two independent mechanisms cut it down, and **`--sample` wins whenever
both apply**:
| `--split X` on a labelled dataset with no `X` rows | `Dataset for task '<task_id>' has no rows in split 'X' (split_field='split'); labelled splits present: ['test', 'train']` |

**Row-id validation runs over the WHOLE dataset, before `--split` and before either sampler.** So
a malformed, id-less or duplicate row sitting in a split you did not select still fails the load.
That is intended, not an oversight: "the dataset is well-formed" must not depend on what a given
invocation happened to select. Split the checks — duplicates whole-set, malformed per-selected-row
— and a bad row in the `test` split validates cleanly under every `--split train` run and surfaces
only at promotion time, which is the most expensive moment to learn about it. The `Dataset row <i>`
index therefore counts over the whole file, which is also the more useful number: it points at the
line.

## Selecting a subset

A full dataset is expensive. Three mechanisms cut it down, in two stages: **`--split` filters
first**, then **one** of the two samplers runs over what survives, with **`--sample` winning
whenever both samplers apply**.

**Stage 1 — the filter.** `--split` is not in that win-order; it is orthogonal, and always applies
first.

**`--split <name>` (CLI only)** — keep only rows whose `dataset.split_field` value (default field:
`split`) equals `<name>`. Exact string match, no case normalization; non-string values compare via
`str()`.

Three behaviours are worth knowing before you rely on it:

- **A task whose rows carry no split label at all passes through unfiltered.** `--split` is global to
the invocation, so an unlabelled dataset sitting beside a labelled one in the same run must not
fail. A row counts as unlabelled when the field is absent, `null`, or `""`.
- **Partial labelling drops the unlabelled rows.** If only some rows carry a label, `--split train`
keeps just the `train` rows — the unlabelled ones are excluded rather than folded in. That is the
safe direction (an unlabelled row never leaks into a named split), but during an incremental
migration it silently *shrinks* the suite, which moves the aggregate metrics `suite_thresholds`
gates on. Finish labelling before you compare two runs.
- **A labelled task with no row in the requested split aborts the run.** Expansion raises
`SplitSelectorError`, naming the splits that do exist, and — unlike every other dataset error —
`resolve_all_tasks` re-raises it rather than recording a `skipped_tasks` entry. So a mistyped
selector (`--split holdou`) fails loudly with a non-zero exit instead of producing a green run of
zero tasks. The distinction is deliberate: the other errors describe a malformed *file*, and one
bad task must not abort a suite; this one describes a malformed *invocation*, and the same
selector applies to every task in the run.
- **A partly labelled dataset logs a WARNING** naming how many rows were dropped, because that run
is legitimate but is measuring a smaller suite than the file suggests. `coder-eval plan --split
<name>` shows the same thing before you spend anything.

**Stage 2 — the samplers**, over whatever survived the filter:

1. **`--sample N` (CLI only)** — a flat uniform-random N rows over the whole dataset. Fixed seed, so
the same N rows come back every run: a reproducible, cheap smoke flavor of a big suite. Unlike a
Expand All @@ -155,6 +196,40 @@ both apply**:
coverage over time matters more than run-to-run comparability. Note the contrast with
`--sample N`, which is fixed-seed and reproducible by default.

### Train and test splits

Label each row with a split and you can develop against one half and confirm on the other, which is
what keeps a measured improvement from being an artifact of the rows you trained on:

```jsonl
{"id": "pos-1", "prompt": "review my task files", "expected_skill": "lint-tasks", "split": "train"}
{"id": "pos-2", "prompt": "are my evals any good?", "expected_skill": "lint-tasks", "split": "test"}
```

```bash
coder-eval run tasks/skills/activation.yaml --split train # iterate here
coder-eval run tasks/skills/activation.yaml --split test # confirm here, once
```

`coder-eval plan` takes the same three selectors, so the exact invocation you are about to pay for
can be previewed for free — it prints the selected row count, names which selector narrowed the
set, and breaks the selection down per stratum:

```bash
coder-eval plan tasks/skills/activation.yaml --split train --sample-per-stratum 2
```

**The filter runs before either sampler, and that ordering is load-bearing.** Sampling first would
leave an unpredictable — possibly zero — number of rows per split, so the two arms of the comparison
would no longer be the same size or the same rows. Filter-then-sample means `--split train --sample 8`
is always drawn from the train rows alone — at most eight of them, and all of them if `train` holds
fewer than eight.

Two consequences worth planning for. A split **halves each side of the suite**, so a dataset sized
for a single one-shot measurement is undersized once split — budget roughly double the rows you
would otherwise want. And a test split is only worth what its independence buys: consult it to confirm
a decision already made on `train`, not to choose between candidates, or it becomes a second train set.

## Suite-level scoring

Per-row pass/fail is rarely the number you care about on a dataset — the suite metric is. Every
Expand Down
Loading