Make the eval harness runnable, then run it - #97
Open
Defaultuser361 wants to merge 4 commits into
Open
Conversation
Two ways the harness graded something other than what it claimed to. Both surfaced on the first real run; neither is visible without executing it. Working directory: the runner subprocess inherited `cwd=ROOT`, so an agent CLI adopted this checkout as project context and answered prompts by inspecting the harness instead of the task. One baseline response opened by identifying `scripts/run_evals.py` and emitted tool-call syntax for tools it did not have. Over a 14-case pass it hit 8 baseline responses and 2 candidate ones -- the asymmetry favours whichever condition discourages exploration, so it lands directly on the number under test. The runners already pass `--setting-sources ""` for exactly this reason; the working directory was an unguarded channel for the same class of leak. Both conditions now run in an empty scratch directory. Frontmatter: `_condition_prompt` injected SKILL.md verbatim, so the candidate prompt carried `disable-model-invocation: true` and the `hermes` metadata block. hooks/always-on.sh strips that frontmatter before injecting the ruleset, so the eval was grading text that never reaches a user. `_strip_frontmatter` mirrors the hook's awk -- byte-identical output on the current SKILL.md -- and leaves a file alone if it opens a block without closing it, rather than emptying it. Tests cover both: the working-directory one uses a stub runner that reports its own `pwd`, and fails against the previous behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`score` aggregates rows a human already wrote, so running the harness end to end meant hand-scoring every response across five dimensions -- 420 numbers for the documented 3-trial pass. That is the step that kept the harness unrun. scripts/judge.py grades a whole `(case, trial)` group in one call, so conditions are compared against each other rather than scored in isolation. Blinding is structural rather than a convention the grader is asked to honour: conditions are relabelled A/B/C and the label order is permuted per group from a digest of the group key, so position carries no signal and a resumed run reproduces the labels it used before. Over the 42 groups of a 3-trial pass the candidate landed in each position 19/23 times. Only the region of rubric.md between the new judge:begin/judge:end markers reaches the grader. The release-gate rules below them name both conditions, so sending the whole document to a blind grader would hand over the exact vocabulary the blinding exists to hide -- caught by the end-to-end test, after every unit test had passed on a prompt that was still leaking. The prompt goes in on stdin. Appending it as a trailing argument let the claude runner's `--tools ""` consume it as that option's value; generation only escapes this because `--max-budget-usd` happens to sit in between. Stdin also removes an argv ceiling that matters when prompts embed whole responses. Graders occasionally drop a field or wrap JSON in prose. Verdicts are validated with the offending case named, retried on the same budget as a failed process, and if still unusable the group is skipped, reported on stderr, and the exit code is non-zero -- one bad reply should not discard the groups already written or the ones queued behind it. Groups missing a condition are reported the same way rather than dropped, since conditions are only comparable when judged on identical rows. Stdlib only, matching the existing scripts. 18 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tests/ was not referenced by any workflow, so the scoring maths, the release gate, and the pairing checks could regress with CI green. Uses stdlib unittest so no dependency is added; matrix covers 3.11 and 3.12. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 cases, 3 trials, claude-opus-4-8, Claude Code 2.1.220. Weighted 4.045 -> 4.473; every dimension improves, including correctness (+0.190) and safety (+0.024). Blocking findings 7 -> 3. The release gate still fails, on "no blocking findings" -- absolute where the neighbouring rules are comparative, so more than halving the count is not enough. Two of the three remaining come from `agent-owned-edit`, which requires acting on a repository while every runner passes `--tools ""` and so cannot be passed by any run. Includes the caveats: three trials is few (per-case SD reaches 0.95), the judge shares a model family with the responses it grades, and 3 of 84 responses carry tool-call syntax as plain text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 2, 2026
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.
The harness in
scripts/is carefully built — paired conditions, blind judging, a weighted rubric, a release gate, resumable runs, and deliberate isolation from operator config — and as far as I can tell it has never been run: no results committed, no numbers in the README, no workflow invoking it.I wanted to know whether the skill actually works, so I tried to run it. This is what that took, and what it found.
Why it couldn't be run as-is
scoreaggregates rows a human has already written. There is no judge, so a 3-trial pass means hand-scoring 84 responses across five dimensions — 420 numbers, blind. That is the step standing between the harness and an answer.scripts/judge.pyfills it in. It grades a whole(case, trial)group in one call so conditions are compared rather than scored in isolation, and blinding is structural instead of a convention the grader is asked to respect: conditions are relabelled A/B/C with the order permuted per group from a digest of the group key, so position carries no signal and a resumed run reproduces its earlier labels. Stdlib only, matching the existing scripts.Four defects, three of them only visible once it executes
The runner adopted this repo as its working directory. The subprocess inherited
cwd=ROOT, so the agent CLI answered prompts by inspecting the harness. One baseline response opened by identifyingscripts/run_evals.py; another degenerated into narrating tool calls for tools it did not have. Over a 14-case pass it hit 8 baseline responses and 2 candidate ones. The asymmetry is the problem — it favours whichever condition discourages exploration, so it lands on the number under test. The runners already pass--setting-sources ""for precisely this reason; the working directory was an unguarded channel for the same class of leak.The eval graded text that never ships.
_condition_promptinjected SKILL.md verbatim, frontmatter included, so the candidate prompt carrieddisable-model-invocation: trueand thehermesmetadata.hooks/always-on.shstrips that before injecting. The new_strip_frontmattermirrors the hook's awk — byte-identical output on the current SKILL.md.rubric.mdleaked the blinding vocabulary. It ends with "Release the candidate only when… higher than baseline". Feeding the whole document to a blind grader hands over the exact words the blinding exists to hide, plus release-gate rules the grader has no business seeing. Now delimited with<!-- judge:begin -->/<!-- judge:end -->. Worth noting this got through every unit test and was caught only by the end-to-end one.--tools ""ate the judge prompt. Appending the prompt as a trailing argument let the empty-valued flag consume it. Generation only escapes this because--max-budget-usdhappens to sit in between — remove the budget flag from a runner and it breaks the same way. The judge passes the prompt on stdin.Each fix ships with a test that fails against the previous behaviour; the working-directory one uses a stub runner that just reports its own
pwd. CI now runs the suite (stdlibunittest, no new dependency) —tests/was not referenced by any workflow, so the scoring maths and release gate could regress with CI green.Result
14 cases, 3 trials,
claude-opus-4-8, Claude Code 2.1.220. Full numbers and caveats inevals/RESULTS.md.Blocking findings 7 → 3. The candidate wins 10 of 14 cases, ties 2, loses 2.
Every dimension moves in the candidate's favour, including the two that would expose a style rule buying brevity with substance. The gains concentrate in cases about reporting state —
multi-step-progress+2.53 anderror-report+2.40 — while cases with an explicit output contract (code-answer,long-form-request) are unchanged, which is what the escape-hatch clauses are supposed to do.It still fails your release gate, on rule 1. That rule is absolute where its neighbours are comparative, so more than halving the blocker count is not enough to pass. Two of the three remaining come from a case nothing can pass (see the companion issue). Whether that asymmetry is intended seems worth deciding deliberately rather than discovering at a release.
Caveats are in RESULTS.md and I would not want them skipped: three trials is few (per-case SD reaches 0.95), the judge shares a model family with the responses it grades, and 3 of 84 responses still carry tool-call syntax as plain text because the CLI primes tool use even with
--tools "".Notes
RESULTS.mdcommit if you would rather generate the numbers yourself — the other three stand on their own, and the harness is what matters.🤖 Generated with Claude Code