Skip to content

fix(evals): harden the agent-eval gate against LLM-judge flakiness#1625

Merged
psdjungpulzze merged 9 commits into
mainfrom
goal/de24de25
Jul 4, 2026
Merged

fix(evals): harden the agent-eval gate against LLM-judge flakiness#1625
psdjungpulzze merged 9 commits into
mainfrom
goal/de24de25

Conversation

@pulzzejason

@pulzzejason pulzzejason commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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-7 removed temperature — 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): sample EVAL_SAMPLES (5); if unanimous, stop (the cheap common path); if split, resample in batches up to EVAL_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: keeps EVAL_SAMPLES=5 (the floors' calibration basis — don't change one without the other) and adds EVAL_MAX_SAMPLES=9, both documented in place.
  • agent-eval.test.ts: keeps main's floors-based assertions; the per-scenario cost cap now scales with EVAL_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 in eval-structure on 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

  • Merged with main, both mechanisms intact — escalation + capabilityPassRate/floors coexist; no scenario deleted, no canary weakened, no floor lowered.
  • Catches real regressions — offline canary proves a regressed config trips its floor every run and escalation can't flip a real fail green.
  • Cost bounded — only non-unanimous scenarios escalate; early-lock stops before the cap; cost cap scales with the escalation max.
  • Post-merge live stability measured (task feat(engine): daemon-token goal PATCH can amend spec fields (Slice 1 of goal-spec parity) #1383): two live gate runs on the merged head both measured senior-pm capabilityPassRate exactly 0.80 (16/20) with an identical failure set (routing family orch-002/003/005/007) — variance eliminated, remaining failures consistent. Floor raised 0.75 → 0.80 (the measured low), evidence recorded in the floors note.

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 typecheck clean; diff vs main touches exactly the 5 eval-gate files.

Goal: de24de25

pulzzejason and others added 9 commits June 30, 2026 12:44
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>
…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.
@psdjungpulzze psdjungpulzze merged commit f365a15 into main Jul 4, 2026
10 checks passed
@psdjungpulzze psdjungpulzze deleted the goal/de24de25 branch July 4, 2026 00:39
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.

2 participants