An eval harness, built as a personal learning project, that gates a deployment on a panel of three LLM judges and then measures the judges instead of trusting them. Three judges score a system under test against a four criterion rubric, two more run in shadow and never vote, and the gate exits 1 when the candidate's pass rate falls below a committed baseline. The part that carries the argument is the calibration layer underneath: per judge agreement with human labels, Cohen's kappa rather than raw agreement, self consistency across repeats, pairwise error correlation, and a refusal to gate at all on a threshold inside the judges' own measured noise.
The strongest thing in the repo is a demonstration of its own failure. A panel whose judges have been captured says ALLOW on a genuine, planted regression while comparing against a valid baseline recorded by that same panel, and the calibration layer is the only reason that build goes red. An uncalibrated harness merges that change with a green tick and a decisive looking ALLOW next to it.
It runs fully offline on five deterministic mock judges and switches to real
models (Anthropic and OpenAI) with two environment variables. Both real model
captures are in SAMPLE_RUN.md, verbatim: 460 live judge calls
across claude-opus-5, claude-sonnet-5 and gpt-5.6-terra with gpt-5.6-luna
and gpt-4o in shadow, plus a smaller second run. They found that the panel's
three judges made completely nested errors across two vendors, that the
cheapest judge tied the most expensive, and that this repo's own reduced-sweep
advice was wrong; all three are written up with their caveats.
Unless a number is explicitly labeled as coming from those captures, every figure in this README is from the offline mock panel, and no offline number here is evidence about any real model. The mock bench ranks the five models in nearly the opposite order to the one the live run measured, which is the point.
The rule this repo follows: no claim without a test. The table below maps each claim in this README to the test that enforces it.
An LLM judge is a measuring instrument, and nobody calibrates it. A team writes a rubric, wires a judge into CI, watches the pass rate, and ships on it. The questions that decide whether that pass rate means anything are never asked: does this judge agree with a human, does it agree with itself twice in a row, do three judges fail on the same cases, and is the threshold being enforced larger than the variance of the judges enforcing it.
The failure mode is not a noisy gate. It is a quiet one. A judge that passes everything agrees with the humans 80% of the time on this deliberately skewed golden set, so a harness reporting accuracy reports 0.800 and looks healthy while being structurally incapable of blocking anything. Cohen's kappa scores the same judge 0.000. That gap is the whole project.
Five controls, all implemented here:
- Per judge calibration against human labels. Agreement, kappa, a confusion matrix, and false pass and false fail reported separately against different denominators, because one accuracy number hides which of the two failure modes you have.
- A measured noise floor, and a refusal. Every case is judged K times.
claude-opus-5andclaude-sonnet-5rejecttemperature(HTTP 400), so judge variance cannot be configured away and has to be measured. When the regression threshold sits inside that variance the gate refuses to produce a verdict rather than producing one it cannot support. - Pairwise error correlation. Three judges cost 3x. If they are wrong on the same cases, majority voting bought less than it appears to, and the report says so with a ratio against what independence would predict.
- Vacuous gate detection. A golden set whose cases give both versions the
same verdict is not a gate.
cases_that_never_discriminate, panel kappa and panel false pass rate are ceilings the panel can fail, and a named suspicion line says which signal fired. - A shadow bench and a cost table. Two cheap judges are scored exactly like voting judges and are excluded from the vote, from unanimity, from split detection and from every gate threshold, so "would a cheaper judge have agreed with the humans just as often" is answerable without betting a release on the answer.
Every decision, verdict and gate outcome lands in a hash chained append only log, and a prompt registry binds prompt content hash to eval run id to gate outcome, so "which eval run approved the prompt that is live" is a lookup rather than an archaeology exercise.
The gate always runs against the deterministic mock panel. A regression gate
has to be reproducible: the golden set pins which judge disagrees with which
human on which case, and a live model moves those around between runs. Real model
behavior belongs in SAMPLE_RUN.md and never in a pass or fail signal for CI. A
gate whose verdict depends on the model of the day is a flaky test with a
governance story attached.
Five gate invocations, verified by exit code:
python -m eval_gate.evals.gate exit 0 ALLOW, panel healthy,
sut.v1 0.800 vs baseline 0.800
python -m eval_gate.evals.gate --candidate sut.v2 exit 1 BLOCK, regression,
0.533 vs 0.800, drop 0.267 > 0.150
python -m eval_gate.evals.gate --panel two_miscalibrated --candidate sut.v2 \
--baseline baseline.two_miscalibrated.json exit 1 ALLOW, but panel_healthy NO;
exit driven by calibration failure
python -m eval_gate.evals.gate --panel two_miscalibrated --candidate sut.v2
exit 1 NO DECISION (refused);
baseline not comparable
python -m eval_gate.evals.gate --threshold 0.05 exit 1 NO DECISION (refused);
inside the 0.100 noise floor
The third invocation is the one that matters. The captured panel passes every
case of both versions, so the measured pass rate is 1.000 against a recorded
1.000: a drop of exactly 0.000, regression_detected NO, deployment_decision
ALLOW, on a regression the human labels call real. The comparison is valid, not
rigged: a team whose panel was captured would have recorded its baseline with
that captured panel, which is why the second baseline file exists. Nothing in the
regression comparison notices, and nothing can, because the measurement it
consumes is worthless. Four calibration checks fail, and that is the only reason
the build is red.
What the offline honest panel measured, and what it refuses to overclaim:
per judge kappa against the human labels
mock-balanced (stands in for gpt-5.6-terra) 0.925 best single member
mock-strict (claude-opus-5) 0.857
mock-lenient (claude-sonnet-5) 0.842
shadow:mock-literalist (gpt-4o) 0.791 non voting
shadow:mock-verbosity (gpt-5.6-luna) 0.108 non voting
panel kappa 0.925, which is EXACTLY its best single member's
noise floor 0.100, measured over 3 repeats per case
- The panel did not beat a single judge. It scored 0.925, the same kappa as
mock-balancedalone to three decimal places, so the 3x spend bought nothing at all on this golden set, and the report prints that in capitals rather than leaving a+0.000in a table for someone to skim past. The claim here is not that a panel is better. It is that the harness measures whether the panel earned its cost and reports the answer either way, which is a better claim than a win would have been. - A cheaper judge came within noise of the dearest, on this golden set. The
claude-sonnet-5slot scored kappa 0.842 againstclaude-opus-5's 0.857, a gap of 0.015 against a measured noise floor of 0.100, so the extra spend is not buying measurable agreement here. Read that with honest limit 2 attached: 30 cases, no confidence interval, and a 0.015 gap is not a resolved finding. - The captured panel does not look unanimous, and that was worth measuring.
Unanimity falls to 0.733, BELOW the honest panel's 0.867, because the one
surviving honest judge still disagrees on the cases it fails. A heuristic keyed
on unanimity stays silent exactly when the panel is captured. The tells that do
fire are
cases_that_never_discriminatesaturating at 30/30, panel kappa collapsing to 0.000, and panel false pass rate hitting 1.000. - Abstentions do not vote, and ties do not resolve. A tie abstains and flags the case for a human, because a tie silently resolved toward pass is how a gate ships a regression while looking decisive.
- A shadow judge cannot change a gate outcome. That is structural rather than conventional: shadow verdicts carry a flag, travel on separately named fields of the run result, land in a separate list on the report, and aggregation raises if one ever reaches the vote.
- An unreadable judge is an abstention, never a fail. Coercing a parse failure into "fail" would attribute the harness's own bug to the system under test and block a release on evidence that does not exist.
- The gate refuses twice, for two different reasons, and says which. Once
when the threshold is inside the noise floor, once when the committed baseline
does not measure what this run measured.
exit_drivernames the condition, so a red build is diagnosable rather than merely red.
| Claim | Test |
|---|---|
| A captured panel says ALLOW on a genuine regression against a valid comparable baseline, and only the calibration layer turns the build red | tests/test_gate.py::test_two_miscalibrated_judges_capture_the_panel_and_only_calibration_turns_the_build_red (mutation-checked: strip the calibration checks and the same run exits 0 on the regression) |
| The capture is caught by the discrimination count saturating at 30/30, not by unanimity, and the suspicion line names which signal fired | tests/test_gate.py::test_cases_that_never_discriminate_is_reported_and_saturates_under_a_captured_panel, tests/test_calibration.py::test_the_suspicion_line_names_which_vacuous_gate_signal_fired (mutation-checked: revert the heuristic to its unanimity only form and it goes silent under total capture) |
| Raw agreement flatters a judge that cannot fail anything; kappa is what exposes it | tests/test_calibration.py::test_raw_agreement_flatters_a_judge_that_always_answers_pass (mutation-checked: read raw agreement instead of kappa against the same floor and the constant pass judge is admitted to the panel) |
| The report says so when the panel does not beat its best single member | tests/test_calibration.py::test_the_report_says_so_when_the_panel_does_not_beat_its_best_member |
| The honest panel gating its own committed baseline exits 0, ALLOW, 0.800 vs 0.800 | tests/test_gate.py::test_the_honest_panel_gating_its_own_committed_baseline_exits_zero |
| The planted regression is caught, blocked, and exits 1: 0.533 vs 0.800, a 0.267 drop past a 0.150 threshold | tests/test_gate.py::test_the_honest_panel_catches_the_sut_v2_regression_and_exits_one |
| A baseline recorded by a different panel produces no deployment decision at all | tests/test_gate.py::test_a_baseline_from_another_panel_produces_no_deployment_decision (mutation-checked: stub comparability to report no problems and the gate manufactures ALLOW from an incomparable baseline) |
| A threshold inside the measured noise floor is refused rather than enforced | tests/test_gate.py::test_a_threshold_inside_the_measured_noise_floor_is_refused (mutation-checked: bypass the noise floor guard and the same 0.050 threshold is accepted at exit 0 beside a printed 0.100 flip rate) |
| One miscalibrated judge is outvoted, the regression is still caught, and the bad judge is named in the same run | tests/test_gate.py::test_one_miscalibrated_judge_is_outvoted_and_the_gate_still_catches_it |
| Toggling the shadow bench off leaves every gate outcome and exit code identical, on all five invocations | tests/test_gate.py::test_toggling_shadow_judges_off_leaves_every_gate_outcome_and_exit_code_identical |
| A shadow verdict can never reach the vote, and the fields it travels on are not the fields the gate reads | tests/test_panel.py::test_a_shadow_judge_never_appears_in_the_panel_vote, ::test_shadow_results_travel_on_a_field_the_gate_never_reads, tests/test_gate.py::test_the_run_result_the_gate_is_handed_keeps_shadow_verdicts_off_every_voting_field |
| A tie abstains and flags the case for human escalation | tests/test_panel.py::test_a_tie_abstains_and_flags_the_case_for_human_escalation (mutation-checked: resolve ties toward pass and the escalation premise raises) |
| Two of three votes carries the panel, and the record keeps the disagreement it resolved | tests/test_panel.py::test_two_of_three_votes_carries_the_panel, ::test_the_panel_record_keeps_the_disagreement_it_resolved, ::test_the_panel_record_carries_the_case_it_judged |
| An abstaining judge does not vote, and a panel reduced to one voter abstains instead of reporting a verdict | tests/test_panel.py::test_an_abstaining_judge_does_not_vote, ::test_fewer_than_two_voting_judges_abstains_the_panel |
| No judges ran is an error, not a verdict | tests/test_panel.py::test_an_empty_panel_is_an_error_rather_than_a_verdict |
| Fenced or prose wrapped judge output still parses | tests/test_judge.py::test_a_verdict_wrapped_in_code_fences_is_still_parsed, ::test_a_verdict_with_prose_before_the_json_is_still_parsed |
| Unreadable judge output abstains instead of raising, and a verdict outside the contract is not coerced toward pass or fail | tests/test_judge.py::test_unparseable_output_becomes_an_abstention_rather_than_an_exception (mutation-checked: swap the tolerant parser for a strict json.loads and the judge call raises), ::test_a_verdict_outside_the_contract_becomes_an_abstention |
| A refusal or a content filtered response is an abstention, not a crash, in either vendor's spelling | tests/test_judge.py::test_a_refusal_stop_reason_becomes_an_abstention, ::test_a_content_filtered_openai_response_becomes_an_abstention |
| A broken judge and a cautious judge do not look the same in the report | tests/test_judge.py::test_an_abstention_from_a_broken_parse_carries_raw_ok_false |
| Two judges of the same kind do not merge into one row | tests/test_judge.py::test_a_judge_verdict_records_the_slot_it_was_asked_for |
| Cohen's kappa is 0.000 for chance level agreement and 1.000 for a perfect judge on a mixed label set | tests/test_calibration.py::test_cohens_kappa_is_zero_for_a_judge_that_agrees_only_by_chance, ::test_cohens_kappa_is_one_for_a_perfect_judge |
| False pass and false fail are reported separately, against different denominators | tests/test_calibration.py::test_false_pass_and_false_fail_are_reported_separately |
| The flip rate is 0.000 for a deterministic judge and rises for one that changes its mind, in the same unit as the gate's threshold | tests/test_calibration.py::test_flip_rate_is_zero_for_a_deterministic_judge, ::test_flip_rate_rises_for_a_judge_that_changes_its_mind |
| Correlated errors are reported as correlated, and independent ones as independent | tests/test_calibration.py::test_error_correlation_is_high_for_two_judges_with_the_same_bias, ::test_error_correlation_is_near_independence_for_unrelated_biases |
| An abstention is excluded from kappa and counted on its own | tests/test_calibration.py::test_an_abstention_is_excluded_from_kappa_and_counted_on_its_own |
| Provider selection needs both the name and the credential; a key alone selects nothing and keys never cross match | tests/test_llm.py::test_provider_name_without_a_key_falls_back_to_mock, ::test_a_key_without_the_matching_provider_name_stays_on_mock, ::test_keys_do_not_cross_match_between_providers, ::test_the_default_provider_is_the_offline_mock, ::test_the_offline_settings_carry_no_api_keys |
| A partially real panel is recorded as degraded, and a fully credentialed one is not | tests/test_llm.py::test_a_partially_real_panel_is_recorded_as_degraded (mutation-checked: make is_mock always answer False and the degraded flag disappears), ::test_a_panel_with_both_credentials_is_not_flagged_as_degraded |
| The three offline judges genuinely disagree, and each one both passes and fails somewhere | tests/test_llm.py::test_an_offline_panel_holds_three_distinct_judges_rather_than_three_clones, ::test_the_three_offline_judges_do_not_all_agree_on_every_case, ::test_each_offline_judge_both_passes_and_fails_somewhere |
| The offline shadow bench is the size its configuration asks for, and every slot is a genuinely distinct behavior | tests/test_llm.py::test_the_offline_shadow_bench_is_the_size_its_configuration_asks_for (mutation-checked: restore the or shadow_mock_judges() fallback, or slice the bench to a fixed two, and it fails), ::test_every_offline_shadow_judge_is_a_genuinely_distinct_behavior (mutation-checked: pad the bench with a repeat of the first behavior and distinctness stops holding), ::test_the_shadow_bench_falls_back_to_mocks_without_a_credential |
| A three judge panel bills three times, and the non voting shadow bench bills too | tests/test_cost.py::test_the_panel_costs_more_than_a_single_judge, ::test_the_shadow_bench_is_billed_even_though_it_does_not_vote |
| Every model a run could bill against has a price entry, and every offline mock prices against the real model it stands in for | tests/test_cost.py::test_the_cost_table_covers_every_configured_judge_model (mutation-checked: delete gpt-5.6-terra from PRICES and the coverage premise stops holding), ::test_every_offline_mock_judge_prices_against_a_real_stand_in_model |
| A model with no price entry raises rather than costing nothing | tests/test_cost.py::test_an_unknown_model_refuses_to_be_priced_at_zero (mutation-checked: restore the PRICES.get(model, (0.00, 0.00)) fallback in price_for and it fails) |
| An unpriced judge prints NO PRICE ON FILE with no dollar figure, and never wins the cheapest comparison | tests/test_cost.py::test_a_report_row_for_an_unpriced_judge_says_no_price_on_file_not_zero_dollars (mutation-checked: give the unknown model a (0.00, 0.00) entry and the marker becomes a figure that reads as free), ::test_the_cost_accuracy_table_never_calls_an_unpriced_judge_the_cheapest (mutation-checked: price the unknown model at zero and it becomes the cheapest row) |
| A dated introductory price expires on its stated date rather than on a default that stays true | tests/test_cost.py::test_the_sonnet_introductory_price_expires_on_its_stated_date (mutation-checked: change the sonnet_intro default back to True and it fails) |
| Pricing more judges than there are slots raises instead of wrapping around and billing the wrong models | tests/test_cost.py::test_pricing_more_judges_than_slots_is_an_error_not_a_wraparound (mutation-checked: restore the modulo index in priced_as_real and it fails) |
| Offline token counts are labeled as the characters/4 approximation they are | tests/test_cost.py::test_an_offline_cost_report_labels_its_token_counts_as_approximate |
| Two offline runs produce identical verdicts and identical calibration numbers | tests/test_determinism.py::test_two_offline_runs_produce_identical_verdicts, ::test_two_offline_runs_produce_identical_calibration_numbers |
| The run id is derived from what the run is rather than from a clock | tests/test_determinism.py::test_the_run_id_is_derived_from_the_run_rather_than_from_a_clock |
| Two runs produce the same audit trail apart from its timestamps, which are excluded rather than frozen | tests/test_determinism.py::test_two_offline_runs_produce_the_same_audit_trail_apart_from_its_timestamps, tests/test_audit.py::test_the_audit_hash_is_stable_across_key_order |
| The golden set skew the whole kappa argument depends on cannot be quietly balanced away | tests/test_determinism.py::test_the_golden_set_label_distribution_is_deliberately_skewed |
| Editing, reordering or removing an audit record is detectable | tests/test_audit.py::test_the_audit_chain_detects_a_tampered_record (mutation-checked: drop prev_hash from the hashed payload and an excised record goes undetected) |
| A CI gate run writes no records into the committed audit directory | tests/test_gate.py::test_the_gate_writes_no_audit_records_into_the_committed_audit_directory |
| A prompt version is identified by the hash of its body, so editing it in place does not inherit the old approval | tests/test_registry.py::test_a_prompt_version_is_identified_by_its_content_hash, ::test_editing_a_prompt_produces_a_new_version |
| A rollback is appended to the history rather than popped from it, and is itself a recorded decision with an actor and a reason | tests/test_registry.py::test_a_rollback_is_recorded_rather_than_erased (mutation-checked: restore history.pop() in PromptRegistry.rollback and it fails), ::test_rollback_restores_the_previous_active_version |
| Rolling back does not launder an ungated version into an approved one | tests/test_registry.py::test_a_rollback_to_a_version_no_green_gate_approved_says_so |
| Which eval run gated which version is a lookup, and a consciously accepted regression is not recorded as an approval | tests/test_registry.py::test_the_registry_records_which_eval_run_gated_which_version |
| Recording a baseline is refused when the panel is unhealthy, when the run was refused, or when the audit chain does not verify | tests/test_registry.py::test_record_baseline_is_refused_when_the_panel_is_unhealthy, ::test_record_baseline_is_refused_when_the_run_itself_was_refused, ::test_record_baseline_is_refused_when_the_audit_chain_is_broken |
| A fresh checkout takes its active version from the committed baseline record instead of the highest version on disk | tests/test_registry.py::test_a_fresh_checkout_takes_its_active_version_from_the_committed_baseline_record |
| Nothing the real model pass measures can reach a gate decision, and the demo's exit code is identical with it running and with it skipped | tests/test_real_pass.py::test_nothing_the_real_pass_measures_can_reach_a_gate_decision (mutation executed in-test: the same report handed to evaluate_gate fails four calibration checks), ::test_the_demo_exit_code_is_the_same_with_the_real_pass_running_as_without_it (mutation-checked: return non-zero when the pass records an incident and the two exit codes diverge) |
| A real judge that errors, refuses or times out becomes a recorded abstention and the paid run carries on | tests/test_real_pass.py::test_a_judge_that_raises_becomes_a_recorded_abstention_rather_than_an_exception (mutation-checked: let the exception out of GuardedJudge.complete and the run dies mid pass), ::test_a_call_that_raised_is_not_billed_for_tokens_it_never_consumed |
| The call count and dollar estimate printed before the first call are the arithmetic of the pass that follows, and computing them costs nothing | tests/test_real_pass.py::test_the_call_count_arithmetic_is_the_calls_the_judges_actually_receive (asserted against each judge's own call log), ::test_the_dollar_estimate_is_priced_arithmetic_and_never_costs_a_network_call (mutation-checked: drop approximate_only and the preflight calls count_tokens 300 times to price a 460 call run) |
| A reduced sweep buys cases that separate the two sut versions, in both directions, and the pass sends exactly those cases | tests/test_real_pass.py::test_a_reduced_sweep_of_discriminating_cases_separates_the_two_sut_versions (mutation executed in-test: the same size taken as a prefix is gc-001..gc-008, which pass in both versions and separate nothing), ::test_the_plans_case_selection_is_the_cases_the_judges_are_actually_sent (asserted against each judge's own call log, and the printed heading lists the ids rather than a range that skips cases) |
| Asking for more cases than a selection holds raises rather than quietly measuring fewer | tests/test_real_pass.py::test_asking_for_more_cases_than_the_golden_set_holds_raises_rather_than_measuring_fewer |
| Repeat 1 of a consistency case is bought once and read by both passes | tests/test_real_pass.py::test_repeat_one_of_a_consistency_case_is_bought_once_and_read_twice (mutation-checked: drop first_repeat and every consistency unit is scored at attempt 1 twice) |
| Real token counts come from the vendor's own accounting, and a slot that reports none is labeled approximate rather than measured | tests/test_real_pass.py::test_the_measured_tokens_come_from_the_vendors_own_accounting, ::test_a_slot_that_reports_no_usage_is_counted_by_approximation_and_says_so (mutation-checked: keep exact true with no usage block and a guess is printed as a measurement) |
| A cost versus accuracy table on real numbers refuses to mix measured dollars with the offline estimate | tests/test_real_pass.py::test_a_measured_cost_table_refuses_to_mix_measured_dollars_with_the_estimate (mutation-checked: fall back to the estimate for an unmeasured judge and the cheapest row is decided by a guess) |
The panel degraded header distinguishes an all real panel from an all mock one from a panel that fell back, and names the slot |
tests/test_real_pass.py::test_the_panel_degraded_header_distinguishes_all_three_panels (mutation-checked: print the bare panel_degraded boolean and a two vendor panel and an offline panel read identically), ::test_the_real_section_names_which_slots_were_real |
| The real model section is skipped offline and prints the call count the paid run would actually make | tests/test_real_pass.py::test_the_real_model_section_is_skipped_offline_and_makes_no_judge_call, ::test_the_skipped_line_quotes_the_call_count_the_paid_run_would_actually_make (mutation-checked: hardcode 460 and a differently sized plan still prints it) |
Requires Python 3.11 or newer. CI runs 3.11, 3.12, and 3.13.
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
pytest -q # 98 tests, fully offline
python -m eval_gate.evals.gate # the CI eval gate
python scripts/run_demo.py # end to end demo over the whole golden set
The four other gate invocations, which are the demonstrations:
python -m eval_gate.evals.gate --candidate sut.v2
python -m eval_gate.evals.gate --panel two_miscalibrated --candidate sut.v2 \
--baseline baseline.two_miscalibrated.json
python -m eval_gate.evals.gate --panel two_miscalibrated --candidate sut.v2
python -m eval_gate.evals.gate --threshold 0.05
SAMPLE_RUN.md holds a verbatim capture of the demo and of all five gate
invocations with their exit codes.
golden set (30 synthetic cases, human labeled, 24 pass / 6 fail on sut.v1)
|
v
two sut versions scored in the same pass a drop between them cannot be an
| artifact of two panels or two days
v
3 voting judges x 30 cases x 2 versions x 3 repeats = 540 judged units
+ 2 shadow judges on separate fields the gate never reads
|
v
tolerant parser fences and prose stripped; a refusal, a content filter
| or an unreadable body becomes abstain, never fail
v
panel aggregation abstentions do not vote; a tie abstains and escalates;
| a shadow verdict raises rather than being counted
v
calibration kappa per judge and for the panel, confusion matrix,
| flip rate per judge, pairwise error correlation,
| cases_that_never_discriminate, unanimity, cost
v
refuse? threshold inside the measured noise floor, --> NO DECISION
| or a baseline that measured something else exit 1
v
regression? candidate pass rate vs the COMMITTED baseline --> BLOCK
| record in baseline.json exit 1
v
panel healthy? kappa floors, false pass ceiling, abstention --> exit 1
| ceiling, vacuous gate ceilings ALLOW is
v still printed
exit 0 only when the panel is healthy AND nothing regressed
|
v
hash chained audit record + registry binding (prompt hash, run id, outcome)
Two things about that shape are deliberate. The comparison is against a file in
version control rather than against sut.v1 measured in the same run: the golden
set carries a planted regression, so an in run comparison would report one every
time and CI would be permanently red, while comparing the active version against
itself is a gate that can never fail. And the exit code follows both questions,
because an earlier version of this module printed deployment_decision BLOCK and
then exited 0, so CI went green while the harness said block.
The harness switches off the offline mock when AGENT_PROVIDER is set to
anthropic or openai and the matching API key is present:
ENV_FILE=~/.secrets/ai.env AGENT_PROVIDER=anthropic python scripts/run_demo.py
ENV_FILE=~/.secrets/ai.env AGENT_PROVIDER=openai python scripts/run_demo.py
ENV_FILE=~/.secrets/ai.env AGENT_PROVIDER=openai OPENAI_MODEL=gpt-5.6-terra python scripts/run_demo.py
Selection requires both the provider name and its credential; anything else falls
back to the mock, so the tests, the demo and the gate never touch the network.
Both SDKs are imported lazily and are not loaded at all in mock mode. Credentials
come from the environment, a gitignored .env, or an ENV_FILE pointing at a
private file outside the repository.
With a provider configured, the demo runs one extra section and only one:
section 11, the real model measurement pass. It is the only section that
calls a live model and the only section whose numbers can never reach an exit
code. Sections 4 through 10 stay on the deterministic mock panel whatever
AGENT_PROVIDER says, because a regression gate has to be reproducible and a
live model moves its own numbers between runs.
The pass goes through the same judge_case and run_panel seam the offline
sections use, so there is no second measurement path that only real models take.
It sweeps the whole 30 case golden set once for both sut versions (a kappa over
60 judgments is worth quoting; one over ten cases is not), then re-scores the
first 8 cases by case id at three repeats to measure a real flip rate. Repeat 1
of those cases is reused from the first sweep rather than bought twice. That is
460 API calls across the three voting and two shadow judges, and the run prints
that count plus a dollar estimate before it makes the first call;
--real-cases, --real-repeat-cases and --real-repeats size it down.
A reduced sweep needs --real-case-selection discriminating, and that flag
exists because a paid run proved it. The first eight cases by id are grounded
and pass in both sut versions, so an 8 case prefix measured kappa 1.000 for every
judge and a 100% pass rate on both versions: a capture of the project's own
cases_that_never_discriminate warning rather than a result. The discriminating
selection takes only cases the two versions are labeled differently on, and
alternates the two directions so the subset holds both a regression and a fix
instead of eight failures pointing the same way. A judge that answers "fail" to
everything cannot score well on it.
A judge that errors, refuses or times out is recorded as an abstention with the reason and the pass carries on, because aborting call 401 of 460 throws away a paid measurement. Tokens and dollars are reported from the vendor's own usage accounting, next to the offline characters/4 estimate for the same calls, so the capture shows how far the approximation was off.
The voting slots default to claude-opus-5, claude-sonnet-5 and
gpt-5.6-terra; the shadow slots to gpt-5.6-luna and gpt-4o. Two Anthropic
slots and one OpenAI slot is a genuine three model, two vendor panel only when
both keys are present; with one vendor's key the third slot falls back to a mock,
and the panel degraded header says so and names the slot rather than printing a
boolean that reads the same for an all real panel and an all mock one. Two models from
one vendor are expected to correlate, which is exactly why the report measures
pairwise error correlation instead of assuming independence. claude-opus-5 and
claude-sonnet-5 reject temperature, top_p and top_k with HTTP 400, so
there is no determinism knob to set and JUDGE_REPEATS is how the variance gets
measured instead.
Two captures, 460 and 160 live calls, $1.5576 and $0.5320 measured. Full output and the caveat on each finding are in SAMPLE_RUN.md; the short version is that most of what they found is a negative result about this design:
- The three voting judges' errors were completely nested, across two vendors.
claude-opus-5andgpt-5.6-terrawere wrong on identically the same four of 60 units, andclaude-sonnet-5's three errors were a subset of those four. No judge was ever wrong alone. Majority voting cannot correct an error all three make, which is why the panel scored kappa 0.857, exactly its two weaker members', and lost toclaude-sonnet-5alone at 0.892 while costing 1.61x. Read it with the caveat: exact nesting, over four errors. - The cheapest judge tied the most expensive.
gpt-5.6-lunaat $0.0952 per sweep scored the same kappa asclaude-opus-5at $0.8130, and the best judge was the mid-pricedclaude-sonnet-5. The offline mock bench ranks those models in nearly the opposite order, which is the clearest possible demonstration that no offline number here is evidence about a real model. - The measured noise floor was 0.000 on both runs, including on a subset chosen to be harder, so the flip rate the gate refuses to go below is a real measurement rather than an artifact of easy cases. The non voting shadows did flip, up to 0.125.
characters/4undercounts Anthropic prompts by 2.35x and is accurate on OpenAI, for a total estimate to measured ratio of 1.51 reproduced across both runs. That is why the offline dollar figure is labeled an approximation everywhere it appears.
Every model id and price in eval_gate/cost.py was checked against the vendor's
own documentation and against the account's model list (client.models.list()
on the OpenAI SDK), not against a search result. While these defaults were being
chosen, four aggregator pages returned four mutually contradictory price tables,
and the one that was wrong about prices was right about the naming. That is the
trap: a source which is neither reliably right nor reliably wrong is not evidence
in either direction. An account's own model list cannot be out of date or SEO
poisoned, so it is the authority here. price_for raises rather than defaulting,
so a model that is renamed or added without a price entry fails loudly instead of
being reported as free.
- The human labels are synthetic, which caps every calibration claim. The 30 golden cases carry labels written by the author to look like plausible human judgment, including four tagged ambiguous where reasonable reviewers would disagree. They were not collected from annotators, so there is no measured inter annotator agreement. This is the ceiling on the whole project: you cannot calibrate a judge better than the labels you calibrate it against. A kappa of 0.857 means the judge agrees with one author's opinion, not with human consensus. Read every number in the calibration report as "agreement with this golden set", never as "accuracy". A real version needs two or more independent annotators per case, a published inter annotator kappa, and adjudication of the cases they split on, which is a data collection project rather than a code change.
- Kappa on 30 cases has no confidence interval, and small gaps are noise. The
harness prints kappa to three decimals and does not bootstrap an interval. On
30 cases that interval is wide. The headline cost finding, that
claude-sonnet-5at 0.842 is within noise ofclaude-opus-5at 0.857, rests on a 0.015 gap an interval would very likely swallow whole. The report does compare that gap against the measured noise floor rather than treating it as meaningful, which is the honest half; quoting three decimals as if they were resolved would be the dishonest half. Read the leading digit. Worth doing: bootstrap an interval and print kappa as one. Until then, treat any kappa difference smaller than the noise floor as absent. - The offline judges are hand written mocks, not recorded real responses. The
gate, the tests and every number in the offline path come from five
deterministic mock judges with deliberately different biases. They exist so the
gate is reproducible, because a live model moves verdicts around between runs
and a regression gate that flakes is a governance story attached to a coin
flip. They do not show that real judges behave similarly. They show that the
harness measures whatever judges it is given. The real model evidence lives in
SAMPLE_RUN.mdand nowhere else, and the two must never be conflated. - One domain, one rubric, 30 cases. Synthetic grounded answer cases over synthetic Acme source snippets, judged against a four criterion rubric. Nothing here shows the design transfers to summarization, code review, tool use traces or open ended writing, where "is this answer grounded in the source" is not the question. A different domain needs a different rubric and a fresh calibration run, and the second is the expensive part.
- The noise floor is estimated from three repeats. The flip rate the gate refuses to go below is measured over K=3 repeats per case. Three is enough to prove the mechanism and thin as an estimate of a rate. A serious deployment would use more repeats and would recompute the floor whenever the judge model or the prompt changes, because both move it.
- The gate compares one aggregate number. Regression detection is a pass rate comparison against a committed baseline. It will not notice a change that trades one class of failure for another at a constant pass rate, and it does no per slice analysis: a candidate that fixes five grounded cases and breaks five ambiguous ones scores as no change. Slice level gating is a seam rather than an oversight, and the golden set already carries the tags that would drive it.
- Different models may judge different domains better, and that is unstudied. The panel is three fixed judges plus two shadow judges. The repo measures how well each agrees with the golden set and how correlated their errors are. It does not search for the best judge per domain, and the shadow bench is a comparison rather than a selection procedure. Choosing judges per domain on measured agreement is the obvious next project and is deliberately out of scope.
- On this golden set, the cases that separate the two sut VERSIONS and the
cases that separate the JUDGES are disjoint, so a reduced sweep cannot measure
both. The twelve cases the versions are labeled differently on are the
blatant ones, invented figures and outright deferrals, and every real model
scored kappa 1.000 on a sample of them. The cases judges actually get wrong are
the four tagged ambiguous, where the rounding is arguable, and those are
labeled the same in both versions so they can never enter a discriminating
subset.
--real-case-selection discriminatingfixes a reduced sweep that measured no version signal at all; it does not and cannot fix this. A reduced sweep is not a substitute for the full 30, and a golden set with ambiguous cases on both sides of a version change would be the real fix. - The shadow bench and the pricing slots disagree about size. The offline
shadow bench supports up to four distinct behaviors; the real shadow slots are
a hardcoded pair. Configuring four shadow judges therefore raises
PricingSlotMismatchrather than mispricing them silently. That is the intended loud failure and it is still an inconsistency: the two limits should be the same number. - Offline dollar figures are approximations. Offline token counts are
characters divided by four, priced against the models each mock stands in for,
and are labeled as such in the output. Real token counts come from the
provider's own usage accounting (and
count_tokenson the Anthropic side, nevertiktoken), and appear only in section 11 of a real run and inSAMPLE_RUN.md. That section prints both numbers side by side so the size of the gap is on the record. Anyone quoting an offline dollar figure as a real cost is quoting an estimate of an estimate. - What is deliberately excluded from the determinism claim. Timestamps and run ids differ per run and are excluded rather than frozen; audit hashing is made stable by canonical JSON, not by faking the clock. Two audit record counts are pinned as literals in the tests (62 for a gate run, 61 for a bare run), which is brittle on purpose: if the golden set size changes, those two assertions are the first thing to fail, which is the intended alarm.
This is a learning project, deliberately small. There is no HTTP API, no authentication, no persistence beyond the log files, no annotator tooling, one golden set, one rubric, and one worked domain. The mock judges stand in for a real panel and the labels stand in for real annotators. Those are seams, not oversights. Sibling repos follow the same discipline of claims mapped to tests, mutation checks on the tests that matter, and behavior verified before publishing: citation-abstention-rag, least-privilege-agent, agentic-review-gate, typed-agent-service, temporal-multi-agent.
MIT