fix(evals): harden the agent-eval gate against LLM-judge flakiness#1625
Merged
Conversation
The `agent eval gate (live)` flaked enough that real PRs were merged manually past it, eroding its real signal (genuine routing regressions — #1299's false 0.44, #1300). Root cause is structural: passRate >= 0.9 over ~25 scenarios (20 capability + 5 canary) tolerates at most ~2 failures, so two borderline scenarios flipping on judge noise sinks the whole gate. EVAL_SAMPLES=3 majority-of-3 (#1306) de-noises per scenario but not the aggregate. The judge runs on claude-opus-4-7, where `temperature` is removed (sending it 400s) — so the judge cannot be pinned deterministic. Per-scenario resampling is therefore the lever, not a quieter judge. - decideScenario: flake-aware escalation. Sample EVAL_SAMPLES (3); if unanimous, stop (cheap common path); if split, resample up to EVAL_MAX_SAMPLES (7), stopping early once a verdict is locked, then decide by majority. Cost stays ≈3 samples/scenario — escalation fires only on the handful that genuinely disagree. Still-split-at-budget scenarios are logged `⚠ borderline` (a human-look signal, not a gate failure). - ci.yml: EVAL_MAX_SAMPLES=7 on the eval-gate job, with rationale. - eval-gate-stability.test.ts: committed regression canary, pure/offline (runs in eval-structure on every PR, no API key). Pins that escalation NEVER rescues a consistently-failing scenario and that a regressed agent trips passRate < 0.9 — so the stabilization can't silently defang the gate. - Justify the 0.9 floor (not replace it) in the assertion, the canary, and evals/README.md; document the mechanism and the deferred alternatives (deterministic-judge model swap, baseline-relative detection). No agent-config update needed: this changes the eval HARNESS only — no tool added/removed, no change to agent-visible data, prompts, or workflows. Goal: de24de25
…bility floors Both stabilizers survive the merge: decideScenario escalation (this branch) now feeds the floors-based gate (G#350/G#352 on main). EVAL_SAMPLES stays 5 (the floors' calibration basis); EVAL_MAX_SAMPLES=9 caps escalation. Cost cap scales with the escalation max. eval-gate-stability.test.ts re-pinned to the floors semantics (capabilityPassRate vs calibrated floor, canaries at 100%, raise-only floor guard).
…t-escalation evidence Two live gate runs on the merged head (EVAL_SAMPLES=5, EVAL_MAX_SAMPLES=9) both measured capabilityPassRate exactly 0.80 with an identical failure set (routing family orch-002/003/005/007) — the run-to-run variance is gone and the remaining failures are consistent prompt weaknesses, not judge noise. Floor = the observed low. Note records the full evidence trail.
…arg opts Merging main into this goal branch surfaced a pre-existing semantic-merge breakage that also has main itself red (f292fa9): #1249 (93719be) gave resumeGoal→resumeGoalBacklog a 4th `opts` arg (claim/leave mode), while #1741 (91792d8) landed the rerouteGoal test asserting resumeGoalBacklog was called with only 3 args. Each passed CI against its own base; together the test sees a trailing {} and fails ("unit tests (shard 2/2)"). Fix the stale expectation to the 4-arg form ("g1","proj","alice",{}), matching its sibling assertions and the real (correct) call. Production code unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oal test expectation
…h's interim 0.80 G#393's Delegation-reply-shape prompt fix removed the routing-family failures this branch had measured as the consistent 0.80 baseline, and its post-fix runs (1.00/1.00/0.95) support the 0.9 default. Keep 0.9 (raise-only) and fold this goal's escalation evidence into the calibration note.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
agent eval gate (live)flaked enough that real PRs were merged manually past it — eroding its real signal (genuine routing regressions: #1299's false 0.44, #1300). Root cause is structural: with an LLM judge that cannot be pinned deterministic (claude-opus-4-7removedtemperature— sending it 400s), a handful of borderline scenarios flip run-to-run and sink a hard aggregate bar.Since this PR was first opened, main shipped the complementary half (G#350/G#352): the gate now asserts capabilityPassRate (capability scenarios only — canaries no longer inflate the denominator) against per-agent calibrated floors (
evals/capability-floors.json; senior-pm 0.75 = its measured post-fix low at EVAL_SAMPLES=5). This PR is now merged with main and the two mechanisms compose: the floor calibrates where the bar sits; escalation removes the run-to-run variance around it.Change (no new infra, judge model unchanged)
decideScenario— flake-aware escalation (evals/judge/runner.ts): sampleEVAL_SAMPLES(5); if unanimous, stop (the cheap common path); if split, resample in batches up toEVAL_MAX_SAMPLES(9), stopping early once a verdict is mathematically locked, then majority-decide. Cost stays ≈5 samples/scenario — escalation fires only on scenarios that genuinely disagree. Still-split-at-budget scenarios are logged⚠ borderline(a human-look signal, not a gate failure).ci.yml: keepsEVAL_SAMPLES=5(the floors' calibration basis — don't change one without the other) and addsEVAL_MAX_SAMPLES=9, both documented in place.agent-eval.test.ts: keeps main's floors-based assertions; the per-scenario cost cap now scales withEVAL_MAX_SAMPLES(an escalated scenario legitimately accrues up to 9 samples; the per-sample cap stays $0.50).eval-gate-stability.test.ts— committed regression canary, pure/offline (runs ineval-structureon every PR, no API key, even on fork PRs). Re-pinned to the floors semantics: escalation never rescues a consistently-failing scenario; a regressed senior-pm (6 routing scenarios broken → 0.70) trips its 0.75 floor; a default-floor agent with 3 consistent failures trips 0.9; canaries get no slack from the floor (one failing canary is red regardless); and a raise-only guard fails if anyone lowers the senior-pm floor below its measured 0.75 without consciously updating the evidence.evals/README.md: documents the escalation mechanism, how it composes with the floors, and the procedure for raising a floor from ≥2 measured live runs.Acceptance criteria
Out of scope (deferred)
Removing/advisory-fying the gate; rewriting agent prompts; baseline-relative regression infra; switching the judge model for determinism.
Agent config
No PM Agent config update needed — this changes the eval harness only (no tool added/removed, no change to agent-visible data, prompts, or workflows).
Validation
Offline agent-eval suite green in the worktree (293 passed, 30 live-skipped,
EVAL_MODE=sample);npm run typecheckclean; diff vs main touches exactly the 5 eval-gate files.Goal:
de24de25