Repository navigation
feat(repo): judged recall over the credited replay hits - #357
antonwolfy wants to merge 8 commits into
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'", "'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'", "'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 4 selected item(s). 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.29% 94.25% -0.05%
==========================================
Files 308 311 +3
Lines 39322 39830 +508
Branches 6651 6718 +67
==========================================
+ Hits 37079 37540 +461
- Misses 1430 1462 +32
- Partials 813 828 +15
🚀 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
|


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, such a reply now stops the run after that call, asnovelty's cap does: a cap stop (exit 1) naming the judge model and saying--max-usdcannot be enforced;--jsonaddsbudget_stop. 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 commentcalibrationChanges
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 (no single task directory, no such thread, no finding at the index, or a file other than the onematchesnames) is reported and leftunjudged. One call per pair goes throughdokimi.judged.judged_reply(redraws, response cache) and asks for{"label": "same point" | "related" | "unrelated", "reason": ...}. A reply that never parses isunjudged.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. A rerun rewrites it, 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); 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.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).recallandrecall_judged.--jsonprints the same summary, plusuncalibrated_arms(86c37d3): the arms ofrowswith no pair labelled by both the person and the judge, which is every arm 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 (default 25; all of an arm's pairs when it has fewer), drawn withrandom.Random(seed). An--outfile that holds a set label is refused (exit 2), not replaced (8e05118).--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 (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 arms are covered, a line names the others (recall_judged is uncalibrated in guidance:raw: ...); 86c37d3.load_hitsalso returnsgrader_failuresandcells(each replay cell whosematcheswas read, with or without a pair, plus itsn_threadsandrecall). Withoutcells, a cell with no credited pair 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);docs/architecture.mdmodule entry.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.jsonand the--outsheet. A judge call that raises (a provider or credential error) is not caught: the command stops with the error, and the cells already judged keep their files. That is how the CLI's top level treats a provider error for every command: it turns only input errors into an exit 2. 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.Testing
On a Linux review host, at
790c0c1(onmain41b3d8d):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). 12596 tests passed, 5 skipped and 6 xfailed; changed-line coverage againstmainwas 94% (511 lines measured; every changed line of 790c0c1 is covered). The PR mergesmaintwice: a1b8822 (at ad734f5; its three conflicts were both sides adding an artifact kind toartifacts.py,runs/kinds.pyandtests/test_runs_scan.py) and 6c52d10 (at 41b3d8d; theartifacts.pydocstring list), each resolved by keeping both.--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).tests/test_repo_hits_judge.py(49 cases, fake LLM, offline): the prompt carries a 396-character comment in full and the finding'srule/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, 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 giveuncalibrated_armsof both arms (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.test_repo_hits_judge,test_repo_report_hits: 61 passed;test_docs_code_refs,test_growth_ratchet,test_audit_test_quality,test_import_architecture: 292 passed.ruff checkis clean andmypypasses on the whole package (317 files).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.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.Revert
Revert the PR's six commits (05ebb9a, 86c37d3, af4c4a4, 38e1da9, 8e05118, 790c0c1); the two 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)