Repository navigation
evaluation-harness v0.6.0
What you get: datasets made from what production did (capture, scrub, split, dataset-from-capture); labels that say
who decided what is right, and reference sets promoted from them; production's own decisions scored as an arm;
models onboarded before they are run; scoring that evolves without re-running an arm; judges that grade, pick the
best or compare two; and one sheet of every item and arm, filled in Excel or another tool and read back. Upgrading
moves no committed score. Two formats (fingerprint v5, model records) and the old references are migrated by a
command, and a few names change to the field's words with no alias (evh run, significant, contenders):
UPGRADING has the steps.
Added
- Labels, and the reference sets promoted from them (RFC-023, PRD-002, ADR-0022). A label says what is right
for one item and who said it: aperson, amodelor arule. Labels come in sets, never edited, under
data/labels/<dataset>/<set>/, and follow an item by its sha256 into any dataset holding the same bytes.evh label-from-run: a finished run's outputs as a model's labels, no calls, the refusals a reference's
provenance needs (a dirty or unknown build, an unfinished or derived run, an empty output, one cut off at
max_tokensunless--allow-cut-off).--not-redistributedkeeps the texts out of git (a.gitignorebeside
them) and records the command that rebuilds them, checked against the set's checksums.evh label-sheetwrites a sheet of the items to label (unlabelled, all, where runs disagree, or chosen),
each run's decision and a proposal beside an empty label;evh label-importreads it back, or a folder of
<item_id>.txt, as a person's set (yestakes the proposal; notes kept). The sheet is CSV that Excel opens
and saves (UTF-8 with its mark,,or;);--choicesputs the allowed answers in the label column's
header and the import refuses any other; the local inspector downloads it.evh label-from-captureturns a
capture's recorded outcomes into aruleset by the rulesreference-from-outcomesread.evh labelslists
the sets, what each covers and how two agree: the same answers, sharing one, Cohen's kappa, Krippendorff's
alpha and (--confusion) the confusion matrix; free-text labels are not compared as text (an MCP tool).
V19 checks every set.- A label can accept more than one answer, when several are right for an item:
{"accept": ["escalate", "file"]}, sorted and without repeats, made by the sheet'salso rightcolumn, a rule'slabel: [file, escalate]or a folder's text, shown "escalate or file". A decision is wrong against a label when it is none of
them; two labels agree only when they accept the same answers. An adapter'sscorereads a reference with
answers(reference)(evaluation_harness.adapter); the bundled one keeps the best overlap. The triage gold's
23file-or-escalatelabels (and their reference) are written so now, bylabels_from_verdicts.pybyte for byte:
every one of the 18,460 item scores of the 284 runs against it is the same replayed old and new. The outcome
rules give the same two answers, so gold and the rules accept the same answers on 34 of the 34 items both label
(kappa 1.00). evh reference-promotemakes a reference set of a label set, at the tier you choose, with a reason and who promoted
it; the manifest records where it came from (for a model's set, the run and model that wrote it). No rule ties
the tier to the source. Every existing reference set records how it was promoted (20 here; 6 copied byte for byte
into label sets, 209 labels), every run's fingerprint unchanged.- Scoring against a label set without promoting it:
evh rescore --labels SET,evh run --labels SET. The run recordsreference_id: labels/<dataset>/<set>and lands indata/runs-labels-<dataset>-<set>/, an
arm of its own; a goal groups such arms under the set, the arm that made a model's labels flagged. - The catalog: a tier is a pill in its own colour, a label set a source chip, wherever either is named; a
Labels page; a dataset's label sets and how often two agree; where each reference set was promoted from.
- Evaluating what production did (RFC-023, PRD-014, ADR-0023). A capture's decisions are evaluated against
labels; the governed datasets grow only by a version you make:evh arm-from-capturemakes production's recorded decisions an arm (provider: recorded): each output the
capture's record of the same id, read by the adapter'srecorded_output(record)(else--field), scored,
tested and compared like any arm, cost 0. The catalog badges itrecorded, its cost and speed "recorded, no
call".- Agreement: the adapter's
same(a, b)says when two outputs are one decision (default: the same text;
the triage adapter compares dispositions).evh inspect --disagreeand the inspector's "only where they
disagree" keep the items where the arms decided differently;label-sheet --items disagreereads the same hook. - The sheet,
evh inspect --sheet --dataset-id D --runs A,B,…and the inspector's 05 sheet (the dataset
page's "Open its sheet"): the items down, a column per arm across with what it wrote and every score it has
(its run's metrics, and a judging pass's where one covered the run;--metric MorSCORER/Mchooses the one it
is read on;--scores-onlyleaves the outputs out), every label set and reference set, the winner per item (on
that metric, and per pick or pairwise judge), the signals wrong (--wrong-against SET), disagree,
split, new (--new-vs D: a field value it never had), unlabelled, each a filter beside--whereand
--by, and a footer per arm: its mean, its win rate and its verdict (the goal's decision, else the paired test
against the leader). On a pick or pairwise verdict only an arm the judge preferred wins a row; a row it gave to
an arm not shown is nobody's win. The item column stays in place as the arms scroll; the rows you tick give theevh pick
andevh label-sheetcommands.--csv FILEwrites it (UTF-8 with the mark Excel needs), and the inspector
downloads the same (/api/sheet,&format=csv). Local only: a public server refuses it. - A sheet to fill, and
evh sheet-import(make sheet-import):--csv FILE --fill labeladdslabel,also rightandnote(--choices);--fill pickorscoreswrites the outputs as blind columns A, B, C… shuffled
per row (--seed), apickor ascore: Xcolumn per output (--scale LO..HI), the rubric as the first row
(#rubric;--rubric FILE) and its key beside the sheet (sheet.key.json).evh sheet-import FILE --dataset-id D --by WHOreads back what was filled: labels as a label set (--set,--kind person|model), picks or scores
as judged runs underdata/runs-judged/<scorer>/(--scorer),judge_won_unvalidatedor
judge_score_unvalidatedper arm per item, through the key. A row or text the key does not have, a letter not on
its row, a score off the scale and a name already used are refused, with nothing written. - Picks:
evh pickmarks a production item for the next version of one or more governed datasets (targets,
tags, a note) indata/picks.jsonl, append-only;evh pickslists what waits (an MCP tool), and the target
dataset's page shows them with a box per row.evh dataset-extend V1 --dataset-id V2 --picks(or--pick)
makes V2 of V1's items and the picks chosen, each checked against the bytes picked,extended_fromrecorded;
labels follow the items; nothing is promoted. evh deploy-sheet --config ARM [--capture C]: what production must match to run what you promoted (the
model, its record and deployment, every setting sent and a record's setting that was not, what each item
reported, the adapter), and with a capture, whether each record says production ran it (the adapter's
production_ran(record)); exit 1 unless every record matches. On the promoted arm's page, and folded beside a
goal's baseline. The triage importer keeps the ledger'sprompt_verandprompt_sha.
- Several projects on one machine (ADR-0024):
catalog-serveandmcp --httptake a port of the project's own
(18000–18999, 19000–19999), the same every time, the next free one if taken; a local catalog names its project;
every command notes a strayEVAL_HOME; a model record's lock is keyed by the record. - Datasets from production: the capture lane (RFC-019, PRD-014, ADR-0019). A project keeps what a
production system did as a capture, then makes datasets and labels from it:evh capturereads records through an importer (--importer jsonl|tsv|github|langfuse, or a
project's ownpath.py:Class) intodata/captures/<id>/: each record's id, body, fields, time, source
and outcome, hashed one by one (capture.json);--since-capturecontinues from an earlier capture.
evh importer-checktests an importer against the contract, including that it keeps tosinceand
until;evh captureslists them, and the catalog's Captures page shows them with the datasets made
from each. A capture keeps the window it records: a record outside it is named and nothing is written
(--latekeeps the ones older thansincethat the earlier capture never saw). Newer outcomes of the same
records are a fresh capture, labelled withlabel-from-capture --capture(tutorial 6 §5).evh capture-scrubruns a project's own scrubber over a capture's records and re-hashes the ones
it changed, keeping their earlier hashes.evh capture --sample-rate R --seed Skeeps a share of the records, by a hash of each id, as they are
read: the same rule draws the same records again, an importer may sample at the source, and a continued
capture keeps the rule.evh dataset-from-capturefreezes a selection (--where,--dedup,--one-per),
typed fields from a field set, and--tag K=Vlabels; only the body becomes the item.- What happened afterwards becomes labels by a small rule set (
when,unless,exists,absent,range,
min_age_days):evh label-from-capture, above. evh dataset-splitmakes<D>_validationand<D>_testby hash (stable across versions, a group
kept whole with--key) or by time.- V17 checks every capture's records against their hashes; a capture whose records a project keeps
out of git (fetched from where it publishes them) is named as not fetched, not failed.
text: hiddenon a dataset: none of its text, nor anything derived from it, on any public surface:
the catalog's pages, the inspector andevh mcpin public mode, and a deployment's copy of the data
(_release.py copy-data, which also refuses a release carrying reference texts or run outputs git does
not track).dataset-from-capture --hide-textsets it.- Dataset tags on the views: the catalog's datasets page groups by each tag key and shows where a
dataset came from (a capture and its selection);evh datasets --tag K=Vfilters the same way. - Two examples from a production fleet:
examples/triage-fleet/(an agent's real triage decisions,
replayed through its own code; the operator's verdicts as gold; the model's own decision scored beside
the fleet's, before and after its gates) andexamples/signal-fleet-dedup/(a regression guard for the
fleet's de-duplication functions; no model). Their data is published on Hugging Face and fetched by a
pinned revision. - A share card on every catalog page: its title and its opening line as
description,og:*and
twitter:card, and withevh catalog --card URLthe image a shared link shows. The pages stay
noindex. - Onboarding models, once, for every eval (RFC-020). A project keeps a model record per model,
data/models/<model_id>.yaml: its aliases, weights and licence, what it can do, the settings that hold
whatever the task (reasoning_effort; device, precision, batch size for an ML model) with why, its history,
and its deployments (each place and build it runs: machine or host, server and version, digest, revision
or hash, the route for a hosted one), each onboarded on its own.evh model-onboard <id> --deployment <d>probes an LLM through the deployment's endpoint: the digest the
server reports, whether it reasons at the default and with the record's settings, JSON, determinism and
speed (the deployment's, not the record's); a different build than the record's is flagged and kept in its
history. An ML model or a baseline through an arm (--config).ONBOARD PASSorONBOARD CHECK.evh arm-smoke --config <arm>runs any arm on a few items before its full run and writes no run: runs,
valid output for the scorer, a projection of the full run's time and cost, build pinned, determinism; for an
LLM every model call (reasoning, slow, long, cut off, no answer, from a provider's_metaor an adapter's
meta["calls"]; a reply is long past half the arm's reply cap, or with no cap against the arm's own median,
so neither a summarising arm nor an agent's short probe calls beside its answers make a long reply).
SMOKE PASSorSMOKE CHECK, written into the record.- An arm names a record and a deployment (
model_record:,deployment:): the record's settings go under
its params, which add the task's own (its reply cap, its call limit: a record refuses them, since they would
reach every other eval's arms); the run records both. V18 checks the records, and the arm configs indata/configsand those validate.json'sconfigsglobs name;validatenotes an arm never smoked, a
reasoning model with nomax_tokens, a tag that is a different build on two deployments. - The catalog's Models page and a page per model, a smoke badge on the arms page, a models tile;
evh modelsfor the same (and overevh mcp). A model is vetted when an arm that names it passed its smoke;
its onboarding alone makes it onboarded.
- Scoring that evolves (RFC-022). A round records the scorer it is judged by, and a scoring change is a
step of its own:evh score-preview --goal G [--round rN]re-scores the goal's arms (or the round's) with the scorer in
the working tree, in a temporary directory, and prints each arm before and after and the decision before
and after; it writes nothing and spends nothing.evh rescore --goal G --round rN --why "…"re-scores a round's arms; the round records the change
(scoring: when, the scorer, why, where).evh round-open --from rN --keep all --scoring "…"opens the next round judged by the current scorer:
every carried arm another scorer judged is re-scored first ($0), each one's measure printed before and after.- Re-scores land in one place the tool designates,
data/runs-rescored/<scorer>/<source>/, read by the
catalog, the goals, the rounds andvalidate;--outstill names any other. Rounds read an arm from its
re-score as the goals do, andevh roundsand the goal page show a round's scorer and scoring changes. evh round-open --adoptrecords a goal's first round from the arms already run for it.- A goal axis's
limit: baseline: an arm is out when the paired test finds it worse than the group's
baseline arm on that measure. validatenotes a round whose arms one adapter scored in more than one version.evh init's.gitignoreignores the throwawaydata/runs-smoke*/anddata/runs-tune*/.
call_timeout_s, an arm's call limit, for the built-in providers: the SDK client's timeout (its own
default is 10 minutes). The RUNBOOK told arms to set a call limit only the triage adapter honoured.- The triage example's adapter takes
reasoning_effortandmax_tokensper arm, reports every model call
forarm-smoke, and records a call that fails the same way twice at temperature 0 as the fleet's own "no
terminal decision" (no_decision,deferred). Withreply_in_tool_call: <name>it reads an answer a server
returns as a tool call to that name, with no text: Ollama 0.40 does that to every gpt-oss answer, and the fleet,
which reads only the text, got nothing. - No key for a local server: the
openaiprovider asks for noOPENAI_API_KEYwhen its address is on this
machine (localhost, 127.x, ::1) or on Ollama's port, 11434; Ollama takes any key. Any other address still
needs one, and a key that is set is used. - Tutorial 7, set up a model before you test it: a model record, onboarding it where it runs, an arm that
names it, a smoke before the full run; on your own machine with Ollama. - Judging modes (RFC-005, PRD-003 FR5): a scorer's
modesays what a judge is asked:grade(the default,
as before),pick(the best of an item's outputs, in one call:outputs_per_callmust hold every arm) or
pairwise(the better of two, every pair of arms;outputs_per_call2). Pick and pairwise projectwonper arm
per item (judged runs' primaryjudge_won) and report each arm's win rate and Bradley-Terry strength with a 95%
interval from resampling the items, and the position and noise checks as the share of verdicts that held. A judge
is validated in its mode (validation.modemust bemode). The SummEval example validatesgemma_land
qwen_sin both (mode: pick, scoredtop1besidetop1_chance;mode: pairwise, scoredpair_agreement
withn_pairs; arandomfloor each): picks 0.364 and 0.263 against a random 0.070, pairwise 0.825 and 0.813
against 0.473; the prompts ask for the JSON first and nothing after it.judge-summ-pick-v1and
judge-summ-pairwise-v1are the summarisation eval's judges in those modes; the pick pass is run (29 arms in
one pick: the order moved about half the picks, see the SummEval README), the pairwise one not yet. - An adapter's optional
primary_metric(params): it names an arm's primary metric from the arm's params,
besidePRIMARY_METRIC(its fallback);runandrescorerecord it. The SummEval adapter's istau,top1or
pair_agreementby mode.
Changed
-
evh experiment-runisevh run, and an experiment is an eval: industry words (OpenAI evals,
inspect_ai, Weave, Langfuse); what Braintrust and LangSmith call an experiment is a run here. The site's
Experiments page is Evals (evals.html); the old address redirects. -
Dataset roles are validation and test (were dev and heldout):
evh dataset-split --test SHARE(was
--heldout) writes<D>_validationand<D>_test, and a split dataset recordssplit.testandpart
validation or test. A goal'srole:says validation, test or pilot. The reports' dev slice is the pilot slice,
a small slice inside the test slice (not a validation split), and their measurement slice the test slice.
evh holdoutkeeps its name. -
The paired test's verdict is significant (was separated):
significant: better,significant: worse,
not significant, everywhere they are printed or shown (evhoutput, the catalog, the inspector, the site,
the reports);family_testandbootstrap_testprintSIGNIFICANT,pair-testSIGNIFICANTor
NOT-SIGNIFICANT. A project reading the old verdict strings reads the new ones. -
A goal's arms that may win the tie-break are its contenders (were tied): the decision's key is
contenders(wastied),evh arms --quality contender|below(wasties|below), the arms page's facet
"a contender", and the why-lines say "Contenders beside it" and "Still undecided after the tie-break". A tie
is still what it was in a paired test: an item where two arms score the same. -
A promoted reference is a reference set (the governed collection under
data/references/<tier>/<name>/);
a reference is the expected answer for one item. The catalog's References page is Reference sets. Commands,
paths and keys (reference-promote,reference_id,reference_tier) keep their names. REFERENCE.md maps
every term to what inspect_ai, lm-evaluation-harness, HELM, OpenAI evals, Braintrust, LangSmith, Langfuse and
Hugging Face call it. -
reference-createandreference-from-outcomesare removed (ADR-0022): a reference set is made only by
promoting labels. A run's outputs arelabel-from-run's (with the same refusals and--not-redistributed),
outcomeslabel-from-capture's (the same rules). A reference set made from outcomes has no rule of its own any more:
promoted, it is a silver like any (with gold beside it, gold still decides; ADR-0020 is superseded). The one
reference set here that namedreference-createas its rebuild says where its texts come from instead. -
validatefails a reference set with no manifest (V16), where it noted one: texts copied into
data/references/by hand are a reference set nobody promoted. Import them as labels where they are and promote
them:reference-promoteadopts the folder when its texts are the labels' (writing its manifest; a folder whose
texts differ is still never overwritten). Tutorial 1, the newcomer scenario,evh self-testand the tests make
gold that way. -
evh reference-manifestis a promotion: it takes--labelled-by,--reasonand--by(and--kind), and
records them aspromoted_fromand the reason, so every reference set says where it came from; the reference schema
requires both. This repository's reference sets are aligned: the caveats name where their labels came from (no
"human-authored" or "model-generated" tier wording), the two that named a removed command as their rebuild name
the 0.6.0 ones, andfrom_outcomesandcost_usd, which only the removed commands wrote, are gone. No run's
fingerprint moves (none of these is hashed). -
The sheet shows each arm's decision, the signals first: an adapter's optional
decision(output)(the
triage example's: its disposition) is what an arm's column shows, the output itself otherwise; with no
same(a, b), two outputs agree when their decisions do. The browser's columns follow the terminal's (UXS-002).
An adapter's optionaltitle(item)names each item (the triage example's: the signal's alert name, not the
probes that open it). -
evh inspect --itemsis gone, and so is--rows; the sheet replaces both (no aliases):evh inspect --sheet --dataset-id D --runs …shows every item's scores per arm and the rows view's signals in one table. The
inspector's endpoints/api/rowsand/api/label-sheetare gone;/api/sheetserves the sheet, and its
&format=csvthe download. The MCP toolinspectoffers the sheet's options but not the ones that write a file
(--csv,--filland theirs). -
The triage example's
write_gold.pyislabels_from_verdicts.py: a source's labels fromverdicts.jsonl,
one file per item, forevh label-import; the tier is the promotion's. Its owner labels rebuild the committed
gold set byte for byte. -
catalog-serveserves on the project's own port, not 8765 (--portoverrides;--publickeeps 8765), and
mcp --httpon another; a bookmark of127.0.0.1:8765moves. -
The release leaves the pick list out: picks are working notes that name production items, so a published
catalog shows none waiting. -
The tutorial kit's desk export records where the desk's router sent each ticket (
routed): tutorial 6 scores
that decision against what happened, and picks the misrouted tickets for a dataset it governs. -
The goal page explains, then answers, then shows how it got there: what it is for first; then the answer
so far, led by the latest round's conclusion when one closed after the baseline was promoted (the promotion's
reason folded under it); then its rounds, one folded section each, headed by its row (round and status, the
conclusion, what it set out to learn, arms and best), and inside it a heading per part (what changed, arms
with retirements grouped by reason and the carried ones folded, scoring, evidence, suggested next, notes). The
rounds table is gone. -
The Models page has one line per cell: the first build and "+k", the deployments' ids, "N goals" with the
first named, the single best placing with "+k more"; vetted records first. A model's page has one folded
section per deployment, holding its build and its own onboarding with the flags it accepts and why; its arms
table names each arm's deployment and reads a re-scored arm as the goals do. -
The datasets table shows each tag as a chip.
-
Fingerprint v5 (RFC-021): what each part identifies.
instrument(the harness and the adapter's code),
data,deployment(the system under test as it executed: from the arm's model record and deployment, the
adapter's hook, or the params; the weights, the server, where it ran; in process only when the arm calls
nothing elsewhere) andsettings(how it was asked: the params that change behaviour, and what a hook declares
about its prompt), each hashed, andhashover the four. The machineevhran on isrunner, recorded outside
the hash: the same deployment run from two machines is the same measurement.evh run-comparenames the part
that moved; the inspector's speed warning tells a deployment that differs from a runner that does. Every
committed run is migrated byevh fingerprint-migrate(idempotent,--dry-run); v4 fingerprints are not kept. -
data/models.yamlis replaced by the model records (data/models/): its 41 entries are migrated one record
each, and the arms page's open / proprietary filter reads the records. -
CI runs once per merge, not twice: a squash onto main whose tree is its pull request's head,
tested green and up to date with main, skips the workflows that already passed on it; the deploy
no longer waits ~13 minutes for a second run of the same files. Anything else runs them again. -
An item id may contain
:,@,+and=(a production record's id, e.g. a fingerprint and its
time), on the inspector's item view too; still never a path. -
A deployment copies its data through
_release.py copy-data, not a list inrender.sh: the same
directories, plus each capture'scapture.json(never its records), with thetext: hiddenrules
applied and the untracked-text refusal checked before anything is written.
Fixed
- Every
evhcommand is amaketarget again, as the READMEs and the RUNBOOK say: the 22 added in 0.5.0 and
0.6.0 had none (make labels,make reference-promote ARGS="…", …). - The home page's "recorded" feed shows the period's events. Its period filter read the date from the table
cell, not the span inside it, so it hid every event and said "Nothing recorded in this period" (since 0.4.2). - Coverage counts an arm scored against a label set as
labels, on the home page and inevh overview; it
counted asnone. - The home page's glance reads a goal's baseline from its re-score (RFC-022), as the goal page does: a baseline
promoted before a re-score showed no value and no test. - A frontier chart's names stay inside the chart: a long name on a dot in the middle ran past the right edge;
it goes to the side it fits, shortened when neither does, never over the y axis. - The label-set and agreement tables keep their promoted and agreement columns on screen on a desktop: long set
ids may break there (a phone still scrolls the table sideways). evh deploy-sheetsays "against no reference" for a run scored against none, notNone (None).- The leaderboard's silver note no longer says "model-generated": a silver reference set can be a person's or a
rule's labels. - A retired arm read through its re-score is out of the goal's contenders. A round's retirements name the arm's
config, and an arm read fromruns-rescored/<scorer>/kept the prefix, so it was never matched as retired and
stayed among the contenders (the triage goal's: 76 arms, 51 once read right). - Sorting a page's later table sorts it by the column clicked. Its header's index was counted over every sortable
header on the page, so a second table sorted by a column it does not have and threw. evh mcp --publicsees only public fields ininspect. The tool runs the command in its own
process, where the inspector's public rules were never set, so a--whereor--byon a field not
marked public was accepted and its values could be read through the counts (0.5.0). A public
server now runs its commands as public (EVH_PUBLIC), and such a filter is refused by name. This
repository's own deployment declares no field sets, so it exposed nothing.- A command's own refusal through
evh mcp(exit 1, itsERROR:line, no traceback, such as a
dataset id that is a path) is told to--hooksasrefused: "command", not as an error: a
deployment's metrics count it as refused and its analytics send no event for it. - The site's links to its home page point at
/, neverindex.html(analytics counted the two
apart, search engines saw two addresses for one page), and each of its pages names its own address
as<link rel="canonical">andog:url, checked bysite/build.py --check. validatechecks every git-tracked runs directory, thosecommittedin validate.json with their floors
and every other one: a directory re-allowed in.gitignorebut not added to the list was never checked
(a set of 65 re-scored triage runs, before RFC-022 gave re-scores one place).- The local catalog reads what is committed. It read every
data/runs*directory on disk, so a smoke
pass in an ignoredruns-smoke*showed as a result locally and was gone from a clone and the published
catalog. Directories git ignores are no longer read (outside a git checkout everything is). A measured arm
the goals read from its re-score says so on its page, on the arms page and inevh arms. - Runs written where git does not keep them are named.
EVAL_RUNS_DIRpointed at any
data/runs-*directory that.gitignoredoes not re-allow wrote runs that were never committed
and that no catalog showed, without a word: 35 self-hosted triage runs sat indata/runs-local
that way.runandrescorenow print aWARNINGwhen they write to such a directory
(throwawayruns-smoke*andruns-tune*stay silent), or outside the project's data, where nothing
reads them, andevh validatenotes one that holds
finished runs. The RUNBOOK's scratch example isdata/runs-smoke, the throwaway name git already
ignores. - One row per arm when a scorer changed. A goal listed an arm twice when its runs had been
re-scored (evh rescore) into a derived set, once per directory, and dropped the measured runs
whose older score lacked the goal's measure: 37 triage arms were listed twice, 15 only from
the re-score. A re-score against the same reference now stands in for the run it re-read on the
goal pages, inevh goalsand in the recommendation, which says how many arms it read that way.
evh goals --goalalso prints a dataset's notes, as its page did. - A deployment the adapter's hook declares keeps the server it was called on. Its
serverwas
always null, so a self-hosted run whose adapter has afingerprint()hook lost its Ollama or vLLM
URL. The 130 triage runs are backfilled with the fleet they replayed (tools/backfill_triage_fleet.py:
the fleet's code hash and commitf07efa9, the prompt's hash, each checked against what the runs'
own items recorded) and re-sealed. - Onboarding a model on several machines at once keeps every result.
model-onboardandarm-smokeread the
record when they started and wrote all of it back when they finished, so the slower erased the other machine's
result. They re-read it under a lock and write only their own deployment or smoke. - A LiteLLM address given with
/v1is not given a second one (http://host:4000/v1became/v1/v1, a 404),
and a reasoning setting sent through LiteLLM is named inallowed_openai_params: most hosted model groups refused
reasoning_effortwithout it (UnsupportedParamsError). - A record can accept a flag as the model's nature (
accepted: {reasoning: "<why>"}): onboarding and arm smokes
report it as accepted and pass (gpt-oss reasons whatever it is asked). A deployment accepts kinds of its own
beside the record's: a hosted gateway's build cannot be pinned, and its repeats at temperature 0 vary. - A round with more than 40 arms is compared. Its evidence asked the inspector for one comparison of every
arm, which refuses past 40, and the refusal left the rows empty without a word (the triage round, 52 arms). Past
the inspector's limit the round runs the same paired test on the scores directly; a refusal becomes a note. - A project module edited within the second is what runs. Adapters and hooks were imported through
Python's bytecode cache, which trusts a.pycwhile the source's modification time (whole seconds) and size
match: a scorer edited to a value of the same length (BONUS = 0.0to0.5) and re-scored at once ran the
old code while the re-score recorded the new code's hash. They are compiled from the source every time. - A system that ran in the harness's own process, with no hook, is in process. A deployment taken
from the params was alwaysin_process: false, so a local baseline (Lead-3, a keyword rule) kept its
libraries on the runner instead of the deployment.evh fingerprint-migratebrings existing v5
fingerprints to the rule when it has the run's params (11 runs here, one baseline's hash). - A run stops when its server cannot be reached. An adapter that turns a failed model call into an
answer (the triage replay records it as the fleet's "no terminal decision") wrote a complete-looking run
when a tunnel dropped: 54 and 65 refused items scored as the model's own failures. When every call an
item reports (meta["calls"]) failed reaching its server (connection refused, no route, a name that does
not resolve),runstops there withSTOPPED, that item unwritten, and--resumecalls it
again. A model's own failure is still a measurement. evh pair-test --roundreads each arm as the round does: from its latest re-score when one stands in for
its runs. It readdata/runsalone, so a measure an arm's re-score added (production's triage run on
triage_cost) found "only 0 shared item(s)" while the round's own evidence tested it. "(its leader)" is said
only when the test chose the leader.
Install
pip install "evaluation-harness @ git+https://github.com/chipi/eval-harness@v0.6.0#subdirectory=harness"