Test · evaluation
Eval Gates
Every commit re-runs your agent against a frozen benchmark and compares the receipt against a baseline you committed. The build fails when leakage grew or a control arm stopped being a control.
- BUILDING
- GitHub Action
- CLI
- Apache-2.0
What runs today: the gate module and the composite action ship inside the harness, and they compare two receipt files with nothing else installed. What is not built yet is anything that keeps state between runs, so trends across many commits are your CI's job.
The artifact
A build failing, with the numbers that failed it
Two real runs of the stub agent: the baseline over 2023-03-01 to 2023-03-10, the candidate over all of March. Nothing about the agent changed, so every failure below is the comparison telling you the two runs are not comparable.
$ pit-harness gate receipt.json --baseline evals/baseline.json --mode block
python -m pit_harness.gate mode=block tolerance=defaults
baseline evals/baseline.json
run bf78382b9021b3be sha256:4580ffd9... bundle sha256:bc924991...
candidate receipt.json
run 9ba0269e2c5cd3b7 sha256:5a14bfb1... bundle sha256:bc924991...
CHECK SUBJECT BASELINE CANDIDATE CHANGE TOLERANCE
positive_control shifted@pit 0.100000 0.000000 -0.100000 0.000000 pass
timing mean_sessions_pit_ahead_of_dump 0.000000 0.011700 +0.011700 0.050000 pass
timing mean_sessions_leaky_ahead_of_pit 1.000000 0.988300 -0.011700 0.050000 pass
timing mean_leaky_lookahead_hours 0.000000 6.661600 +6.661600 0.500000 FAIL
timing max_leaky_lookahead_hours 0.000000 8.026700 +8.026700 0.500000 FAIL
timing acceptance_fallback_rate 1.000000 0.939427 -0.060573 0.050000 FAIL
integrity baseline body_sha256 sha256:4580ffd9... sha256:4580ffd9... n/a n/a pass
integrity candidate body_sha256 sha256:5a14bfb1... sha256:5a14bfb1... n/a n/a pass
integrity candidate run valid n/a True n/a n/a pass
comparability bundle_content_digest sha256:bc924991... sha256:bc924991... n/a n/a pass
comparability config_sha256 sha256:e846d10f... sha256:38983f39... n/a n/a WARN
comparability harness version 0.1.0 0.1.0 n/a n/a pass
3 failures, 1 warning, 12 checks.
FAIL timing mean_leaky_lookahead_hours: moved +6.661600 hours, past the 0.500000 allowed, so the two runs saw different clocks
FAIL timing max_leaky_lookahead_hours: moved +8.026700 hours, past the 0.500000 allowed, so the two runs saw different clocks
FAIL timing acceptance_fallback_rate: moved -0.060573 fraction, past the 0.050000 allowed, so the two runs saw different clocks
WARN comparability config_sha256: the run configuration changed, so a moved number may be the config rather than the agent
Blocking on 3 failing checks, exit 1.
Run on 2026-08-27. The whole table is printed, passes included, and the
failures are repeated underneath with the numbers behind them.
--json prints the same report machine-readably, and
--report-json PATH writes it to a file while the table
still goes to the log.
Read the three failures back and they say something specific: the March window carries acceptance stamps the ten-day window did not, so the PIT clock stopped falling back to the filer clock, and the naive arm's lookahead became measurable at 6.66 hours on average and 8.03 at worst. Those numbers moved because the two runs read different clocks. Comparing contamination between them would be comparing different populations, which is exactly what the gate is for.
The failure it prevents
Leakage arrives in a pull request that looks fine
A join gets rewritten, a helper starts reading a current-ticker table, a cache warms from a later snapshot. The tests pass, because the change is correct in every way except the clock, and the eval score goes up, because the agent now sees things it could not have seen. Nothing in the diff points at a clock, so the higher score reads as progress and the commit lands.
The gate turns that into a red build on the commit that introduced it. It compares the gap between the agent's score and what each control arm reproduces, and it only counts growth: a delta that shrank, meaning the main arm moved closer to its controls, passes.
It also checks the check. If the baseline said
mask_holds and the candidate says
mask_leaks, the build fails at any tolerance, including
one set deliberately wide. The shifted arm's delta measures
memorisation only while the mask holds, so once the probe recovers the
true date or the issuer, that delta is measuring something else and
reading low for a reason unrelated to the agent. A tolerance there
would apply to the check that qualifies every contamination number
above it.
How it works
What the gate compares
| Check | What moves it | Default tolerance |
|---|---|---|
| contamination | Growth in the main-minus-control delta, per clock and control arm. A cell the baseline had and the candidate reports as unavailable fails; a cell the candidate added warns. | 0.005 in return units, so 50 basis points |
| positive_control | The probe verdict flipping from mask_holds to mask_leaks, a probe that stops running, or a probed arm the candidate dropped. |
none, and deliberately so |
| timing | Drift in either direction across five aggregates: the two mean session gaps, the mean and max naive lookahead in hours, and the acceptance-fallback share. | 0.05 sessions · 0.5 hours · 0.05 fraction |
| integrity | Either receipt failing to hash to its declared body_sha256, or a candidate whose valid is false because the network guard caught an egress attempt. |
none |
| comparability | A changed release digest, config hash or harness version. These warn and let the build through until --fail-on-warn. |
none |
acceptance_fallback_rate is the one worth knowing about:
it is the share of events that fell back to the filer clock because the
row carried no acceptance stamp. It rises when a rebuild lost stamps,
which silently collapses the PIT clock onto the dump clock and makes
two arms identical without anything looking broken.
Exit 0 on no failures, or on --mode report whatever it
found. Exit 1 in block mode with at least one failure. Exit 2 when a
receipt could not be read at all, in both modes, so a broken file never
reads as a pass.
Integration
Three steps in a workflow
- uses: actions/checkout@v5
- run: pit-harness run --config run.json --receipt receipt.json
- uses: ./harness/action
with:
receipt: receipt.json
baseline_path: evals/baseline.json
tolerance: "0.005"
mode: block
harness/action/ is a composite action written with
bash and python3 and no other action. It
installs the package sitting next to it, runs the gate, appends the
table to the job summary, and exits 1 in block mode. Its outputs are
status, failures, warnings and
exit_code, so a later step can comment on the pull request
or upload the JSON report without re-reading the log.
Establishing a baseline is two commands: run the harness, commit the receipt. Read that baseline's own probe verdict before you commit it, because a baseline that was already leaking will pass every candidate that leaks the same amount.
$ pit-harness run --config run.json --receipt receipt.json $ cp receipt.json evals/baseline.json && git add evals/baseline.json
The market
What other CI gates compare
A per-vendor sweep of shipped SDKs and documentation on 2026-08-27.
- Ships a gate
- Langfuse, whose experiment action landed 2026-05-25 and fails on regression by default, and Confident AI, whose DeepEval ships a pull-request eval gate with a regression tolerance and a governance command.
- Ships something weaker
- Braintrust's action takes no threshold and fails only on an exception. LangSmith alerts without blocking.
- Documents a do-it-yourself gate
- Arize, Galileo, HoneyHive, Maxim and Opik point at an
assertor asys.exiton their CI pages.
This gate compares the gap between the agent's score and what a control arm reproduces, and then checks that the control still held. Measuring leakage needs a control arm and a probe against it; the vendors above ship neither, so what their gates compare is the score.
Limits
What the gate cannot see
- The baseline sets the floor
- If the run you committed was already scoring on memorised knowledge, every candidate that leaks the same amount compares clean.
- A changed release resets the comparison
- The deltas and the timing aggregates are properties of a run over one release. Regenerate the baseline when the release changes; the comparability warning tells you it happened, and
--fail-on-warnmakes it stop the build. - It reads the probe's verdict and nothing more
- A model that recovers the period and says nothing about it passes the probe, and therefore passes this check.
- Calibration is not gated
- The harness marks that metric a stub, and the gate does not compare it.
- It compares two runs at a time
- The gate keeps no state between runs. Tracking a number across many commits is something your CI does with the receipt files.
Related
Upstream and downstream
- Evaluation Harness
- The runner that writes the receipts this reads. The gate has no other input.
- Benchmark Datasets
- The release both runs are scored against, and the digest the comparability check watches.
- Contamination Certificates
- The same evidence signed for an outside reader, rather than checked for your build.
- Decision Audit Logs
- The production counterpart: the same discipline applied to decisions rather than to commits.