fix(grade): match transcript_check tool patterns by descriptor role - #314
Merged
Merged
Conversation
`tool_invocation_matches` applied its regex only to the native invocation rendering, so a frozen `Bash|Read` pattern scored zero across a Codex fleet whose transcripts record `command_execution` — the same behavioral assertion reporting different results because a harness picked a different tool name. Match tool names portably instead, in two stages: the regex runs against the native rendering, and on a miss the run's own descriptor supplies the role its tool name belongs to while the registry-wide vocabulary union supplies every portable spelling of that role. Only the name is substituted, so argument regexes keep their behavior; a tool declared in no role gets no aliases; and `assistant_message_matches` plus `must_precede` are unchanged. Nothing names a harness, so a BYOH descriptor opts in through its `[tools]` table alone. Evidence distinguishes the two: an alias match reports the invocation the harness actually recorded and names the alias and role that matched it, and a miss names the roles whose aliases were tried. Closes #308. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CGVCheSsLsP2jdHhtrtER
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.
Closes #308.
The problem
tool_invocation_matchesapplied its authored regex only to the native invocation rendering. A frozen pattern such asBash|Readmatches Claude Code's tool names but not Codex'scommand_execution, so a Codex fleet run scored 0/10 on every mechanical transcript assertion despite direct evidence of the behavior in all 80 runs. The same behavioral assertion reported different results purely because a harness picked a different native tool name.The descriptor vocabulary was already the portability seam everywhere else —
sandbox::policyanddetect_stray_writesclassify againstall_tool_vocabulary()so "no harness's tool naming is hardcoded here", andmust_precede: first_writewas already role-aware.tool_invocation_matcheswas the one holdout.The change
Tool names now match portably, in two stages:
write/patch/shell/read— validated disjoint per descriptor), and the registry-wide union supplies every portable spelling of that role. Only the name is substituted.Nothing names a harness and no alias list is hard-coded, so a BYOH descriptor opts in through its
[tools]table alone — declaringshell = ["cool_exec"]is the whole opt-in, with no need to cross-list other harnesses' names. No shipped descriptor's[tools]changed.Preserved as-is: argument regexes (arguments are carried through the substitution verbatim),
assistant_message_matches,must_precedesemantics (ordering still classifies against the run's own vocabulary only — the union could call another harness's name a write), and native-name evidence wording. A tool the run's descriptor declares in no role is matched by its native name alone; nothing is invented for it.Before / after
One authored assertion,
"pattern": "Bash|Read", against a Codex run whose only invocation iscommand_execution:Evidence keeps the two kinds of match apart: a native match reads
matched ordinal 0: Bash {"command":"ls"}with no alias mentioned, and an alias match reports the invocation the harness actually recorded alongside the alias and role that matched it. A miss now names the roles that were expanded:Note on granularity
Aliasing is role-wide, so within a role names are interchangeable — on Claude Code a
Globcall now satisfies aReadpattern, since both arereadtools. That is the ticket's stated contract and is inherent to any role-based scheme. The shipped guide documents it and points at the workaround: key the pattern off arguments when tools inside one role must be told apart.Scope audit
Every surface that compares an authored pattern or a tool name was checked.
grade,grade --finalize, andingestall funnel through onefinalize()call site, so the fix covers all three.judge-tasks,compare,aggregate,judge-evidence.md, andvalidateare unaffected; the guard and stray-write audit were already union-based.Tests
src/pipeline/grade/transcript_check/alias_tests.rs: alias matches, native precedence, undeclared names, cross-role isolation, both ordering directions, argument regexes, empty vocabulary, and the roles-expanded miss message.ToolRole/ToolVocabularyunit tests insrc/adapters/harness.rs.tests/cli/grade.rs: oneBash|Readassertion graded throughgrade --finalizepasses on Codex, OpenCode, Cline and Claude Code. OpenCode and Cline declare onlybash/run_commands, so they fail without the union — this is the case that pins the design.tests/run/byoh.rs: a descriptor declaringshell = ["zap_exec"]— a name no other descriptor knows — grades aBash|Readassertion through[tools]alone.conditions.jsonnames no harness matches native names only, with byte-identical pre-change evidence.Documentation
docs/guides/judging.md— new "Portable tool patterns" section (shipped aseval-magic docs judging) with the two-stage rule, a worked example across all four built-ins, the evidence formats, and the role-granularity consequence.docs/guides/byoh.md— what[tools]buys a custom harness, and that cross-listing is neither needed nor wanted.schema/evals.schema.json—transcriptCheck.patterndescription.--helpforgradeandingest; the[tools]comment inharnesses/template.toml.codex-notes.md,cline-notes.md,progressive-enhancements.md.Verification
transcript_check.rscrossed 500 lines, so the new tests were split into atranscript_check/alias_tests.rssibling per the repo's documented extraction pattern; the remaining file is 366 lines of production code plus the pre-existing 165-line inline test module.🤖 Generated with Claude Code
https://claude.ai/code/session_018CGVCheSsLsP2jdHhtrtER