Skip to main content
Source Code: src/gaia/eval/release_scorecard.py (core generator) · src/gaia/eval/scorecard_gate.py (release gate)Distinct from src/gaia/eval/scorecard.py — that file is the per-run scenario PASS/FAIL aggregator used internally by gaia eval agent. This document describes the outward-facing release artifact.

Overview

Each published hub agent ships a release scorecard — a single SCORECARD.md file (updated in place per release, versioned via the publish snapshot, the same way README.md works) that records:
  • The eval recipe: dataset reference, methodology, configuration, and metric definitions.
  • The measured results: per-metric values, number of test cases actually run, and dataset size.
  • A single named aggregate score: a deterministic, recomputable percentage so a reviewer can verify the number without re-running the eval.
  • A Reproduction section: the exact commands to reproduce the result from scratch.
Scorecards are committed alongside the agent’s README and linked from it. A standalone release gate (scorecard_gate.py) blocks packaging when the scorecard is missing, when its aggregate score regresses below the prior version’s, or — when enforcement is on — when it is below an absolute bar or an anti-gaming floor.

Acceptance metric — email triage (#1437)

The email-triage scorecard’s aggregate is within-one-bucket acceptance accuracy, not exact 4-way match. Triage priority is an ordinal scale, so the user-facing question is “how far off”, not “exact match”:
A prediction is credited when it is exact or an adjacent bucket (|rank(pred) − rank(expected)| ≤ 1) — what a user feels (nothing urgent buried in low-priority). PERSONAL is not a priority level and is absent from the corpus, so it is scored exact-only; re-place it on the scale if the corpus gains PERSONAL examples. Exact 4-way agreement (~40% on a 4B on-device model) is below the realistic ceiling — single-rater human agreement on subjective priority is only ~60–75% — so the 80% bar (#1437) is on this acceptance metric, with these reported (non-gating) secondaries:
  • urgent_vs_not_accuracy — binary accuracy on the needs-attention axis ({URGENT, NEEDS_RESPONSE} vs rest).
  • urgent_recall — recall on that axis; the input to the gate’s anti-gaming URGENT floor.
  • category_accuracy — exact 4-way match, kept as a reference.
The secondaries are recorded with weight 0, so they are displayed but excluded from aggregate.value (which stays round(100 × within_one_bucket_accuracy, 2), recomputable from the displayed components per the formula below).

Trustworthy numbers — variance & CI (#1894)

The corpus + greedy decoding (temperature=0.0) are fixed, and the needs_llm routing is a deterministic heuristic, so the only run-to-run noise is GPU floating-point non-determinism (not seedable — measured, not removed). gaia eval benchmark --experiments N runs N times over the same corpus and the adapter records an additive acceptance_variance block (mean / stdev / CV% / 95% CI / n_runs) in recipe.config. It never affects aggregate.value (the mean); it lets the gate tell a real regression from noise.

File format

Scorecard files are Markdown with YAML front matter (.md). The front matter holds all machine-readable fields; the body is a human-readable summary with a worked recomputation and a Reproduction section.

Required fields

A scorecard missing any of these is invalid and will be rejected by the release gate:

Optional fields

These blocks are optional and additive — a scorecard is valid with or without them, and none of them ever affects aggregate.value. They add reviewer-facing detail about how the number was produced and where the misses are: Token accounting rows (results.performance, issue #1891) — real LLM token counts including the nested per-email classify calls the outer-turn stats used to miss:
tokens_per_triage is reported, not gated: do not add a release-gate bar for it without a committed, ctx-stamped baseline scorecard to compare against (see _tokens_per_triage_comment in tests/fixtures/email/quality_gate_thresholds.json).
The email-triage adapter fills both in automatically. For a new agent, set ResultPayload.environment / ResultPayload.breakdown when your harness exposes the data, and omit them otherwise.

Two counts — defined distinctly

recipe.dataset.size and results.test_cases_run are intentionally separate fields:
  • recipe.dataset.size — total labeled examples available in the dataset (fixed for a given dataset version).
  • results.test_cases_run — the subset actually executed in this run (may be limited by --limit). Must be ≤ recipe.dataset.size.
They may be numerically equal (when the full dataset is run), but they represent different things.
Comparability depends on a consistent --limit. Future regression checks compare aggregate scores. If one run uses --limit 12 and the next uses --limit 100, the scores may differ for reasons unrelated to model quality. Record the exact limit in recipe.config and keep it consistent across versions.

Aggregate score formula

where each valueᵢ is a metric value in [0, 1] and each weightᵢ defaults to 1.0. The result is a percentage in [0, 100]. For a single metric with weight 1.0:
A reader can reproduce this value from aggregate.components alone — no eval-harness access needed. The aggregate.formula field in the front matter states the formula in human-readable form so it is self-documenting.

Storage convention

Each agent package ships a single SCORECARD.md file, updated in place per release — the same way README.md works. Per-version uniqueness comes from the publish snapshot (R2 stores the file at agents/<id>/<version>/SCORECARD.md; the npm package ships only the current version’s SCORECARD.md).
The doc-root is the location of the agent’s canonical README: The relative link ./SCORECARD.md resolves both in-repo and when the directory is published as an npm package. The npm files array includes SCORECARD.md (not a scorecards/ directory).

Versioning policy

Patch releases — carry forward

For a patch release (same major.minor, patch incremented), the prior version’s results are carried forward verbatim using carry_forward(). Pass the path to the agent’s current SCORECARD.md:
The resulting scorecard has inherited_from: "0.2.4" and identical results and aggregate fields. The aggregate score is unchanged, so the release gate’s equal-score case passes. carry_forward() reads the prior version from the agent.version field in the front matter — not from the filename.

Minor / major releases — re-run required

For a minor or major bump, carry_forward() raises ValueError with a “re-run” message. Run the eval fresh and generate a new scorecard:
This writes hub/agents/npm/agent-email/SCORECARD.md in place.

Release gate

scorecard_gate.py is a standalone script that exits non-zero on failure:
--baseline-file and --baseline-ref are mutually exclusive. If the file doesn’t exist at the given ref, the gate treats it as first adoption (presence-only pass).

Gate logic

  1. Presence check: --scorecard path must exist and be a valid scorecard. → exit 1 if not.
  2. Absolute bar + anti-gaming floor (opt-in, applies to every path including first adoption):
    • --min-aggregate <N>: exit 1 if aggregate.value < N (the #1437 80% bar is --min-aggregate 80).
    • --min-urgent-recall <r>: exit 1 if the card’s urgent_recall secondary is below r (a high aggregate must not come with buried urgent mail). Fails loud if the metric is absent.
    • Omit both for report mode (presence + regression only). The email release workflow reads these from the thresholds manifest (acceptance_target, urgent_recall_floor, acceptance_enforce) — data, not code.
  3. Baseline resolution:
    • --baseline-file: read the given file directly (no git access; suitable for unit tests).
    • --baseline-ref: resolve via git show <ref>:<scorecard-path>. If the file does not exist at that ref → first adoption, exit 0.
    • Neither specified: first adoption, exit 0 (presence-only pass).
  4. Regression check (variance-aware, #1894): if the baseline records a within-one stdev, a regression is flagged only when candidate < baseline − k·stdev (--regression-k, default 1; stdev is on the [0,1] scale, scaled ×100 to match aggregate.value). With no recorded stdev, a strict < is used.
  5. Equal or above the threshold → exit 0.

Exit codes

--allow-regression

When a regression is intentional (e.g. a dataset correction or methodology change), use --allow-regression. The gate prints a GHA ::warning:: annotation naming both versions and scores, then exits 0:

Keeping the scorecard current (the update / reject loop)

The scorecard must move with the agent: when LLM-affecting code changes, the eval is re-run and the committed SCORECARD.md refreshed — upward. A regression is blocked. Two enforcement points work together:
  1. Reject-on-worse (always on, GitHub-hosted). The scorecard-gate job in release_agent_<id>.yml runs on every release. It only parses committed files (no eval), so it runs on a standard runner and fails the build if the committed scorecard regressed below the prior version or is missing. This is the hard gate.
  2. Run-and-refresh (self-hosted AMD, manual dispatch). gaia eval benchmark needs Lemonade on AMD hardware, so it cannot run on GitHub-hosted runners — it runs on the [self-hosted, Windows, stx] pool, serialized against the other evals by the shared lemonade-eval concurrency group. The Email Agent Eval — scorecard refresh workflow (.github/workflows/email_scorecard_refresh.yml) is workflow_dispatch-only (#2094 — it previously also ran on pushes touching the email agent, but a full eval costs ~10.5h and that trigger never once completed a run), and takes two profiles:
    • full (limit ≥ 249 and experiments ≥ the committed card’s n_runs) — reproduces the committed card’s methodology and is the only profile allowed to commit a refreshed card;
    • subset (limit < 249) — an end-to-end smoke run that exercises every step and then never commits (a small-sample number is not a valid stand-in for the published figure).
    A full run then either commits the refreshed card (score clears the absolute bar and shows no regression beyond the recorded noise band) or fails loudly (regression, or a card below the acceptance bar / urgent-recall floor from tests/fixtures/email/quality_gate_thresholds.json). Before any eval spend, the run also fails fast if its basis (ctx_size, experiments) differs from the committed card’s — unless rebaseline=true (with a required rebaseline_reason) marks the basis change deliberate.
So the published number always traces to a full-corpus run, and the release gate is the backstop on hosted CI. Locally, gen_scorecard.py + scorecard_gate.py reproduce both steps (see the adding-eval-scorecard skill).
The refresh job needs contents: write and runs only on the repo’s own branches — a fork PR’s GITHUB_TOKEN is read-only and cannot auto-commit. For a fork PR, run the eval locally/on AMD hardware and commit the scorecard manually; the release gate still enforces no-regression.

Adding a scorecard for a new agent

Use the adding-eval-scorecard skill. In Claude Code, invoke it instead of following these steps by hand — it carries the exact commands, the harness→payload→generator flow, the headless-eval gotchas (keyring/PYTHONPATH/tool-timeout), and the verification evidence to capture. The steps below are the reference the skill automates.
  1. Write a packaging/gen_scorecard.py adapter (see hub/agents/python/email/packaging/gen_scorecard.py for a reference). The adapter should populate reproduction_command with the exact commands needed to reproduce the scorecard.
  2. Run the eval and call the adapter → commit the resulting SCORECARD.md to <doc-root>/SCORECARD.md.
  3. Link the scorecard from the README: ./SCORECARD.md.
  4. Add SCORECARD.md to the npm package.json files array (if published on npm); do not add a scorecards/ directory.
  5. Wire scorecard_gate into the release workflow (see release_agent_email.yml for the job topology). Use --scorecard <path>/SCORECARD.md and --baseline-ref <prev-tag> (best-effort).