Repository navigation
feat(repo): judged recall over the credited replay hits - #357
Conversation
Settles intel#291: dokimi:review_replay credits a human thread to the nearest finding within the window by location alone, so a finding that only sits near a thread gets the credit. `repo hits judge <batch> --judge PROVIDER/MODEL` is an optional, paid step after grading. It takes each credited pair from load_hits, reads the thread's full comment from the task's gold file under <batch>/_eval/content and the finding's full text from the cell's answer.txt at index `finding` (matches cuts both to 200 characters), and asks the judge, through dokimi.judged.judged_reply, for `same point`, `related` or `unrelated`. Each fully judged cell gets hits_judged.json (repo_hits_judged.v1) with the labels, their counts and recall_judged, the `same point` pairs over n_threads, so recall_judged <= recall. grade.json is not touched. Before any call it refuses (exit 2) a judge that runs any answer model of the batch, using novelty's same_client rule (a CLI alias counts as its family's full id), and a --labels entry naming no credited pair or holding a label other than the three. A reply that never parses, or a pair whose full text is not found, is unjudged. --dry-run prices the pairs at list price; --max-usd starts no new call once spent, and a cell left unjudged gets no file (exit 1). `repo hits sheet <batch> --out FILE` samples pairs for a person, 25 per arm by default with random.Random(seed); with that file, --labels gives agreement and Cohen's kappa per arm, and without it the output says recall_judged is uncalibrated. load_hits also returns the replay cells whose matches were read and the grader-failure count, so a cell with no credited pair counts in the means. Reading recall_judged in repo compare is left to a later change. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
CLI interface change detectedThis PR changes dokimi's user-facing CLI surface (commands and/or options): + added command `dokimi repo hits`
+ option `--help/-h`
+ added command `dokimi repo hits judge`
+ argument `batch_dir`
+ option `--dry-run`
+ option `--help/-h`
+ option `--json`
+ option `--judge`
+ option `--labels`
+ option `--max-usd`
+ added command `dokimi repo hits sheet`
+ argument `batch_dir`
+ option `--help/-h`
+ option `--out`
+ option `--per-arm`
+ option `--seed`
~ `dokimi runs list` parameters changed:
~ changed option `--kind`: choices: ["'answers'", "'arms'", "'cli_routes'", "'effective_instructions'", "'eval'", "'eval_coverage'", "'explore_runs'", "'guidance_reach'", "'headroom'", "'matrix_rollup'", "'need_graph'", "'novelty'", "'plan'", "'plugin_delta'", "'questions'", "'redteam'", "'repo_budget'", "'repo_environment'", "'repo_readiness'", "'result'", "'skill_evidence'", "'subject_scorecard'", "'task_inspect'", "'task_runs'", "'tool_preference'"] -> ["'answers'", "'arms'", "'cli_routes'", "'effective_instructions'", "'eval'", "'eval_coverage'", "'explore_runs'", "'guidance_reach'", "'headroom'", "'matrix_rollup'", "'need_graph'", "'novelty'", "'plan'", "'plugin_delta'", "'questions'", "'redteam'", "'repo_budget'", "'repo_environment'", "'repo_hits_judged'", "'repo_readiness'", "'result'", "'skill_evidence'", "'subject_scorecard'", "'task_inspect'", "'task_runs'", "'tool_preference'"]Updated on every push; the |
There was a problem hiding this comment.
🟡 Changes recommended
The judge exclusion check ignores resolved models, allowing self-judging, while calibration output can suppress warnings despite having no jointly labelled pairs.
2 open issues
What changed in this PR
2 new findings.
This PR adds optional judged recall to replay evaluation, checking whether location-credited findings actually match human review threads without changing deterministic grading.
Changes:
- Adds
repo hits judgewith full-text pairing, budget controls, cost estimates, and per-cell artifacts. - Adds sampled human-label sheets and agreement statistics for calibration.
- Registers the artifact and CLI commands, with offline tests and documentation;
repo compareintegration remains deferred.
| File | Description |
|---|---|
dokimi/tests/test_runs_show.py |
Adds judged-hit artifact display expectations. |
dokimi/tests/test_runs_scan.py |
Adds artifact discovery expectations. |
dokimi/tests/test_repo_hits_judge.py |
Tests judging, calibration, budgets, caching, and input validation. |
dokimi/tests/test_cli_routes.py |
Checks the paid judging route. |
dokimi/tests/fixtures/artifacts/repo_hits_judged.v1.json |
Provides a judged-hit artifact fixture. |
dokimi/tests/data/cli_flags_allowlist.txt |
Allows calibration-specific flags. |
dokimi/dokimi/schemas/repo_hits_judged.v1.json |
Defines the judged-hit artifact format. |
dokimi/dokimi/runs/kinds.py |
Registers artifact display fields. |
dokimi/dokimi/repos/report.py |
Exposes replay cells and grader-failure counts. |
dokimi/dokimi/repos/hits_judge.py |
Implements text recovery, judging, artifacts, and summaries. |
dokimi/dokimi/repos/hits_calibration.py |
Implements sampling and agreement statistics. |
dokimi/dokimi/commands/repo_run.py |
Registers the hits subcommands. |
dokimi/dokimi/commands/repo_hits.py |
Adds judge and sheet command handlers. |
dokimi/dokimi/cli_routes.py |
Classifies judging as paid and sampling as free. |
dokimi/dokimi/artifacts.py |
Registers the new artifact schema. |
dokimi/docs/repo-evaluation-contract.md |
Documents judged recall, calibration, and command behavior. |
dokimi/docs/reference/cli-interface.json |
Updates the generated CLI reference. |
dokimi/docs/architecture.md |
Documents the new replay-judging modules. |
dokimi/changelog.d/20261101_120000_antonwolfy_replay-judged-recall.md |
Announces judged recall and calibration commands. |
Review effort: Balanced
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 20 selected item(s). This PR has reached its review limit, so only a re-request starts a review. Review details
|
Settles the review comments on intel#357. The judge-independence check read only the `model` a cell was asked of. A cell asked of one model and answered by another (the runner's `model_resolved`, from the trace, in both result.json and meta.json) accepted the answering model as its judge. answer_models now adds the resolved models of both records through report.resolved_models (a string or a list) next to the requested names, so such a judge exits 2 before any call. With --labels, an unfilled sheet, an empty list, or labels only on pairs the judge left unjudged gave a calibration with no jointly labelled pair, which dropped the uncalibrated line and printed an empty calibration heading. No pair labelled by both now reads as uncalibrated, with the reason; when only some arms have such pairs, a line names the others. The JSON summary gains `uncalibrated_arms`, the arms of `rows` with no jointly labelled pair (every arm without --labels); `calibration` is unchanged. read_labels matched an entry by cell and thread and ignored its `finding`, so a hand-edited entry, or a sheet older than a regrade, was compared against a pair the person did not label. An entry whose `finding` is missing or is not the finding the batch credits for its thread is now refused with exit 2 before any judge call, pointing at `repo hits sheet`. The contract's §4.5 and §9 say the same. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #357 +/- ##
==========================================
- Coverage 94.40% 94.39% -0.02%
==========================================
Files 313 316 +3
Lines 40240 40842 +602
Branches 6828 6902 +74
==========================================
+ Hits 37990 38551 +561
- Misses 1435 1462 +27
- Partials 815 829 +14
🚀 New features to boost your workflow:
|
Settles the review point on intel#357 that an OSError from writing a cell's hits_judged.json, or from removing an earlier one when --max-usd stops that cell, escaped `repo hits judge` as a traceback with exit 1, while `repo hits sheet` reports an unwritable output with exit 2. judge_cells now stops at the first such failure, before any further judge call, and raises HitsWriteError with the path, the OSError and what it had done, so a Python caller gets a typed error. The command turns it into one line on stderr naming the path and the error, the judge calls made and their cost, and that the cells written before keep their files, and exits 2. The contract's section 9 says so. Tests: a directory where a cell's file goes, and an earlier file that cannot be removed when the budget stops its cell; each exits 2 naming the path, makes no call after the failure and keeps the cells written before. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Settles the review comment on intel#357 about a judged cell regraded out of the hits keeping its old hits_judged.json. judge_cells visited only the cells load_hits reads, whose grade.json has matches. A cell judged earlier and then regraded into a grader failure, a grade without matches or another grader kept its earlier hits_judged.json, which runs list / runs show still list. Before its first judge call, judge_cells now removes the hits_judged.json of every other finished cell of the batch (load_cells). A removal that fails raises HitsWriteError, so the command exits 2 before it spends anything. --dry-run still writes and removes nothing. The text summary names the cells (the first ten and a count), and the --json summary lists them in stale_removed. The contract's section 9 and the judge_cells docstring say so. Tests: a cell regraded to a grader failure and to a grade with no matches loses its file on the next run, the other cells keep theirs, and text and JSON report it; a removal that fails exits 2 with no judge call; --dry-run removes nothing. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
Review rounds at
|
Bring in main at ad734f5 (intel#339, a SCIP code index reader; intel#340, eval coverage against a frozen need graph). Both sides register new artifact kinds at the end of the same lists, which are in insertion order, not sorted. Each conflict keeps both sides, main's entries first: - dokimi/dokimi/artifacts.py: the module docstring names need_graph and eval_coverage and then repo_hits_judged; SCHEMA_VERSIONS lists need_graph.v1 and eval_coverage.v1, then repo_hits_judged.v1. - dokimi/dokimi/runs/kinds.py: FIELDS keeps the need_graph and eval_coverage rows, then the repo_hits_judged row. - dokimi/tests/test_runs_scan.py: EXPECTED keeps the need_graph.v1 and eval_coverage.v1 rows, then the repo_hits_judged.v1 row. The auto-merged docs/reference/cli-interface.json is what scripts/cli_surface.py dump writes for the merged tree, so it needs no change. cli_routes.py, tests/data/cli_flags_allowlist.txt, tests/test_runs_show.py and the contract keep the entries of both sides. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
|
napetrov
left a comment
There was a problem hiding this comment.
Verdict: comment only, no blocking finding. 0 blocking, 2 questions, 2 nits, all introduced by this PR (code new to it, so probes ran on head only). Head a1b8822, merge base ad734f5, lane L4 (a new persisted format repo_hits_judged.v1 and new CLI leaves).
CI: 0 failing Actions jobs on head; the merge base is behind the base tip 41b3d8d, which is not a failure.
Raised before: the Copilot and exai threads T1–T7 are fixed on head (checked at each line). The four findings here are not among them.
Claims checked: C1–C8 implemented, no contradiction in the PR body. The one contradicted claim is the refusal text, not a PR-body claim: see the nit on hits_judge.py.
Unverified: the removal of hits_judged.json for a cell with no grade.json (not probed); the live judge (not run, no paid call); the context traces line of cli-interface.json (not checked against base).
Coverage: checked the 19 files of the diff on head, the grader pairing in graders/review.py, and probes V1–V4 on head. Not looked at: a live judge, the full test suite (the edge pass ran test_repo_hits_judge.py: 38 passed on head).
Brings in main up to 41b3d8d (intel#335, intel#281, intel#343, intel#344, intel#345, intel#274 and dependency updates). Conflict: - dokimi/dokimi/artifacts.py: the module docstring's list of artifact kinds. Main added effective_instructions (intel#335) before need_graph; this branch added repo_hits_judged after task_inspect. Kept both, in the docstring's order. SCHEMA_VERSIONS auto-merged with both sides' kinds. Auto-merged and checked: runs/kinds.py, cli_routes.py, tests/data/cli_flags_allowlist.txt, tests/test_cli_routes.py, tests/test_runs_scan.py, tests/test_runs_show.py and the contract keep both sides' entries. docs/reference/cli-interface.json was regenerated with scripts/cli_surface.py dump and matches the auto-merged file. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Settles four review points on repo hits judge / repo hits sheet (intel#357): - --max-usd with a judge that has no list price: an unpriced reply counted as $0, so the cap never fired. With --max-usd set, a live reply with no known price now stops the run after that call, as novelty's spend cap does: a cap stop (the cells written keep their file, exit 1) whose line names the judge model, says --max-usd cannot be enforced, gives the calls made and says to drop --max-usd to run unmetered (--json: budget_stop). Without --max-usd an unpriced call still counts as 0, and the spend line says how many calls had no price and that the spend is not known (--json: unpriced_calls). A replayed reply stays free. - The refusal of a judge said "must be of another family" while the check is the same-model rule (same_client). It now says the judge runs the model of an answer and must run another model. - read_labels took the later of two different labels for one pair. Two entries giving one (cell, thread) different non-null labels are now refused with exit 2, naming both entry indices, the cell, the thread and both labels; the same label twice, or a null next to a label, is taken. - repo hits sheet --out replaced a sheet a person had labelled. It now refuses with exit 2, before writing, when the file holds a JSON list with a label set, naming the path and the count; a file with no label set, or that is not a JSON list, is replaced as before. The contract's section 9, the --max-usd and --out help, the Meter and read_labels docstrings and the changelog fragment say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Settles review comment 4441684509 on intel#357: `read_labels` checked a sheet entry's cell, thread and finding index, but not the texts the person labelled. After answer.txt or the task's gold file changed, with the same finding index credited, a stale label was compared with the judge's label of other texts, and the arm could read as calibrated. A labelled entry's `comment` and `finding_text` must now equal the pair's current full texts exactly (the thread's comment in the gold file, the finding's text in answer.txt). Otherwise `--labels` is refused with exit 2 before any judge call, naming the entry, the thread, the cell and which text changed, and saying to write a new sheet. A labelled entry missing either key is refused, and so is one whose pair's texts are no longer found. An entry with a null label is not checked. The contract's section 9, the `read_labels` docstring and the changelog fragment say so; tests cover a changed finding text, a changed gold comment, a missing key, an unchecked null entry and a pair whose texts are gone. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
Review rounds at
|
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
Review at 790c0c1. The pairing, the judge-independence check, calibration with the label and text checks, the unpriced stop under --max-usd, the write errors and the stale-file removal work as described. Two things still change the numbers the table prints: a pair the judge never read lowers recall_judged as a 0, and a cell whose answer has no findings JSON drops out of the means without being counted. Details in the line comments.
What I ran, at 790c0c1:
make ci-local --offline: 19 passed, 0 failed, 13 skipped (12,543 tests passed).- A repro batch with one more
guidance:rawcell whose answer has no findings JSON: 5 replay cells, 1 graded without hit details, 1 grader failure, 2 written, and the fifth cell in no count.guidance:rawshows recall 0.667 over 1 cell, whererepo reportscores 0.333 over 2. - A fake judge that raises on its third call: the error escapes the command as a traceback, with no summary and no spend line.
Checked, no issues
- Earlier threads: each Copilot, exai-bot and @napetrov thread is fixed in the code as its reply says:
model_resolvedin the judge check, the refusal text, the unpriced stop, duplicate labels, the labelled-sheet overwrite, and labels on changed texts. - Artifact and registries:
repo_hits_judged.v1is a new schema with a fixture, registered inSCHEMA_VERSIONS,runs/kinds.pyand the scan tests. - Scores: the grader and
grade.jsonare untouched;recall_judgedis a new, optional figure, so earlier runs stay comparable. - Docs and changelog: contract §4.5 and §9,
architecture.mdand the CLI reference follow the code; the fragment is underAdded. New files carry the SPDX header.
Review of intel#357 (comment 4445906496): a cell with an unjudged pair (its full text not found, or no judge reply parsed) was written with a recall_judged that counted that pair as another point, and the 0 went into the arm's mean like a measured one. cell_doc now writes recall_judged null when any pair of the cell is unjudged, and rows() leaves those cells out of the mean recall_judged (the mean recall still covers every written cell). The summary counts and names them on their own line (`unjudged cells: N`) and in --json (`unjudged_cells`). The schema's description of recall_judged, the contract (§4.5, §9) and the tests say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Settles the review comment on intel#357 that the stop on repeated judge errors never fires with the response cache on. With the cache on and no stored reply, judged_reply probes each draw in replay-only mode before the live call, and each probe raises caching.CacheMiss through Meter's ask. Meter appended those misses to `failing`, so the live auth errors were never 3 in a row and the run went on through the whole batch with exit 0. Meter now re-raises CacheMiss before it records a failure, so only calls that failed count toward FAILING. A test runs the judge with an empty response cache and a judge that fails every live call with one auth error: the run stops after 3 pairs with exit 1. The contract says a cache miss is not a failed call. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Settles the review comment on intel#357 that the contract and the changelog promise an immediate stop after a judge reply with no known price under --max-usd. The code starts no further live call after such a reply. The reply still labels its pair, pairs replayed from the response cache still finish, and only when pairs are left do their cells get no file, with budget_stop set and exit 1. A batch whose last pair got the unpriced reply ends with exit 0 and budget_stop null. The contract section 9, the changelog fragment, the --max-usd help and the Meter docstring now say that. The behaviour is unchanged. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Settles review comment 4447261394 on intel#357: `labelled_entries` treated any error reading an existing sheet as "no sheet", so a labelled sheet with no read permission (or a directory, or a file that is not UTF-8 text) gave 0 labelled entries and `repo hits sheet` went on to overwrite it, losing the person's labels. Only `FileNotFoundError` now counts as no sheet. Any other `OSError`, or a `UnicodeDecodeError`, raises `HitsInputError` naming the file, and the command exits 2 before anything is written. A readable file that is not JSON or not a JSON list is still replaced, as before. The other read-before-overwrite sites of the change do not have the pattern: the stale `hits_judged.json` removal reads nothing and turns any `OSError` into exit 2, and the `--labels` file is refused on any read error and never written. The contract §9, the `--out` help and the changelog fragment say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
LGTM
I checked 2b71e36. Every point of my last review is fixed as the replies say: a cell with an unjudged pair now has recall_judged null and is left out of the means (4226669); a parse-error cell counts as 0 (5af8d24); a failed judge call makes its pair unjudged, and three in a row stop the run with the spend line (8567b5b, 127f8e2); texts changed since grading are not judged (88700aa); the texts go to the judge as data blocks (d3a1b52); and the run holds the batch lock (b55c577).
Merge: safe now: it merges cleanly into current main, 7 commits behind, and its tests pass on the merge. Once it is in, #373, #364, #336, #325, #323 and #365 conflict with it and need a rebase.
Disagreed with @napetrov on cached pairs being refused after an unpriced stop: replied in its thread.
What I ran, at 2b71e36:
make ci-local --offline: 18 passed, 1 failed, 13 skipped (12,555 tests passed). The failure isschema_compat, and it is not from this PR:mainaddedheadroom.v1(#279) and widenedrepo_environment.v1(#349) after this branch's base.- On the merge with current
main:test_repo_hits_judge.py, the runs scan/show, CLI routes, artifacts, CLI flags (snapshot) and docs-refs tests: 309 passed, 1 skipped. - With the response cache filled for one arm and an unpriced judge under
--max-usd: one live call, its cell stopped, and the cached arm's cell was replayed and written.
Checked, no issues
- New code since 790c0c1: the grader's
_cutand the new[:CUT]checks agree forNoneand non-string messages; the parse-error cell keepsn_threadsnull andrecall_judged0; a stopped or failing cell has its earlier file removed. - Schema: only descriptions of
repo_hits_judged.v1changed;recall_judgedwas already typed number or null. - Docs and changelog: contract §9, the
--max-usdand--outhelp and the fragment match the code, the reworded unpriced-stop text included.
repo hits sheet --out replaced, with exit 0, a labelled sheet with a UTF-8 BOM or a trailing comma, and every label was lost. Such a file is now refused like an unreadable one; only a missing or blank file counts as no sheet. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…uest id A provider error's text carries a per-call request id, so 3 auth errors in a row never compared equal and the run went on to exit 0 with every pair unjudged. An error with a status_code now repeats by its type and status; the full text is kept for the stop line. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
The suite passed with the failure reset after a reply, the one-error condition of the stop, or the label's strip().lower() removed. Pin each by its outcome: no stop on failures not in a row or on different errors, and title-case labels counted, not left unjudged. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Settles review comment 4449501562 on intel#357: after a judge reply with no known price under --max-usd, a direct Meter ask raises BudgetSpent before the response cache is read, so a cached pair looked refused too. Through `repo hits judge` it is not: judged_reply probes the cache in replay_only() before each live call, and the meter skips its stop check there, so a cached pair is answered with no live call. The behaviour stays. A command-level test pins it: the response cache filled for one arm, an unpriced judge under --max-usd 5, one live call for the other arm, whose cell stops, the cached arm's cell written, exit 1. The Meter docstring now says the stop applies to live calls and a caller replays through replay_only(). Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Settles the conversation comment on intel#357 on (arm, model) calibration. The judged table has one row per (arm, model), but `repo hits sheet` sampled per arm and `--labels` calibrated per arm, so in a batch with two answer models, labels on one model's pairs gave no uncalibrated line and `uncalibrated_arms: []`: the other model's recall_judged read as calibrated though nobody labelled its pairs. calibrate() now gives one entry per (arm, model), each with `arm` and `model`. The summary replaces `uncalibrated_arms` with `uncalibrated_rows`, the sorted `{"arm", "model"}` of the rows no pair labelled by both covers, and the text names them as `arm (model)`. `repo hits sheet` draws `--per-arm N` pairs from each (arm, model), so each row gets its own sample, and its line counts them per row. The contract (§4.5, §9), architecture.md, the --help texts and the changelog fragment say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
Round on 16eded4 (napetrov's re-review and @xaleryb's comment), head now 6ac19e2:
|
Conflict: - dokimi/docs/repo-evaluation-contract.md (§9, the `repo report --hits` bullet): main (intel#365) appended a sentence linking reviewing.md's "Reading a replay batch" to the old end of the bullet; this branch had rewritten that end (the `cells` records, parse-error cells and grader failures) and added the `repo hits judge` and `repo hits sheet` bullets after it. Kept this branch's text and added main's sentence at the end of the `--hits` bullet, before the `repo hits` bullets. docs/reference/cli-interface.json merged cleanly; regenerated with scripts/cli_surface.py dump, which writes the merged file unchanged. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
intel#365 added "Reading a replay batch" to docs/reviewing.md, whose bullet "Read the hits before citing one" describes reading the pairs of `repo report --hits` by hand. This branch adds the command that judges those pairs (issue intel#291), so the bullet now names it: `repo hits judge <batch> --judge PROVIDER/MODEL` labels every credited pair and gives `recall_judged`, which stays uncalibrated until a person labels a sample (`repo hits sheet`, then `--labels`) and `uncalibrated_rows` is empty. It narrows which pairs to read and does not replace reading the pairs a write-up cites. The sentence links contract §9. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
Round 12, head now 2e11889: c94831a merges |
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
LGTM
I checked 2e11889. The commits since my last review are right: calibration now goes per (arm, model), so labels on one answer model no longer make another model's rows read as calibrated (6ac19e2); a sheet that does not parse as JSON is not replaced (d35787a); repeated provider errors stop the run even when their request ids differ (fefac63); and a test pins that cached pairs finish after an unpriced stop (5987484).
Merge: safe now: it merges cleanly into current main, 1 commit behind, and its tests pass on the merge. Once it is in, #373, #364, #325, #336 and #323 conflict with it and need a rebase.
What I ran, at 2e11889:
make ci-local --offline: 19 passed, 0 failed, 13 skipped (12,986 tests passed).- On the merge with current
main(which changedreport.pyin #332): the hits judge,repo report --hits,repo compare, runs scan/show, CLI routes, artifacts, CLI flags (snapshot) and docs-refs tests: 603 passed, 1 skipped.
Checked, no issues
- Per-row calibration:
Pair.rowtakes the model from the sameresult.jsonkey thatrows()groups by, so the sheet strata,calibrate()anduncalibrated_rowsname the same rows as the table. - Docs: contract §4.5 and §9,
architecture.md,reviewing.md, the--helptexts and the fragment useuncalibrated_rows; no reference touncalibrated_armsis left.
Brings in main up to 39dbd2a (intel#332 first useful result, intel#357 judged recall, intel#381, intel#367 reach --scenarios shadow). Conflicts, each resolved by keeping both sides: - docs/repo-evaluation-contract.md: §7 step 3 lists the bash=<dir> scenario and then main's shadow=. / shadow=<dir> scenarios. - dokimi/commands/repo.py: cmd_repo_reach imports main's short list plus check_kinds, and runs check_kinds then check_shadow; the --scenarios help names root, cwd, read, bash, shadow and says what bash and shadow need from the profile. - dokimi/repos/reach.py: one KINDS ("root", "cwd", "read", "bash", "shadow") in place of the two each side added; Scenario keeps the bash prompt and main's shadow field; scenarios() builds read= and bash= and then main's shadow ones; run_reach keeps main's dry-run docstring and calls check_kinds first. - dokimi/repos/events.py: main split _apply into _apply_init, _apply_assistant and _apply_final and counts malformed tool_use and tool_result blocks as dropped lines. The split now carries this branch's _Stream: _apply_assistant checks the block and hands it to _tool_use (the Bash reads), and _apply_final marks permission denials through _deny. - scripts/growth_baseline.json: main's side, without the events.py::_apply entry; the split leaves _apply under the cap. tests/test_repo_useful.py (main's, from intel#332) called _apply_result with main's (block, by_id); it now passes the branch's (out, block, stream). docs/reference/cli-interface.json was regenerated with scripts/cli_surface.py dump and matches the merge. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
artifacts.py, runs/kinds.py and test_runs_scan.py keep reach_matrix.v1 from this branch and repo_hits_judged.v1 from intel#357. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
artifacts.py keeps dokimi_config.v1 from this branch and repo_hits_judged.v1 from intel#357. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
* feat(repo): judged recall over the credited replay hits Settles intel#291: dokimi:review_replay credits a human thread to the nearest finding within the window by location alone, so a finding that only sits near a thread gets the credit. `repo hits judge <batch> --judge PROVIDER/MODEL` is an optional, paid step after grading. It takes each credited pair from load_hits, reads the thread's full comment from the task's gold file under <batch>/_eval/content and the finding's full text from the cell's answer.txt at index `finding` (matches cuts both to 200 characters), and asks the judge, through dokimi.judged.judged_reply, for `same point`, `related` or `unrelated`. Each fully judged cell gets hits_judged.json (repo_hits_judged.v1) with the labels, their counts and recall_judged, the `same point` pairs over n_threads, so recall_judged <= recall. grade.json is not touched. Before any call it refuses (exit 2) a judge that runs any answer model of the batch, using novelty's same_client rule (a CLI alias counts as its family's full id), and a --labels entry naming no credited pair or holding a label other than the three. A reply that never parses, or a pair whose full text is not found, is unjudged. --dry-run prices the pairs at list price; --max-usd starts no new call once spent, and a cell left unjudged gets no file (exit 1). `repo hits sheet <batch> --out FILE` samples pairs for a person, 25 per arm by default with random.Random(seed); with that file, --labels gives agreement and Cohen's kappa per arm, and without it the output says recall_judged is uncalibrated. load_hits also returns the replay cells whose matches were read and the grader-failure count, so a cell with no credited pair counts in the means. Reading recall_judged in repo compare is left to a later change. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): judge refusal, calibration and sheet checks of hits judge Settles the review comments on intel#357. The judge-independence check read only the `model` a cell was asked of. A cell asked of one model and answered by another (the runner's `model_resolved`, from the trace, in both result.json and meta.json) accepted the answering model as its judge. answer_models now adds the resolved models of both records through report.resolved_models (a string or a list) next to the requested names, so such a judge exits 2 before any call. With --labels, an unfilled sheet, an empty list, or labels only on pairs the judge left unjudged gave a calibration with no jointly labelled pair, which dropped the uncalibrated line and printed an empty calibration heading. No pair labelled by both now reads as uncalibrated, with the reason; when only some arms have such pairs, a line names the others. The JSON summary gains `uncalibrated_arms`, the arms of `rows` with no jointly labelled pair (every arm without --labels); `calibration` is unchanged. read_labels matched an entry by cell and thread and ignored its `finding`, so a hand-edited entry, or a sheet older than a regrade, was compared against a pair the person did not label. An entry whose `finding` is missing or is not the finding the batch credits for its thread is now refused with exit 2 before any judge call, pointing at `repo hits sheet`. The contract's §4.5 and §9 say the same. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): exit 2 when hits judge cannot write a cell's file Settles the review point on intel#357 that an OSError from writing a cell's hits_judged.json, or from removing an earlier one when --max-usd stops that cell, escaped `repo hits judge` as a traceback with exit 1, while `repo hits sheet` reports an unwritable output with exit 2. judge_cells now stops at the first such failure, before any further judge call, and raises HitsWriteError with the path, the OSError and what it had done, so a Python caller gets a typed error. The command turns it into one line on stderr naming the path and the error, the judge calls made and their cost, and that the cells written before keep their files, and exits 2. The contract's section 9 says so. Tests: a directory where a cell's file goes, and an earlier file that cannot be removed when the budget stops its cell; each exits 2 naming the path, makes no call after the failure and keeps the cells written before. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): remove hits_judged.json of cells hits judge no longer reads Settles the review comment on intel#357 about a judged cell regraded out of the hits keeping its old hits_judged.json. judge_cells visited only the cells load_hits reads, whose grade.json has matches. A cell judged earlier and then regraded into a grader failure, a grade without matches or another grader kept its earlier hits_judged.json, which runs list / runs show still list. Before its first judge call, judge_cells now removes the hits_judged.json of every other finished cell of the batch (load_cells). A removal that fails raises HitsWriteError, so the command exits 2 before it spends anything. --dry-run still writes and removes nothing. The text summary names the cells (the first ten and a count), and the --json summary lists them in stale_removed. The contract's section 9 and the judge_cells docstring say so. Tests: a cell regraded to a grader failure and to a grade with no matches loses its file on the next run, the other cells keep theirs, and text and JSON report it; a removal that fails exits 2 with no judge call; --dry-run removes nothing. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge cap on unpriced calls, label and sheet checks Settles four review points on repo hits judge / repo hits sheet (intel#357): - --max-usd with a judge that has no list price: an unpriced reply counted as $0, so the cap never fired. With --max-usd set, a live reply with no known price now stops the run after that call, as novelty's spend cap does: a cap stop (the cells written keep their file, exit 1) whose line names the judge model, says --max-usd cannot be enforced, gives the calls made and says to drop --max-usd to run unmetered (--json: budget_stop). Without --max-usd an unpriced call still counts as 0, and the spend line says how many calls had no price and that the spend is not known (--json: unpriced_calls). A replayed reply stays free. - The refusal of a judge said "must be of another family" while the check is the same-model rule (same_client). It now says the judge runs the model of an answer and must run another model. - read_labels took the later of two different labels for one pair. Two entries giving one (cell, thread) different non-null labels are now refused with exit 2, naming both entry indices, the cell, the thread and both labels; the same label twice, or a null next to a label, is taken. - repo hits sheet --out replaced a sheet a person had labelled. It now refuses with exit 2, before writing, when the file holds a JSON list with a label set, naming the path and the count; a file with no label set, or that is not a JSON list, is replaced as before. The contract's section 9, the --max-usd and --out help, the Meter and read_labels docstrings and the changelog fragment say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge refuses a label on texts that changed Settles review comment 4441684509 on intel#357: `read_labels` checked a sheet entry's cell, thread and finding index, but not the texts the person labelled. After answer.txt or the task's gold file changed, with the same finding index credited, a stale label was compared with the judge's label of other texts, and the arm could read as calibrated. A labelled entry's `comment` and `finding_text` must now equal the pair's current full texts exactly (the thread's comment in the gold file, the finding's text in answer.txt). Otherwise `--labels` is refused with exit 2 before any judge call, naming the entry, the thread, the cell and which text changed, and saying to write a new sheet. A labelled entry missing either key is refused, and so is one whose pair's texts are no longer found. An entry with a null label is not checked. The contract's section 9, the `read_labels` docstring and the changelog fragment say so; tests cover a changed finding text, a changed gold comment, a missing key, an unchecked null entry and a pair whose texts are gone. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge gives no recall_judged to a cell not fully judged Review of intel#357 (comment 4445906496): a cell with an unjudged pair (its full text not found, or no judge reply parsed) was written with a recall_judged that counted that pair as another point, and the 0 went into the arm's mean like a measured one. cell_doc now writes recall_judged null when any pair of the cell is unjudged, and rows() leaves those cells out of the mean recall_judged (the mean recall still covers every written cell). The summary counts and names them on their own line (`unjudged cells: N`) and in --json (`unjudged_cells`). The schema's description of recall_judged, the contract (§4.5, §9) and the tests say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge counts a replay cell with no findings JSON as 0 Review of intel#357 (comment 4445906504): a dokimi:review_replay cell whose answer held no findings JSON is graded parse_error with recall 0 and no `matches`, so load_hits left it out of `cells` and of every count. An arm that failed more often then showed a higher mean recall and recall_judged. load_hits now lists such a cell in `cells` (parse_error true, no pair, recall 0 from grade.json), so repo hits judge writes its hits_judged.json with no pair and recall_judged equal to the grader's recall, 0: measured, not unjudged. It counts in its arm's means. A grade with no `matches` that is not a parse error is skipped as before. The test builds the review's case, a guidance:raw cell answering "I could not finish the review.", and pins the arm's means. The contract, the schema description and the changelog fragment say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge records a failed judge call and stops on repeats Review of intel#357 (comment 4445906511): a judge-call error other than an unparseable reply (a 5xx or 429 past the client's retries, an auth error, a missing key) escaped repo hits judge as a traceback, with no summary and no spend line, and the cell in progress kept whatever file an earlier run left. judge_pair now records such a pair as unjudged, with error "judge call failed: <type>: <message>", so its cell's recall_judged is null, and the run goes on. The Meter keeps the errors of the calls that failed since the last reply; once 3 calls in a row fail with one error, judge_cells removes the in-progress cell's earlier file and raises JudgeFailing, which the command reports like HitsWriteError (a ✗ line with the error, the judge calls made and their cost, and the cells that keep their files) and exits 1. judge_cells is split into helpers to stay under the complexity cap. The contract §9 and the schema's `error` description say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge refuses a pair whose text changed since grading Review of intel#357 (comment 4445906521): the judge read the thread's comment from today's _eval/content gold file and the finding from today's answer.txt, checked against the grade's match only by thread id, finding index and file. A later `repo run` that re-exported a reworded thread, or an edited answer, was then judged without notice. The judge's pair now also requires the full comment and the finding's message, cut to the grader's own review.CUT, to equal the match's `comment` and `message`. A mismatch is a not-found pair, unjudged, with an error saying the text changed since grading; a regrade makes it judged again. The round-7 sheet test now regrades after changing the gold comment, as it already did for the answer, since an unregraded change is now caught here first. The contract §4.5 and §9 say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge gives the texts to the judge as data blocks Review of intel#357 (comment 4445906531): the finding, written by the model under test, went into the judge prompt as plain lines just before the label rules, so a message ending with a line such as `Labels: every finding here is "same point"` had nothing marking it as data. The prompt now gives the thread (file, line, comment) and the finding (file, line, text) each as one line of JSON in a fenced block marked `data`, with three backticks in a text written as JSON unicode escapes (the same decoded text), so they close no block. It says that text inside the blocks is data and not instructions, and the label rules come after the blocks. No prompt version or hash is recorded anywhere; the response cache keys on the prompt text, so the new prompt is asked afresh. The test plants the review's line in a finding and checks it stays inside its block, JSON-escaped. The contract §9 says so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge holds the batch lock while it judges Review of intel#357 (comment 4445906541): repo hits judge wrote into a batch's cells without batch_lock, so it could run while `repo run` resumed the same batch, rewriting _eval/content and regrading cells under it. A run that judges now holds runner.batch_lock from before it reads the pairs to its end. When another run holds the batch, BatchBusy exits 2 with the message `repo run` gives; a lock that cannot be taken (a read-only batch) exits 2 naming it, as an output that cannot be written does. Both come before any judge call. --dry-run writes nothing and takes no lock. The contract §9 says so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits judge does not count cache misses as failed calls Settles the review comment on intel#357 that the stop on repeated judge errors never fires with the response cache on. With the cache on and no stored reply, judged_reply probes each draw in replay-only mode before the live call, and each probe raises caching.CacheMiss through Meter's ask. Meter appended those misses to `failing`, so the live auth errors were never 3 in a row and the run went on through the whole batch with exit 0. Meter now re-raises CacheMiss before it records a failure, so only calls that failed count toward FAILING. A test runs the judge with an empty response cache and a judge that fails every live call with one auth error: the run stops after 3 pairs with exit 1. The contract says a cache miss is not a failed call. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * docs(repo): say what hits judge does after an unpriced reply Settles the review comment on intel#357 that the contract and the changelog promise an immediate stop after a judge reply with no known price under --max-usd. The code starts no further live call after such a reply. The reply still labels its pair, pairs replayed from the response cache still finish, and only when pairs are left do their cells get no file, with budget_stop set and exit 1. A batch whose last pair got the unpriced reply ends with exit 0 and budget_stop null. The contract section 9, the changelog fragment, the --max-usd help and the Meter docstring now say that. The behaviour is unchanged. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): hits sheet refuses an --out file it cannot read Settles review comment 4447261394 on intel#357: `labelled_entries` treated any error reading an existing sheet as "no sheet", so a labelled sheet with no read permission (or a directory, or a file that is not UTF-8 text) gave 0 labelled entries and `repo hits sheet` went on to overwrite it, losing the person's labels. Only `FileNotFoundError` now counts as no sheet. Any other `OSError`, or a `UnicodeDecodeError`, raises `HitsInputError` naming the file, and the command exits 2 before anything is written. A readable file that is not JSON or not a JSON list is still replaced, as before. The other read-before-overwrite sites of the change do not have the pattern: the stale `hits_judged.json` removal reads nothing and turns any `OSError` into exit 2, and the `--labels` file is refused on any read error and never written. The contract §9, the `--out` help and the changelog fragment say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): refuse to replace a sheet that does not parse as JSON repo hits sheet --out replaced, with exit 0, a labelled sheet with a UTF-8 BOM or a trailing comma, and every label was lost. Such a file is now refused like an unreadable one; only a missing or blank file counts as no sheet. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * fix(repo): stop the judge run on a provider error with a changing request id A provider error's text carries a per-call request id, so 3 auth errors in a row never compared equal and the run went on to exit 0 with every pair unjudged. An error with a status_code now repeats by its type and status; the full text is kept for the stop line. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * test(repo): pin the judge stop rule and the label normalisation The suite passed with the failure reset after a reply, the one-error condition of the stop, or the label's strip().lower() removed. Pin each by its outcome: no stop on failures not in a row or on different errors, and title-case labels counted, not left unjudged. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> * test(repo): pin that cached pairs finish after an unpriced stop Settles review comment 4449501562 on intel#357: after a judge reply with no known price under --max-usd, a direct Meter ask raises BudgetSpent before the response cache is read, so a cached pair looked refused too. Through `repo hits judge` it is not: judged_reply probes the cache in replay_only() before each live call, and the meter skips its stop check there, so a cached pair is answered with no live call. The behaviour stays. A command-level test pins it: the response cache filled for one arm, an unpriced judge under --max-usd 5, one live call for the other arm, whose cell stops, the cached arm's cell written, exit 1. The Meter docstring now says the stop applies to live calls and a caller replays through replay_only(). Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * fix(repo): calibrate judged recall per arm and answer model Settles the conversation comment on intel#357 on (arm, model) calibration. The judged table has one row per (arm, model), but `repo hits sheet` sampled per arm and `--labels` calibrated per arm, so in a batch with two answer models, labels on one model's pairs gave no uncalibrated line and `uncalibrated_arms: []`: the other model's recall_judged read as calibrated though nobody labelled its pairs. calibrate() now gives one entry per (arm, model), each with `arm` and `model`. The summary replaces `uncalibrated_arms` with `uncalibrated_rows`, the sorted `{"arm", "model"}` of the rows no pair labelled by both covers, and the text names them as `arm (model)`. `repo hits sheet` draws `--per-arm N` pairs from each (arm, model), so each row gets its own sample, and its line counts them per row. The contract (§4.5, §9), architecture.md, the --help texts and the changelog fragment say so. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> * docs(repo): name repo hits judge where reviewing.md reads the hits intel#365 added "Reading a replay batch" to docs/reviewing.md, whose bullet "Read the hits before citing one" describes reading the pairs of `repo report --hits` by hand. This branch adds the command that judges those pairs (issue intel#291), so the bullet now names it: `repo hits judge <batch> --judge PROVIDER/MODEL` labels every credited pair and gives `recall_judged`, which stays uncalibrated until a person labels a sample (`repo hits sheet`, then `--labels`) and `uncalibrated_rows` is empty. It narrows which pairs to read and does not replace reading the pairs a write-up cites. The sentence links contract §9. Signed-off-by: Anton Volkov <antonwolfy@gmail.com> --------- Signed-off-by: Anton Volkov <antonwolfy@gmail.com> Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com> Co-authored-by: Alexander Rybkin <alexander.rybkin@intel.com>


Summary
Settles #291 (issue, decisions):
dokimi:review_replaycredits a human thread to the nearest finding on its file withinwindowlines, by location alone, so a finding that only sits near a thread gets the credit.On
mainthere is no way to rescore a credited pair:repo report --hitslists the pairs withcommentandmessagecut to 200 characters, andpython cli.py repo hits judge ...exits 2 withinvalid choice: 'hits'. In the test batch of this PR (two arms, one cell each, three gold threads), the grader credits, among others, a thread asking to add a TODO naming the follow-up issue (a 396-character comment) to a finding saying the TODO is repeated five times:recall(grader)recall_judgedguidance:noner1t_todo↔ "The TODO is repeated five times",t_op↔ "mul adds"guidance:rawr1t_todo↔ "Add a TODO naming the issue",7↔ "read() has no docstring"The labels in that table come from the tests' fake judge, not a real model; no real model was called for this PR.
The second commit (86c37d3) settles the review of this PR: the judge check missed the model that actually answered a cell, a labels file with no jointly labelled pair hid the uncalibrated warning, and a sheet entry could label a finding other than the credited one. On the same test batch, at 05ebb9a and at 86c37d3:
guidance:rawcell asked ofclaude-sonnet-5-5withmodel_resolved: "claude-opus-5-5",--judge anthropic/claude-opus-5-5✗ judge anthropic/claude-opus-5-5 runs the same model as the answer model 'claude-opus-5-5' of this batch; ...--labelswith the untouchedrepo hits sheetfile (everylabel: null)calibration against the labels (person vs judge):heading, no uncalibrated line; JSONcalibration: []recall_judged is uncalibrated (no pair carries both a person's and the judge's label): ...; JSONcalibration: [],uncalibrated_arms: ["guidance:none", "guidance:raw"]findingchanged to 99✗ <sheet> entry 1: the sheet labels finding 99 of thread 't_todo', but the batch credits finding 0 for it in cell ...; write a new sheet with `repo hits sheet`The third commit (af4c4a4) settles a later review comment: a
hits_judged.jsonthat cannot be written, or an earlier one that cannot be removed when--max-usdstops its cell, escapedrepo hits judgeas a traceback with exit 1, whilerepo hits sheetreports an unwritable output with exit 2. On the same test batch with the fake judge at $0.40 a call, at 86c37d3 and at af4c4a4:guidance:rawr1'shits_judged.jsongoesIsADirectoryError: [Errno 21] Is a directory: '<cell>/hits_judged.json.<pid>.<tid>.tmp' -> '<cell>/hits_judged.json'✗ cannot write <cell>/hits_judged.json: Is a directory; stopped there after 4 judge call(s), $1.6000; the 1 cell(s) written before keep their hits_judged.jsonguidance:rawr1's cell directory read-only (chmod 555)PermissionError: [Errno 13] Permission denied: '<cell>/hits_judged.json.<pid>.<tid>.tmp'✗ cannot write <cell>/hits_judged.json: Permission denied; stopped there after 4 judge call(s), ...--max-usd 1with a directory in place of the stale file of the cell the budget stopsIsADirectoryError: [Errno 21] Is a directory: '<cell>/hits_judged.json'✗ cannot remove <cell>/hits_judged.json: Is a directory; stopped there after 3 judge call(s), $1.2000; the 1 cell(s) written before keep their hits_judged.jsonThe fourth commit (38e1da9) settles a later review comment:
repo hits judgevisits only the cells whosegrade.jsonhasmatches, so a cell judged once and then regraded into a stateload_hitsskips kept its oldhits_judged.json, whichruns list/runs showstill list. On the same test batch, after a firstrepo hits judge, withguidance:rawr1 regraded and the command run again, at af4c4a4 and at 38e1da9:result.jsongrade.status: grader_failure)hits_judged.jsonstays; nostale_removedkey, no lineremoved the earlier hits_judged.json of 1 cell(s) not judged now (a grader failure, a grade without hit details, another grader): review_replay_pr3098__guidance-raw__claude-sonnet-5-5__r1; JSONstale_removed: ["review_replay_pr3098__guidance-raw__claude-sonnet-5-5__r1"]matches(a parse error)✗ cannot remove <cell>/hits_judged.json: Is a directory; stopped there after 0 judge call(s), $0.0000; the 0 cell(s) written before keep their hits_judged.jsonThe fifth commit (8e05118) settles napetrov's review, after a merge of
main(6c52d10, theartifacts.pydocstring list conflict resolved by keeping both kinds):--max-usdwith an unpriced judge (comment): a reply with no known price counted as $0, so the cap never fired. Under--max-usd, no further live call starts after such a reply, as withnovelty's cap. When pairs are left, that is a cap stop (exit 1) naming the judge model and saying--max-usdcannot be enforced;--jsonaddsbudget_stop. 31837e0 corrects the docs on what happens when none is left. Without--max-usdthe spend line says how many calls had no price (--json:unpriced_calls).--labels(comment) exit 2, naming both entries; the same label twice is taken.repo hits sheet --out(comment) refuses, exit 2 before writing, to replace a file holding a set label; an unlabelled sheet is replaced as before.The sixth commit (790c0c1) settles a later review comment (comment):
--labelschecked an entry's cell, thread and finding index, not the texts the person labelled. When the credited finding's text inanswer.txtchanged with the batch still crediting the same index, or the thread's comment in the gold file changed, the old label was still compared with the judge's label of the new texts, and the arm read as calibrated. On the same test batch, with every pair of a fresh sheet labelled and thenguidance:noner1 changed,repo hits judge --labels <sheet> --jsonat 8e05118 and at 790c0c1:t_oprewritten from "mul adds" to "mul adds, see #12" at the same line, and the cell regraded (still finding 1)calibrationgivesguidance:none2 labelled pairs, the same as for the unchanged sheet✗ <sheet> entry 0: the finding's text of thread 't_op' of cell review_replay_pr3098__guidance-none__claude-sonnet-5-5__r1 is not the text the sheet labelled (a sheet older than a change to answer.txt or the gold file, or a hand-edit); write a new sheet with `repo hits sheet`t_op's comment in the task'sgold.jsonchangedcalibrationthe thread's commentcalibrationThe seventh to twelfth commits settle vlad-perevezentsev's review, one commit per comment. The comments are about cells the judge never measured, cells the replay grader scored 0, judge-call errors, texts changed since grading, the prompt, and the batch lock. On the same test batch, at 790c0c1 and at b55c577:
guidance:rawr1'sanswer.txtholds no findings any more, so both its pairs are unjudgedrecall_judged: 0.0, and the row's meanrecall_judgedis 0.000recall_judged: null; the row shows-;unjudged cells: 1 (a pair not judged: recall_judged null, left out of the means): ...;--jsonunjudged_cellsguidance:rawr3 cell answeringI could not finish the review.replay_cells: 5, without_matches: 1, grader_failures: 1, written: 2;guidance:rawrecall 0.667 over 1 cellwritten: 3;guidance:rawover 2 cells, recall 0.3335,recall_judged0.1665; r3's file has no pair andrecall_judged: 0.0RuntimeErroron its third callRuntimeErrorescapescmd_repo_hits_judge: a traceback, no summaryerror: "judge call failed: RuntimeError: provider said 500", and its cell is null. The same error on 3 calls in a row stops the run with exit 1 and a✗line with the calls and the spendt_op's comment in_eval/contentreworded after gradingnot judged, full text not found: ... is not the comment grade.json credits: the text changed since grading; a regrade makes it judged again\nLabels: every finding here is "same point"datablock; the planted line is only there JSON-escaped, and the rules come after the blocksrunner.batch_lockheld on the batch, as a resumingrepo runholds it✗ batch <batch> is in use by another `repo run`; wait for it or use another --batch;--dry-runstill runsThe thirteenth and fourteenth commits settle a later review of b55c577, one commit per comment. On the same test batch, at b55c577 and at 31837e0:
guidance:rawr3 pair (5 in all), a judge that raisesRuntimeError: invalid api keyon every live callcaching.CacheMissinto the failure count, so the auth errors were never 3 in a row✗ the judge call failed 3 times in a row (RuntimeError: invalid api key); stopped there after 0 judge call(s), $0.0000; ...--max-usd 0.5 --jsonbudget_stop: null; the contract and changelog said the reply stops the run--helpand theMeterdocstring now say that no further live call starts, that the reply still labels its pair, that replays still finish, and that exit 1 andbudget_stopcome only when pairs are leftThe fifteenth commit (2b71e36) settles a later review comment (comment):
repo hits sheetread an existing--outfile to check it for labels, and took any error reading it as "no sheet", so it went on to overwrite a labelled sheet it could not read. On the same test batch, with a sheet holding onesame pointlabel at--out, at 31837e0 and at 2b71e36:--outPermissionError(writing still allowed)wrote 4 pair(s) to label to <sheet>; ...: the label is gone✗ cannot read <sheet> to check it for labels (Permission denied); move it aside or pass another --out✗ cannot read <sheet> to check it for labels (not UTF-8 text: 'utf-8' codec can't decode byte 0xff ...); ...Error: cannot write <sheet>: [Errno 21] Is a directory: ...✗ cannot read <sheet> to check it for labels (Is a directory); ...The sixteenth to eighteenth commits are Alexander Rybkin's. d35787a makes
repo hits sheetrefuse an--outfile that does not parse as JSON (a BOM, a trailing comma), where it replaced it before and lost its labels. fefac63 counts a provider error that has astatus_codeas repeated by its type and status, since its text carries a per-call request id. Before it, 3 auth errors in a row never compared equal, and the run went on to exit 0 with every pair unjudged. 16eded4 pins the stop rule and the label normalisation.The nineteenth and twentieth commits settle a later review of 16eded4.
Meter.askafter the stop is refused before the cache is read.repo hits judgedoes not take that path:judged_replyprobes the cache inreplay_only()before each live call, and the meter skips its stop check there. Through the command, with the cache filled forguidance:rawr1, an uncachedguidance:noner1 and an unpriced judge under--max-usd 5, the run exits 1 after 1 live call.guidance:noner1 is inbudget_stopped, andguidance:rawr1 is replayed and written (written: 1). The behaviour stays; a test pins it, and theMeterdocstring now says the stop applies to live calls and a caller replays throughreplay_only().guidance:nonearm answered by two models, with a fresh sheet and labels only on theclaude-sonnet-5-5pairs,repo hits judge --labels <sheet> --json, at 16eded4 and at 6ac19e2:repo hits sheetwrote 4 pair(s) to label to <sheet>; guidance:none: 4 of 4; with--per-arm 1(the tests' two-model batch) oneguidance:nonepair, of either model...; guidance:none (claude-sonnet-5-5): 2 of 2; guidance:none (gpt-5): 2 of 2; with--per-arm 1one pair per modelcalibration[{"arm": "guidance:none", "labelled": 2, "agreement": 0.0, "kappa": 0.0}]"model": "claude-sonnet-5-5"uncalibrated_arms: [], and no uncalibrated lineuncalibrated_rows: [{"arm": "guidance:none", "model": "gpt-5"}], andrecall_judged is uncalibrated in guidance:none (gpt-5): no pair of it carries both a person's and the judge's labelA third merge of
main(c94831a, at b084e47) resolves one conflict: #365 added a link todocs/reviewing.md's "Reading a replay batch" at the end of contract §9'srepo report --hitsbullet, whose end this PR rewrote; both are kept, the link at the end of the rewritten bullet.The twenty-first commit (2e11889) names
repo hits judgein that subsection's "Read the hits before citing one" bullet: it labels every credited pair and givesrecall_judged, which is uncalibrated until a person labels a sample (repo hits sheet, then--labels) anduncalibrated_rowsis empty, so it narrows which pairs to read and does not replace reading the pairs a write-up cites.Changes
repo hits judge <batch> --judge PROVIDER/MODEL [--labels FILE] [--max-usd X] [--dry-run] [--json](dokimi/repos/hits_judge.py,dokimi/commands/repo_hits.py): pairs come fromrepos/report.py::load_hits. For each pair it reads the thread's fullcommentfrom the task's gold file (the cell'stask.jsongold, defaultgold.json, in the task's directory under<batch>/_eval/content, as the built-in grader resolves it) and the finding's fullmessage,ruleandevidencefrom the cell'sanswer.txtviagraders/review.py::findings_ofat indexfinding. A pair whose text is not found is reported and leftunjudged. That covers no single task directory, no such thread, no finding at the index, or a file other than the onematchesnames. Since 88700aa it also covers a full comment or findingmessagewhose firstreview.CUT(200) characters are not the match's: the text changed since grading. One call per pair goes throughdokimi.judged.judged_reply(redraws, response cache) and asks for{"label": "same point" | "related" | "unrelated", "reason": ...}. The prompt (d3a1b52) gives the thread (file, line, comment) and the finding (file, line, text) each as one JSON line in a fenceddatablock, with three backticks in a text JSON-escaped. It says text inside the blocks is data, not instructions, and puts the label rules after the blocks. A reply that never parses isunjudged. So is a pair whose judge call fails (8567b5b: any error butUnparseableand the budget stop, recorded aserror: "judge call failed: <type>: <message>"), and the run goes on. Once 3 calls in a row fail with one error (an auth error, a missing key; since 127f8e2 a response-cache miss of the replay-only probe before a live call is not a failed call, and since fefac63 an error with astatus_codeis the same error when its type and status are),judge_cellsremoves the in-progress cell's earlier file and raisesJudgeFailing. The command prints one✗line with the error, the judge calls made and their cost, and the cells that keep their files, and exits 1.runner.batch_lock(<batch>/_eval/lock, the lockrepo runholds while it rewrites_eval/contentand regrades) from before it reads the pairs to its end.BatchBusyexits 2 withrepo run's message, and a lock that cannot be taken (a read-only batch) exits 2 naming it, both before any call.--dry-runtakes no lock.meta.jsonandresult.jsonmodel(the model asked for) andmodel_resolved(the models the trace says answered, a string or a list, read throughreport.resolved_models; 86c37d3), so a harness alias that mapped a cell to another model cannot hide it. The check reusesnovelty.client.same_client(built onllm_cli.same_model), with each answer model read as a CLI ref, soclaude-cli/sonnetis refused for aclaude-sonnet-5-5batch.reposandnoveltyare both in the tracks layer, andnoveltyimports nothing fromrepos, so this adds no upward import and no cycle.<cell>/hits_judged.json, a new artifactrepo_hits_judged.v1(dokimi/schemas/repo_hits_judged.v1.json, registered inartifacts.SCHEMA_VERSIONS, with itsruns list/runs showfields inruns/kinds.pyand a fixture). It holds the judge, each pair'sthread,finding,labelandreason(errorwhen unjudged), the counts ofsame point,related,unrelatedandunjudged,n_threads(fromgrade.jsonmetrics), the grader'srecall, andrecall_judged=same point/n_threads. Since 4226669,recall_judgedis null when any pair of the cell is unjudged: a pair the judge never labelled is not measured, so it does not count as another point. Since 5af8d24, a cell whose answer had no findings JSON (the grader'sparse_error) is written with no pair andrecall_judgedequal to the grader'srecall, 0: that is measured. A rerun rewrites the file, and the response cache replays the calls.--dry-runcounts the pairs and prices one call each withdokimi.llm_pricing.list_cost_usd(prompt characters / 4, 100 reply tokens), with no call.--max-usd Xstarts no new live call once X is spent, or once a live call came back with no known price (8e05118). That reply still labels its pair when it parses, and pairs replayed from the response cache still finish (31837e0 says so in the docs; 5987484 pins it through the command). A cell not fully judged gets no file (an earlier one is removed), it is listed, and the command exits 1, asrepo rundoes after--max-batch-usd. When no pair is left, the run exits 0 withbudget_stopnull.judge_cellsstops at the firsthits_judged.jsonit cannot write, or earlier one it cannot remove when the budget stops a cell, before any further judge call, and raisesHitsWriteError(dokimi/repos/hits_judge.py) with the path, theOSErrorand what it had done, so a Python caller gets a typed error.cmd_repo_hits_judgeturns it into one✗line on stderr naming the path and the error, the judge calls made and their cost, and that the cells written before keep their files, and exits 2 (EXIT_USAGE), asrepo hits sheetdoes throughcommands/_output.exit_on_unwritable. The command keeps all its exit codes.judge_cellsremoves thehits_judged.jsonof every finished cell of the batch (report.load_cells) thatload_hitsdoes not read: a cell regraded since into a grader failure, a grade withoutmatches, or another grader. A removal that fails raisesHitsWriteError, so the command exits 2 before it spends anything.--dry-runreturns beforejudge_cellsand removes nothing. The text summary names the cells (the first ten, thenand N more, asrepo reportlists missing cells), and--jsonlists them instale_removed([]when none).recall, and the meanrecall_judgedof the cells whoserecall_judgedis not null (4226669). A line counts and names the cells left out (unjudged cells: N ...;--json:unjudged_cells).--jsonprints the same summary, plusuncalibrated_rows(uncalibrated_armsfrom 86c37d3, per (arm, model) since 6ac19e2): the sorted{"arm", "model"}of each row ofrowswith no pair labelled by both the person and the judge, which is every row without--labels.dokimi/repos/hits_calibration.py):repo hits sheet <batch> --out FILE [--per-arm N] [--seed N]makes no call. It writes a JSON list of pairs with their full texts andlabel: null, N per arm and answer model, a row of the judged table, since 6ac19e2 (default 25; all of a row's pairs when it has fewer), drawn withrandom.Random(seed). An--outfile that holds a set label is refused (exit 2), not replaced (8e05118). So is (2b71e36) an existing--outfile that cannot be read (no read permission, a directory, not UTF-8 text), before anything is written, since whether it holds labels is not known, and (d35787a) so is a file that does not parse as JSON (a BOM, a trailing comma). A missing or blank file counts as no sheet, and JSON that is not a list is replaced (hits_calibration.labelled_entries).--labels FILEreads that file back. These entries exit 2 naming the entry, before any call: one that names a cell or thread of no credited pair, one with a label outside the three, two that give one pair different labels (8e05118), (86c37d3) one with nofindingor with afindingother than the one the batch credits for its thread (a hand-edit, or a sheet older than a regrade), and (790c0c1) a labelled entry whosecommentorfinding_textis missing or is not, exactly (no normalisation), the pair's full text now, or whose pair's texts are no longer found (the message then names why, rather than a changed text).hits_calibration._check_textsdoes the check; an entry withlabel: nullis not checked. The output then gives agreement and Cohen's kappa per arm and answer model (6ac19e2; per arm before), eachcalibrationentry with itsarmandmodel(a few lines, no new dependency). Without--labelsit printsrecall_judged is uncalibrated: label a sample of pairs (repo hits sheet) .... So does a labels file where no pair carries both labels (an unfilled sheet,[], or labels only on pairs the judge left unjudged), with that reason added and no empty calibration heading. When only some rows are covered, a line names the others (recall_judged is uncalibrated in guidance:raw (claude-sonnet-5-5): ...); 86c37d3, by row since 6ac19e2.load_hitsalso returnsgrader_failuresandcells: each replay cell whosematcheswas read, with or without a pair, and (5af8d24) each cell gradedparse_error, with itsn_threads,recallandparse_error. Withoutcells, a cell with no credited pair, or with no findings at all, would drop out of the means.--labelsand--per-armgo ontests/data/cli_flags_allowlist.txtwith a reason (no registry concept for them yet).--judge,--max-usd,--dry-run,--json,--outand--seedare canonical. Route table:repo hits judgeis paid andrepo hits sheetis free. The commands are registered fromcommands/repo_run.py::register(+4 lines), which passes in its_usdparser.docs/reference/cli-interface.jsonis regenerated.--hits --jsonkeys; 86c37d3:model_resolvedin the judge check, thefindingcheck of--labels, the uncalibrated line anduncalibrated_arms; af4c4a4: exit 2 when a cell's file cannot be written or removed; 38e1da9: the removal of earlier files of cells no longer judged, andstale_removed; 790c0c1: the text check of--labels; 4226669 to b55c577: nullrecall_judgedandunjudged cells, parse-error cells, failed judge calls and the stop on repeats, the check of the texts against the match, the data blocks of the prompt, and the lock; 127f8e2: a cache miss is not a failed call; 31837e0: what--max-usddoes after an unpriced reply, also in--helpand theMeterdocstring; 2b71e36: an--outofrepo hits sheetthat cannot be read, also in--help; d35787a: one that does not parse as JSON; 6ac19e2: calibration, the sheet anduncalibrated_rowsper (arm, model), also in--help);docs/architecture.mdmodule entry (per (arm, model) since 6ac19e2);docs/reviewing.md's "Read the hits before citing one" bullet namesrepo hits judgeand when its figure is calibrated (2e11889). The schema's descriptions ofrecall_judgedanderrorsay the same; its types are unchanged.cellssentence, 31837e0 rewords its unpriced-reply clause, 2b71e36 adds the--outfile that cannot be read, d35787a the one that does not parse as JSON, and 6ac19e2 says per arm and answer model and namesuncalibrated_rows.Not in this PR, as decided on the issue: reading
recall_judgedinrepo compare(repos/compare.pyis at its line cap; after #332), and judging near-misses outside the window.Risk
Grading is unchanged: the grader,
grade.jsonandrepo report's table are as before.repo report --hits --jsongains two keys (grader_failures,cells); the existing keys are unchanged.runs list --kindgains the choicerepo_hits_judged. The new commands write only<cell>/hits_judged.json, the--outsheet, and the batch's_eval/lock(b55c577, the filerepo runalready locks). A judge call that raises leaves its pair unjudged (8567b5b). The run stops with exit 1 only when the same error repeats on 3 calls in a row. Since 127f8e2 that stop also fires with the response cache on: before it, a judge failing every call ran through the whole batch there, exit 0. A transient error therefore costs one pair's figure and makes its cell null, and a rerun with the response cache asks again only the pairs not stored. A cell with any unjudged pair now hasrecall_judged: nulland drops out of its arm's meanrecall_judged(4226669). That mean can then cover fewer cells than the meanrecallbeside it, and theunjudged cellsline says how many. A cell that answered with no findings JSON now gets a file and counts as 0 in both means (5af8d24), andrepo report --hits --jsonlists it incells, which gains aparse_errorkey. A pair whose gold comment or finding message changed since grading is no longer judged until the cell is regraded (88700aa). The prompt changed (d3a1b52), so the response cache replays none of the judge calls stored before it.repo hits judgenow exits 2 while arepo runholds the batch; the refusal message, kept fromrepo run, suggests--batch, whichrepo hits judgedoes not take. The--labelsfile must carry each entry'sfinding, asrepo hits sheetwrites it. A hand-written file without it is refused, and so is a sheet written before a regrade moved a thread's credit. A labelled entry must also carrycommentandfinding_textequal to the pair's current texts (790c0c1): a sheet labelled beforeanswer.txtor the gold file changed, or with a text edited by hand (whitespace included), is refused, and a new sheet must be written and labelled again. Ahits_judged.jsonthat cannot be written or removed now exits 2 with one line, not 1 with a traceback (af4c4a4). The run stops there as before, and the calls already paid for are in the response cache when it is on. A run that judges now also deletes thehits_judged.jsonof a cell whose current grade it does not read (38e1da9); a person who wants to keep such a file must copy it before rerunning the command. The--jsonsummary gainsstale_removed. Since 2b71e36,repo hits sheetexits 2 on an existing--outfile it cannot read (no read permission, not UTF-8 text), where it overwrote it before; a directory at--outexited 2 before too, from the write, and now gets thecannot read ... to check it for labelsline instead. Since d35787a a readable--outfile that does not parse as JSON is refused too, where it was replaced. Since fefac63 the run stops on 3 provider errors in a row with one type and status even when their texts differ. Since 6ac19e2 the--jsonsummary hasuncalibrated_rows(a list of{"arm", "model"}) in place ofuncalibrated_arms(a list of arm names), and eachcalibrationentry gainsmodel. Neither key is onmain, so only a reader of this PR's earlier output sees the rename. In a batch with more than one answer model per arm, a sheet now holds up to--per-armpairs per model, not per arm, so it is larger, and an arm's agreement and kappa are split by model.Testing
On a Linux review host, at
2e11889(onmainb084e47):make ci-local: 30 passed, 0 failed, 7 skipped. Skipped:external-catalog-smoke(needs docker and network),test-latest,docs.yml:deployandpip-audit.yml:report(job conditions false on a PR),cli-surface / Package the result(an expression ci-local cannot evaluate),conda-forge-deps(no micromamba), and semgrep secrets (not installed). 13039 tests passed and 5 skipped; changed-line coverage againstmainwas 95% (606 lines measured, as at 6ac19e2: the docs commit adds no code line). The PR mergesmainthree times: a1b8822 (at ad734f5; its three conflicts were both sides adding an artifact kind toartifacts.py,runs/kinds.pyandtests/test_runs_scan.py), 6c52d10 (at 41b3d8d; theartifacts.pydocstring list) and c94831a (at b084e47; contract §9'srepo report --hitsbullet), each resolved by keeping both. After c94831a,git diff --stat origin/main HEADequals the--statof the PR's own commits against the previous base (19 files, the same lines), andscripts/cli_surface.py dumpleaves the mergeddocs/reference/cli-interface.jsonunchanged.This round (c94831a, 2e11889): a merge and a docs sentence, so no test changed and there are no new mutants.
test_docs_code_refschecks the names in the new sentence.Round of 5987484 and 6ac19e2. The two tables of the nineteenth and twentieth commits are a script run through the command at 16eded4 and at 6ac19e2. 5987484's new case,
test_pairs_in_the_response_cache_still_finish_after_an_unpriced_stop, passes at 16eded4 too, since the behaviour does not change. 6ac19e2's three new cases fail against 16eded4's code: two models with one labelled naming the other row (text and JSON), then both labelled giving[]; the sheet command drawing one pair per model; andsheet()taking its share from each model of an arm. Run against 16eded4's code, this round's test file fails 11 of its 71 cases; the other 8 are cases that readuncalibrated_rowsor a calibration entry'smodel.The tables above: each "at 05ebb9a", "at 86c37d3", "at af4c4a4" or "at 8e05118" row is what that commit prints on the test batch with the fake judge, and each row of the next commit is what that commit prints. All 14 new or changed cases of round two fail against 05ebb9a's code, the 2 new cases of af4c4a4 fail against 86c37d3's code, 3 of the 4 new cases of 38e1da9 fail against af4c4a4's code (the fourth,
--dry-runremoves nothing, holds there too, since af4c4a4 removes nothing), and the 3 new cases of 790c0c1 fail against 8e05118's code (each gets exit 0 and judge calls where it expects exit 2 and none). The "at 790c0c1" column of the last table is a script run on 790c0c1, and the "at b55c577" column is the same script on b55c577. Run against 790c0c1's code, this round's test file fails 31 of its 56 cases. That includes cases the round did not change, because the fake judge now finds the finding by its JSON line in the new prompt. So the evidence per comment is the mutants below. For the thirteenth commit (127f8e2), its new case fails against b55c577's code (exit 0 and 5 live calls where it expects exit 1 and 3); the "at b55c577" and "at 31837e0" columns of the table on 127f8e2 and 31837e0 are that case and a script run on one credited pair. For the fifteenth commit (2b71e36), its new case fails against 31837e0's code in all three of its forms (exit 0 and the label overwritten for the unreadable and the non-UTF-8 file; for the directory, exit 2 from the write but not the new line), and the last table is a script run on 31837e0 and on 2b71e36.tests/test_repo_hits_judge.py(71 cases, fake LLM, offline). New at 6ac19e2: in a batch whereguidance:noneis answered byclaude-sonnet-5-5andgpt-5.1, labels on the sonnet pairs only giveuncalibrated_rows: [{"arm": "guidance:none", "model": "gpt-5.1"}]and the line namingguidance:none (gpt-5.1), and a sheet with every pair labelled gives onecalibrationentry per row,uncalibrated_rows: []and no uncalibrated line;repo hits sheet --per-arm 1writes one pair per row, and its line counts them per row;sheet()draws 25 of an arm's 30 pairs of one model and all 3 of another. Changed at 6ac19e2: the cases that readuncalibrated_armsreaduncalibrated_rows, calibration entries carrymodel, and the text lines namearm (model). New at 5987484: the response cache filled forguidance:rawr1, an uncachedguidance:noner1 and an unpriced judge under--max-usd 5give exit 1, 1 live call,guidance:noner1 inbudget_stoppedwith the unpriced reason, andguidance:rawr1 written with no unjudged pair. New at 2b71e36: an existing--outfile whose read raisesPermissionError(Path.read_textmonkeypatched, so the case holds as root and on Windows, with writing still allowed), a directory, and a non-UTF-8 file each makerepo hits sheetexit 2 with nothing on stdout, thecannot read <path> to check it for labels (<why>)line, and the file's bytes unchanged (3 cases); the case that replaces an unlabelled file gains a readable file that is not JSON. New at 127f8e2: with the response cache on and empty and a judge that raisesRuntimeError: invalid api keyon every live call, a 5-pair batch stops after 3 live calls with exit 1 and the exact✗line (0 judge calls, $0.0000, 1 cell written before), and the cell in progress and the next cell get no file. 31837e0 changes no test: none pinned the old wording.New at b55c577:
recall_judged: null, named on theunjudged cellsline and inunjudged_cells, and left out of the arm's mean (0.333 from the other cell); an unparseable reply also makes its cell null (4226669);written: 3andguidance:raw0.3335 / 0.1665 over 2 cells, the cell's file has no pair andrecall_judged: 0.0(5af8d24);judge call failed: RuntimeError: ...and exit 0, and the same error from the second call on stops after 4 requests with exit 1, the exact✗line, the in-progress cell's stale file removed and the next cell not started (8567b5b);answer.txtmessage changed at the same index and place each leave the pair unjudged with the "changed since grading" error and no prompt carrying the new text, and a regrade makes it judged again (2 cases; 88700aa);Labels:line yields exactly four fence lines, a one-line JSON finding that decodes to the planted text, the line only JSON-escaped, and the rules after the blocks (d3a1b52);repo run's message, no call and no file;--dry-runexits 0 under it; a run after release judges; a lock path that is a directory exits 2 (b55c577).Changed: the first case checks the prompt's JSON lines in place of the plain lines, the fake judge finds the finding by its JSON line, and the round-7 sheet case regrades after changing the gold comment, since 88700aa now catches an unregraded change first. Earlier cases: the prompt carries a 396-character comment in full and the finding's
rule/evidence;recall_judgedand label counts on known labels; a cell with no credited pair is written and counted in the means; an unparseable reply isunjudgedafter its 3 draws; a judge that is an answer model (same id,claude-cli/sonnet, and aclaude-cli/haikuagainst ameta.jsonmodel) is refused with exit 2 and no call, with the same-model wording, while a haiku judge on a sonnet batch is taken; so is a judge that a cell'smodel_resolvednames, as a string or a list, inresult.jsonor inmeta.json(4 cases);--dry-runmakes no call and prices each pair;--max-usd 1at $0.40 a call makes 3 calls, leaves one cell without a file (removing a stale one) and exits 1; unpriced replies under--max-usd 0.50stop after 1 call with the reason, 5 calls with no cap, and a priced control stops at the cap; a missing text is reported and not judged; grader-failure and pre-matchescells are counted, not judged; kappa 0.531 and agreement 0.7 on a known 10-pair table;--labelsper arm (per arm and model since 6ac19e2), plus refusals of an unknown thread, an unknown cell, a non-object entry, a fourth label and two different labels for one pair, each with no judge call; an all-null sheet,[], and labels only on unjudged pairs print the uncalibrated line and give both arms' rows inuncalibrated_rows(uncalibrated_armsbefore 6ac19e2; text and JSON); labels on one arm name the other as uncalibrated (text and JSON); an entry whosefindingis 99, or that has none, exits 2 with no judge call; new in 790c0c1: a labelled sheet that calibrates is refused, exit 2 and no judge call, once the credited finding's text changes at the same index and once the gold comment changes, with the entry, thread, cell and changed text in the message (2 cases); a labelled entry with nofinding_textis refused, an entry withlabel: nulland an editedcommentis taken, and a labelled entry whose pair'sanswer.txtholds no findings any more is refused with that reason; the sheet is stratified, capped per arm, seeded, carries the full texts, refuses to replace a labelled sheet byte for byte and replaces an unlabelled one; the schema validates a written file and rejects a fourth label; a rerun with the response cache makes no call and writes the same file; a directory where a cell'shits_judged.jsongoes, and an earlier file whose removal raisesPermissionErrorwhen--max-usd 1stops its cell, each exit 2 with the path in the message, make no judge call after the failure (4 and 3 calls), and leave the cell written before in place; a judged cell regraded to a grader failure, and in another case to a grade with nomatches, loses itshits_judged.jsonon the next run while the other cell keeps its own, with the removal named in the text and instale_removed, and a third run reportsstale_removed: [](2 cases); a directory in place of that file exits 2 with the path, no judge call and the other cell's file kept;--dry-runremoves nothing.Guard and touched tests at 2e11889:
test_repo_hits_judge,test_docs_code_refs,test_growth_ratchet,test_audit_test_quality,test_import_architecture,test_cli_flags,test_cli_surface,test_runs_scan,test_runs_show,test_artifacts,test_schema_compat,test_artifact_fixtures: 676 passed, 1 skipped.ruff checkandmypyon the whole package pass insidemake ci-local.Mutant of 5987484: the replay skip removed from
Meter.ask(the stop checked insidereplay_only()too): the new case fails (the cached cell is inbudget_stoppedtoo), and the other 70 pass, the rerun-from-cache case among them.Mutants of 6ac19e2: a row counted as calibrated when any row of its arm is (the old per-arm coverage): the two-model case fails;
sheet()grouping by arm only: both sheet cases fail; the model dropped from the row names of the text: the two-model case and the one-arm case fail. Each of the other cases passes.Mutants of 2b71e36, each failing only the cases it should: any
OSErrorbut a missing file taken as no sheet again (the no-permission and directory cases fail); aUnicodeDecodeErrortaken as no sheet again (the non-UTF-8 case fails); a readable non-JSON file refused instead of replaced (the not-JSON case fails).Mutant of 127f8e2: the re-raise of
caching.CacheMissinMeter.askingremoved, so the probes' misses count as failed calls again: the new cache case fails, and the other 56 cases pass.Mutants of b55c577: 13 breaks in a copy of the tree, each failing its tests, and each of those tests passing unbroken:
load_hitsskipping parse-error cells again, and theirrecall_judgedleft to the rescore (null): the parse-error case fails for each;--dry-runtaking it: the lock case fails for each.The mutants of 790c0c1, 8e05118, 38e1da9, af4c4a4, 86c37d3 and 05ebb9a were run at those commits. d35787a, fefac63 and 16eded4 are another contributor's; their tests were not re-mutated this round.
Bad inputs run by hand (at 05ebb9a): a missing batch directory and a Windows path (
C:\Users\x\batch) exit 2 (✗ not a directory);--judgewithout a provider exits 2 listing the providers; a labels file that is not a JSON list, or missing, exits 2 naming the file; an empty batch dir gives0 of 0 credited pair(s)for--dry-runand an empty sheet.Not run: a real judge model (no paid call was made for this PR), and a real provider error; the failed-call cases use a fake judge that raises.
Merged together at
790c0c1with the branches for novelty: spread the atom cap across the artifact instead of its opening #293 (novelty cap spread,82d576a), repo compare: flag replay tasks a guidance revision may have seen #290's recording half (f6a9cb9) ontomain41b3d8d: the full suite (12645 passed, 5 skipped, 6 xfailed),mypy(318 files) andruff checkpass. Not yet run merged together at2b71e36.Revert
Revert the PR's twenty-one commits (05ebb9a, 86c37d3, af4c4a4, 38e1da9, 8e05118, 790c0c1, 4226669, 5af8d24, 8567b5b, 88700aa, d3a1b52, b55c577, 127f8e2, 31837e0, 2b71e36, d35787a, fefac63, 16eded4, 5987484, 6ac19e2, 2e11889); the three merges of
mainneed no revert. What they leave behind:hits_judged.jsonfiles in batch cells, which nothing else reads, and sheet files people wrote. A plain revert drops therepo_hits_judged.v1schema with no superseded entry, so remove those files if anything is to validate them later.Checklist
pytestpassesruff,mypy, and schema checks passAGENTS.md.github/.licenserc.yaml)