Skip to content

Spec: blind pairwise grading of process Trials by a Grader panel and people - #1944

Merged
JacobStephens2 merged 28 commits into
mainfrom
issue-1872
Nov 10, 2026
Merged

JacobStephens2 merged 28 commits into
mainfrom
issue-1872

Conversation

@JacobStephens2

@JacobStephens2 JacobStephens2 commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

Blind pairwise grading of each Baseline–Candidate pair of process Trials (Baseline Trial k with Candidate Trial k), by a Grader panel, by people, or both. The Bar fixes who decides before the first Trial (ADR 0028).

thirdshift experiment <candidate>
  cost::confirm             Arms' estimate, then per panel seat: sessions × frozen price      (#1884)
  workers::run              Trials, each with its code Grader Score
  pairs::run                per finished pair: base-tests + cross-tests, offline in the       (#1881)
                              code-Grader sandbox; each side's diff kept → pairs/<pair>.json
  model_grader::run         review Scores; Grader chosen by the family rule
  panel::run                pair × seat × order 1,2: blind packet → calls.json               (#1882)
                              stops at --panel-spending-limit (per run) and pauses           (#1884)
  refresh_verdict
    deciders::decide        who decides each judged pair under ADR 0028                       (#1886)
    grading::assess         Trial passes = code Grader passes && decider calls it ready
                              (counted modes only; statistics unchanged)

Console /experiments/<id>/pairs/<pair>   ⇄  thirdshift experiments <id> --pair <pair>      (#1885)
  Ticket · checklist · Change A | Change B (code before tests, evidence) — no Arm, no model call
  POST merge (sealed per side) → POST preference [+ rubric] → reveal Arms, panel, other readers
  append-only reviews/<pair>/<n>.json, attributed to the reader

How the Verdict is decided for each pair:

panel call  = per seat: orders agree ? call : tie
              ballots per family (default) | per Model; arm-family seats sit out while another family votes
              strict majority, else tie; tied merge → not ready
pair given to readers?  → people's call (ready only if every reader says so); not yet called → waiting_on_people
else panel counted?     → the panel's call stands in
else (people-only sample) → the pair is left out; each sampled pair of a process suite is one unit
both counted and people–panel agreement on the sample < --panel-agreement → inconclusive (says why)
  • Bar flags: --panel-seat harness=…,model=…[,effort=…], --panel-vote family|model, --panel-arm-family-rule on|off, --panel off|beside|counted, --panel-families, --panel-agreement, --panel-spending-limit, --people off|beside|counted, --people-reader, --people-sample and --people-overlap. The Console's start options take the same flags.
  • Providers (Allow Fireworks and Baseten through opencode, with a key per provider #1883):
    • The sandbox proxy allows api.fireworks.ai and inference.baseten.co.
    • OpenCode takes a key per provider under [opencode.keys], falling back to api_key.
  • Result format 10: adds pairs, panel, panel_spend, people, bar.panel, bar.people, deciders, PairPage and the waiting_on_people state.
  • Docs: the guide, docs/experiment-results.md, docs/eval-tasks.md, docs/trial.md, the glossary, code navigation, the Panel Grader prompt and the three hand-written site pages.

This review's fixes (760fcbba)

 grading::evaluate
-  unit = sample_only ? 1 : trials                         # every suite
+  unit = sample_only && suite has process tasks ? 1 : trials
 pairwise::page
   sides drawn from the seed
+  patch  = blind_diff(patch)                              # as the panel's packet
+  error  = "the evidence could not run"                   # git errors name trials/<hash>--baseline--…
 model_grader::prepare
-  first configured Model whose name differs from both Arms'
+  first configured Model from a family neither Arm runs on (panel::arm_families)
 panel::Sides::read
-  fs::read_to_string(task.directory.join(source))
+  pairwise::ticket(task)                                  # stays inside the task folder

The review also fixed two pieces of text. panel.rs and docs/experiment-results.md no longer say the panel is "never in" the Verdict. The guide's panel examples now seat Kimi through the Kimi Code Harness rather than OpenCode, as the Spec's Q6.4 says.

Evidence

Execution evidence

All three behaviour fixes started from the reviewer's failing test.

Spec P4: a sample-only Verdict resets every suite's statistics.

  • Test: cargo test --bin thirdshift deciders::tests::a_sample_only_verdict_leaves_other_suites_statistics_alone
  • Before:
    assertion `left == right` failed: {… "review":{… "bar":{… "trials_per_task":1}, … "retained_tasks":12 …}
      left: Number(1)
     right: 3
    
  • After: test result: ok. 10 passed; 0 failed (all of deciders::tests).

Spec P1: the blind page shows identifying lines and error text.

  • Test: cargo test --test console pairwise::identifying_lines_and_paths_stay_off_the_blind_page
  • Before: the test panicked at tests/console/pairwise.rs:613. The page contained opus-fixture and unavailable: other Arm: Eval git failed: fatal: /h/trials/abc--baseline--0/attempt-1/repository.
  • After: cargo test --test console pairwise → test result: ok. 8 passed; 0 failed.

Spec P6: the single model Grader excludes by exact Model name.

  • Test: cargo test --test experiments model_graders::the_model_grader_comes_from_another_family_than_either_arm
  • Before:
    assertion `left == right` failed: {"effort":null,"harness":{"name":"claude",…},"model":"judge-claude"}
      left: String("judge-claude")
     right: "judge-gpt"
    
  • After: cargo test --test experiments model_graders → test result: ok. 14 passed; 0 failed.

Final validation: scripts/affected-tests refs/remotes/origin/main.

  • Passed: npm test --prefix site, cargo build --locked and npm test --prefix console.
  • cargo test had two failures, both in tests/process.rs findings::*, which this branch doesn't touch. They failed under the full run's load. Re-run alone, cargo test --test process findings:: → test result: ok. 18 passed; 0 failed.
  • Regeneration checks: cargo test --bin thirdshift prompts_page → 14 passed. npm run docs:check --prefix site → exit 0.

Visual evidence

These captures were published by the Ticket PRs and are reused here. This review's fixes change only which diff lines and error text the blind page prints, not its layout.

Blind pair page, 1440×900, before any call (#1885). Before: the route doesn't exist.

Before
After

Blind pair page, 390×844 (#1885).

Before
After

Experiment page, People's calls with counted readers' queues, 1440×900 (#1886).

Before
After

The Pair evidence table (#1881) has no published capture: that session couldn't upload one.

Seams under test

  • thirdshift experiment candidate --json through the Experiment fixture, with fake Harness scripts as panel Graders (tests/experiments/{pairs,panel,model_graders}.rs).
  • Console HTTP responses for the pair page, its POSTs and the Experiment page (tests/console/{pairwise,experiments}.rs).
  • deciders::decide with grading::assess, for Verdicts under each Bar setting (src/evals/experiment/deciders/tests.rs).

Review coverage

Reviewed with thirdshift-code-review against refs/remotes/origin/main. Standards raised 12 findings and Spec raised 11. Every changed file was read by both axes, apart from generated output:

  • prompts/panel-grader.md is excluded from both axes. Its source is src/prompt.rs, and cargo test --bin thirdshift prompts_page passes.
  • site/docs/index.html is excluded from both axes. Its source is docs/guide.md, and npm run docs:check --prefix site passes.

Unaddressed findings

Standards

  • S1. Credentials::read_from is pub(crate) for a test. Declined, as on Allow Fireworks and Baseten through OpenCode, with a key per provider #1942: the sandbox tests build OpenCodeKeys through the real Credentials parser (src/sandbox.rs:1993), not through a constructor that bypasses it.
  • S3. Test deadline. The reviewer judged it compliant. The doc comment in tests/experiments/pairs.rs names what the 4s covers. No change.
  • S4. Three Call types, two Preference types and two rubrics (Mysterious Name). The rubric and result shapes are open in Align the people's blind-page rubric and panel result shape with the Grader panel (#1882) #1947. Renaming before that decision would churn the same types twice.
  • S5. Repeated pair walks (Duplicated Code). Declined, as on Estimate and cap the Grader panel's spend (#1884) #1951: panel::remaining must count unfinished pairs before any Trial, while panel::run skips them (src/evals/experiment/panel.rs, remaining/run).
  • S6, the part about the git diff argument vector. The two call sites differ in purpose. pairs::diff splits the diff per file and keeps it; panel::Sides::read blinds the whole patch. The Ticket-read part of S6 is fixed.
  • S7. Three seeded draws. Declined. Each draw answers a different question: the panel's first order, the reader's sample and the reader's sides. Keeping them independent leaves a reader's side A uncorrelated with the panel's first order. Merging them would change no result.
  • S8. Repeated decided.calls match in grading::evaluate. Declined, as on Count the panel or people in the Verdict, with seeded samples for several readers #1953: only evaluate knows dropped_as_void (src/evals/experiment/grading.rs, evaluate).
  • S9. WaitingOnPeople edits across five files. This follows the existing WaitingOnLeaks pattern (src/console/peek.rs, src/logs/reading/inbox.rs). Gathering them is a refactor outside this Spec.
  • S10. Tuples and travelling parameters. These are judgement calls on private helpers with one or two callers. No change.
  • S11. src/resend_key.rs owns OpenCode keys. That file is the Credentials module, which already holds every provider's key. Its name predates this Spec.
  • S12. Small duplications in pairwise. These are judgement calls. pairwise::root() follows the six existing copies of the Experiments root, and gathering them all is outside this Spec.

Spec

Security review outcome

No unaddressed introduced findings.

Merge Danger

Door: two-way

Everything is opt-in through the Bar. Without --panel-seat or people flags, an Experiment runs as before, apart from base-tests and cross-tests for tasks that name process_grading.new_tests. The model Grader's choice is the exception: an Experiment can now pick a different Grader Model, when a configured Model shares a family with an Arm. Result format 10 is additive.

Blast Radius: Experiments

Consumers of Experiment JSON see format 10. The Console gains the pair page and the Experiment page gains its People's calls and Pair evidence sections. The sandbox proxy allows two more provider hosts, for OpenCode sessions only.

Closes #1872

Tickets

Built with claude 2.1.314 · opus · default effort

…#1883)

The proxy's OpenCode provider hosts gain Baseten's inference.baseten.co
beside Fireworks' api.fireworks.ai. The Credentials take an
[opencode.keys] table of keys by OpenCode provider name, with
opencode.api_key kept for any provider without its own, so two OpenCode
seats on different providers each get their own key.
After both Arms' Trials of a pair finish, run each Arm's new test files
on the unchanged base and against the other Arm's final commit, offline in
the code-Grader sandbox under the Bar's timeout, reusing the process
Grader's export and overlay. A task opts in with
process_grading.new_tests (patterns and a per-file command). Results are
kept per pair as evidence, never Scores, and shown as the Experiment's
pairs (result format 9), in the text view and on the Console page.

Closes #1881
Work out the OpenCode Trial's provider once, keep vocabulary to the
glossary's, list the OpenCode keys in the Credentials entry and module
doc, and rewrap long lines.
A timed-out run or an unpreparable snapshot no longer reads as new tests
failing on the base or as a cross-test disagreement: the code Grader now
says whether each check ran to an exit status, and such runs leave the
result null. Document per-field null rules, show unfinished runs in the
text view and Console, and share the HEAD lookup and attempt discovery.
Allow Fireworks and Baseten through OpenCode, with a key per provider
Run base-tests and cross-tests for each pair of Trials
…dict

The Bar can name a Grader panel: --panel-seat harness=..,model=..[,effort=..]
seats, several per family, a vote unit (--panel-vote family|model, family
by default) and the arm-family rule (--panel-arm-family-rule on|off, on by
default). Each seat reads every finished pair of process Trials in a fresh
sandboxed session per presentation order. The blind packet holds the
Ticket, the criteria, a fixed rubric, and per side the outcome, the diff
against the base and the Pair evidence, with A and B shuffled per pair and
swapped in the second order. Validated calls are saved by Arm, so a resume
asks only for missing ones.

A Grader's call that flips with the order is a tie. Seats from either
Arm's family are reported but don't vote while another family sits. Each
family takes its own majority first, or each Model votes; a call needs
more than half the votes, and a tied merge call is not ready. Every
seat's calls and the panel's result are reported under panel (result
format 10) and in the text view, beside the unchanged Verdict.

Closes #1882
Each pair with evidence gets a blind page and a CLI twin: the Ticket, its
numbered acceptance checklist, both changes side by side with code before
tests, and their evidence, on sides drawn per pair from the Bar's seed.
A reader seals a merge call per side before a preference and optional
rubric; records are append-only and attributed. Arm identity and the
panel's calls are revealed after the preference. People's calls and
people-panel agreement show beside the Verdict (result format 10).
Seal a reader's merge calls until their preference, and mark a review begun
after the reader's reveal as not blind, keeping it out of people-panel
agreement. Document the --pair JSON and review records and check them, with
people's calls and panel calls, against the result contract. Share the
preference values, next-step command, reveal line and changed-path listing;
read the Ticket input by its shared name and require a file. Describe the
blind page on the workflow page and read its format number from the contract.
Remove identifying added lines from panel packet diffs, as reports
already are, and read diffs within the Bar's timeout. Share one private
Grader session between the model Grader and the panel. Decode the
preference as an enum, name ballots, seat calls and the Arm shown as A
plainly, key per-Model votes by Model, call the sides Trials in the
prompt, and grade with the panel after the model Grader.
Add the blind pairwise review page to the Console
Keep the Grader panel beside #1885's blind pairwise page: the panel's
result fills each Pair's panel calls, for the page's reveal and
people–panel agreement, and its contract shares ArmPreference.
Grade each pair of process Trials with a Grader panel, beside the Verdict
The pre-Experiment estimate lists each panel seat's remaining sessions and
expected cost from the frozen price table. Panel spend is reported apart
from the Arms under panel_spend, and --panel-spending-limit pauses grading
once a run's panel sessions reach it; a resume continues from saved calls.
…eral readers (#1886)

The Bar now sets the Grader panel and the people each off, beside or
counted, as ADR 0028 describes. Named readers get queues drawn by pair
from the Bar's seed with a set overlap, and a shared pair is ready only
if every reader calls it ready. Counted calls make a Trial pass only when
its code Grader passes and its pair is called ready; a people-only sample
decides on the sampled pairs alone; with both counted the panel stands in
on unread pairs, guarded by a voting-family check at start and a
people–panel agreement level. The Verdict waits on people while a reader
has pairs to call, and people–people agreement is reported.
Release Trial repositories when a people's call settles the Verdict,
through one shared publish step for rechecks, leak decisions and counted
people's calls. Name the Verdict's sample agreement and the Bar's
PeopleSetting apart from the reported people, and count pairs, not Eval
tasks, in a Verdict on the people's sample alone.
Save a panel call before pricing its session, so an unpriceable session
under a spending limit pauses without being graded again on resume. Ask
about the panel alone when only its calls remain. Name the estimate's
types, move a seat's measured mean onto SeatSpend, share the positive
amount parser and the plural helper.
Estimate and cap the Grader panel's spend (#1884)
# Conflicts:
#	docs/experiment-results.md
#	docs/guide.md
#	site/architecture/index.html
#	site/docs/index.html
#	site/how-it-works/index.html
#	site/index.html
#	src/console/start/experiment.rs
#	src/evals/experiment/options.rs
#	src/evals/experiment/panel.rs
#	src/evals/experiment/panel/tests.rs
#	tests/experiments/panel.rs
Count the panel or people in the Verdict, with seeded samples for several readers
- Limit a sample-only Verdict's per-pair unit to suites of process Eval tasks.
- Blind the pair page's diffs and evidence errors as the panel's packets are.
- Choose the single model Grader by the panel's family rule, not exact Model names.
- Read the panel's Ticket through the pair page's guarded reader.
- Say the panel counts in the Verdict when the Bar counts it; seat Kimi through
  its own Harness in the guide's panel examples.
@JacobStephens2
JacobStephens2 marked this pull request as ready for review November 10, 2026 16:55
@JacobStephens2
JacobStephens2 merged commit 4b9e6a4 into main Nov 10, 2026
16 checks passed
@JacobStephens2
JacobStephens2 deleted the issue-1872 branch November 10, 2026 17:22
@JacobStephens2 JacobStephens2 mentioned this pull request Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spec: blind pairwise grading of process Trials by a Grader panel and people

1 participant