Skip to content

feat(judge): support judge-specific skills#141

Draft
roark47 wants to merge 22 commits into
mainfrom
codex/implement-proposal-0002
Draft

feat(judge): support judge-specific skills#141
roark47 wants to merge 22 commits into
mainfrom
codex/implement-proposal-0002

Conversation

@roark47

@roark47 roark47 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add judge.skills to agent_judge configuration and validation.
  • Install judge-specific Skills on the judge agent before grading, isolated from the run agent and preserved in benchmark without_skill runs.
  • Require judge Skill usage in the agent judge prompt without concatenating Skill file contents.
  • Surface judge Skill metadata in JSON, HTML, and JUnit reports.
  • Document the feature in English/Chinese guides, custom engine notes, and skill-upper references.

Impact

This lets eval authors keep long or reusable grading rubrics in judge-only Skills while preserving the measured Skill-under-test environment. Existing configs without judge.skills keep their previous behavior.

Validation

  • make fmt
  • GOCACHE=/Users/kongtang/Code/skill-up-proposal-0002/.cache/go-build make verify
  • GOCACHE=/Users/kongtang/Code/skill-up-proposal-0002/.cache/go-build make test

Note: gh auth status reports expired local tokens, so the PR was created through the GitHub connector after pushing the branch with git.

roark47 added 22 commits July 6, 2026 14:07
Add schema types and validation rules for multi-turn conversation evaluation:

Schema changes (internal/config/schema.go):
- Turn.Capture []CaptureRule for variable extraction from responses
- Turn.TimeoutSeconds for per-turn timeout control
- PostCondition.MustContainAll and MustNotContain fields
- CaptureRule type (Variable, Pattern, JSONPath)
- TurnResponseContainsRule, TurnResponseNotContainsRule
- ToolCalledInTurnRule, ToolNotCalledInTurnRule
- Rule struct extended with 4 per-turn assertion fields

Validation changes (internal/config/validator.go):
- Reject ambiguous input.prompt + input.turns combination
- Enforce input.turns[*].role must be "user"
- Validate post_condition.on_fail enum (fail, skip_remaining)
- Require at least one check in post_condition
- Validate capture variable identifier pattern
- Enforce exactly one extractor per capture rule
- Compile-time regex validation for capture patterns
- Per-turn judge rules: turn number range checks
- Required fields for turn_response_contains/not_contains
- Required name for tool_called_in_turn/tool_not_called_in_turn

All existing tests pass unchanged (backward compatible).
Introduce SessionResumer optional interface and SessionResult.SessionID field
to support multi-turn conversation evaluation:

Interface (internal/agent/agent.go):
- SessionResult.SessionID string field for session tracking across turns
- SessionResumer interface with RunTurn method
- Compile-time satisfaction checks for ClaudeCodeAgent, QoderCLIAgent

ClaudeCodeAgent (internal/agent/claude_code.go):
- Run populates SessionResult.SessionID from generated UUID
- RunTurn: empty sessionID delegates to Run (first turn)
- RunTurn: non-empty sessionID uses --resume flag
- buildClaudeResumeCmd for session resume commands
- Extract claudeRunEnvVars and handleClaudeRunResult helpers

QoderCLIAgent (internal/agent/qodercli.go):
- buildSessionResult extracts SessionID from session JSONL filename
- RunTurn: empty sessionID delegates to Run (first turn)
- RunTurn: non-empty sessionID uses -r flag
- buildQoderResumeCmd for session resume commands

Utilities (internal/agent/session_lookup.go):
- extractSessionIDFromPath strips directory and extension

All existing tests pass unchanged (backward compatible).
Implement the evaluator multi-turn engine that orchestrates per-turn
agent execution with post-condition gates, variable capture, and
template substitution:

Types (internal/evaluator/multiturn.go):
- TurnStatus: completed, skipped, failed, error
- TurnResult: per-turn outcome with response, transcript, captured vars
- multiTurnState: mutable state for the multi-turn loop

Engine (internal/evaluator/multiturn.go):
- executeMultiTurn: main loop — template substitution, agent invocation,
  post-condition evaluation, variable capture, result aggregation
- executeSingleTurn: per-turn timeout + resumer invocation
- buildTurnResult: construct TurnResult from agent SessionResult
- handlePostCondition: on_fail=fail (abort) / skip_remaining (mark rest)
- evaluatePostCondition: must_contain_all/any, must_not_contain checks
- captureVariables: regex named group (?P<value>...) or first group
- substituteTemplate: {{variable}} replacement with unresolved error
- buildAggregateResult: merge per-turn metrics into final SessionResult
- multiTurnStatus: derive case status from turn outcomes

Integration (internal/evaluator/evaluator.go):
- EvalResult.TurnResults []TurnResult field for per-turn data
- executeCaseOnce branches to multi-turn when len(turns)>1 AND agent
  implements SessionResumer; non-resumer agents fall through to the
  existing batch Run path (backward compatible)

Tests (internal/evaluator/multiturn_test.go):
- TestSubstituteTemplate (6 cases)
- TestEvaluatePostCondition (8 cases)
- TestCaptureVariables (6 cases)
- TestMultiTurnStatus (5 cases)
- TestExecuteMultiTurn_HappyPath
- TestExecuteMultiTurn_PostConditionFail
- TestExecuteMultiTurn_PostConditionSkipRemaining
- TestExecuteMultiTurn_TemplateSubstitution
- TestExecuteMultiTurn_AgentError
- TestExecuteMultiTurn_NoResumer
- TestExecuteMultiTurn_UnresolvedTemplate
Post Conditions:
- extractCaptureValue rejects multiple unnamed capture groups (requires
  exactly one or a named group 'value')

Capture Variables:
- Add comprehensive test coverage for edge cases: multiple unnamed groups
  error, case-sensitivity in post_condition matchers, named group priority

Tests:
- TestCaptureVariables_EdgeCases: multi-group error, named+unnamed priority
- TestPostConditionCaseSensitivity: explicit case-sensitive assertion
- TestExtractCaptureValue: unit tests for the extraction helper
- Split long test functions to satisfy funlen linter (80 stmt limit)

Lint:
- Replace fmt.Errorf (no formatting) with errors.New
- gofumpt formatting fixes
Judge Input:
- Add InputTurnResult type with TurnNumber, Content, Response,
  Transcript, Status, and Reason fields
- Add TurnResults []InputTurnResult field to judge.Input

Per-turn rule evaluators (internal/judge/rule_based.go):
- turn_response_contains: check contains_all/contains_any on a
  specific turn's response
- turn_response_not_contains: check forbidden keywords absence
- tool_called_in_turn: verify tool usage with optional partial args
- tool_not_called_in_turn: verify tool absence in a turn
- Shared lookupTurn helper: validates turn existence and completed
  status before content assertions

Evaluator wiring (internal/evaluator/evaluator.go):
- Add toJudgeTurnResults converter from evaluator.TurnResult to
  judge.InputTurnResult
- Pass TurnResults in judge.Input within evaluateCaseSession

Tests:
- 16 new test cases covering all 4 per-turn rules: positive/negative
  matching, missing turns, skipped turns, failed turns, args matching
- Table-driven patterns to satisfy dupl linter
CaseResult (internal/report/reporter.go):
- Add TurnResults []CaseTurnResult field (omitempty for backward compat)
- Define CaseTurnResult with TurnNumber, Content, Response, Status, Reason

JSON report:
- turn_results automatically serialized in result.json/report.json
- Omitted for single-turn cases (nil slice + omitempty)

HTML report (internal/report/html.go):
- Add embeddedTurn type and TurnResults field to embeddedCase
- caseTurnResultsToEmbedded converts report types to embedded JS data

JUnit report (internal/report/junit.go):
- buildFailureBody appends non-completed turn status/reason lines
- Turn-scoped assertion texts already contain turn numbers for CI

Runner wiring (internal/runner/runner.go):
- evalResultToCaseResult passes TurnResults via evalTurnResultsToReport
- Converter maps evaluator.TurnResult to report.CaseTurnResult
writing-evals.md (EN + ZH):
- New 'Multi-turn conversations' section covering input.prompt vs
  input.turns decision matrix, post_condition gate semantics,
  capture/template variable syntax, agent support matrix, and
  per-turn judge assertion examples.

e2e/testdata/multi-turn-conversation:
- phase-gate-skip-attempt.yaml: convert fake single-prompt to real
  input.turns with post_condition + turn_response_contains assertions.
- multi-turn-refinement.yaml: split into 2 real turns with capture
  and per-turn assertions.

skills/skill-upper:
- references/case-yaml.md: expand multi-turn section with capture
  field reference and per-turn judge assertion examples.
- assets/case.yaml.tmpl: add capture and per-turn judge commented
  examples.

CHANGELOG.md:
- Add [Unreleased] entries for multi-turn conversation feature,
  per-turn assertions, session resumption, and report TurnResults.
CodexAgent now implements the SessionResumer interface, enabling native
multi-turn conversation support via `codex resume <thread-id>`.

codex.go:
- Add RunTurn method: first turn (sessionID=="") delegates to Run;
  subsequent turns build a `codex resume --json` command with the
  thread ID obtained from the first turn's event stream.
- Set SessionResult.SessionID = streamParsed.threadID in buildSessionResult
  so the evaluator can pass it forward between turns.
- Add buildCodexResumeCmdWithLastMessage helper (mirrors the exec variant
  but uses `codex resume` subcommand with sessionID positional arg).

codex_test.go:
- TestCodexRunTurn_FirstTurnDelegatesToRun: verifies first turn uses
  `codex exec` and extracts thread ID into SessionID.
- TestCodexRunTurn_ResumeUsesCorrectCommand: verifies subsequent turns
  use `codex resume` with the session ID and instruction.
- TestBuildCodexResumeCmdWithLastMessage: unit test for command builder.

Docs: update agent support matrix in writing-evals.md (EN+ZH) and
CHANGELOG.md to reflect Codex multi-turn support.
The top-level 'codex resume' command is a TUI-only subcommand that does
not support --json, --skip-git-repo-check, or --output-last-message.

Switch to 'codex exec resume' which is the non-interactive equivalent
supporting all required flags. Remove the --sandbox flag from resume
commands since the session inherits sandbox settings from its initial
creation; use --dangerously-bypass-approvals-and-sandbox to ensure
non-interactive execution.
When assertions pass, the evidence field now includes the actual
keywords that were checked, making it possible to audit results
without referring back to the case YAML.

- turn_response_contains: evidence shows contains_all/contains_any lists
- turn_response_not_contains: evidence shows the forbidden keywords list
- output_contains: evidence shows all/any/not keyword lists
…lure

multiTurnStatus previously treated all TurnFailed results as hard
failures, skipping judge execution entirely. This broke the
on_fail=skip_remaining contract which is designed to skip subsequent
turns but still run the judge on whatever data was collected.

Distinguish the two modes by checking whether TurnSkipped entries
follow the failed turn — if so, it is a skip_remaining case and
the evaluator proceeds to judge evaluation.
- Show per-turn Prompt/Response cards when turn_results is available,
  replacing the previous single prompt/response view
- Synthesize FORMAL GRADES from turn post-condition failures when the
  judge was not executed (grading is null but turn_results has failures)
- Hide the top-level Prompt section for multi-turn cases to avoid
  showing only the first turn's content
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.

1 participant