diff --git a/.claude/harness-candidates.md b/.claude/harness-candidates.md index e067e9ad..4d9e58e4 100644 --- a/.claude/harness-candidates.md +++ b/.claude/harness-candidates.md @@ -455,3 +455,18 @@ with the two `action.yml` items above — one considered change to the action's `final_status`, which does not exist in `run.json` and would have made a new assertion dead on arrival. Guard: assert the key set that non-Python consumers depend on, mirroring how CE030 pins doc/schema parity. +## From the split-field / optimize-skill plan (2026-08-12) + +- [ ] **A run whose every task is skipped exits 0 — a green run of zero tasks.** When + `resolve_all_tasks` demotes every task to `skipped_tasks` (a load failure, `skip: true`, + or now a `--split` selector matching no labelled row), the run reports success: nothing + failed, so the exit gate in `cli/run_command.py` — which keys only on failed/errored tasks + and suite gates — passes. Verified directly: `coder-eval run --split holdou` + prints one yellow "1 task file(s) skipped" line and exits 0. This is pre-existing, but + `--split` makes it reachable by a one-character CLI typo rather than a broken file, and + the whole point of a holdout confirmation is that you trust its verdict. Not guarded, and + not a five-minute fix: making an all-skipped run non-green changes exit semantics for + every skipped-task path (including deliberate `skip: true` suites and tag filters that + match nothing), so it needs a decision about which of those should be fatal, plus tests + per case. A narrower option is to fail only when a CLI *selector* (`--split`, `--tags`) + eliminated everything, since that is unambiguously a user error rather than repo state. diff --git a/.claude/shared/run-layout.md b/.claude/shared/run-layout.md index 57edc4a6..7e62e707 100644 --- a/.claude/shared/run-layout.md +++ b/.claude/shared/run-layout.md @@ -14,6 +14,53 @@ runs/////{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////{suite.json, suite.md} +``` + +`` 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.