Multi-phase Evaluation
Multi-phase Evaluation
Multi-phase evaluation lets you chain multiple harness invocations in a single task, with different models assigned to different phases. The most common pattern is adversarial review: an implementation model produces a fix, a more capable reviewer model critiques the diff, and the implementation model revises based on the feedback.
When to use it
- Adversarial review: A cheaper/faster model implements, a more expensive model reviews. Does the review improve quality enough to justify the cost?
- Iterative refinement: Implement → review → revise → review → revise. Does a second revision pass improve results?
- Self-correction: The same model reviews its own work. Set the review phase’s
model_role: implementation(instead ofreview) so the implementation model runs it, or list the same model twice with different roles. Does self-review help?
Multi-phase is not a replacement for the open-ended LLM judge. The judge evaluates the final output post-hoc; multi-phase review feeds feedback back to the implementer before evaluation.
How it works
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Phase 1 │────►│ Phase 2 │────►│ Phase 3 │────►│ Evaluation │ │ implement │ │ review │ │ revise │ │ (SWE tests) │ │ model: A │ │ model: B │ │ model: A │ │ │ │ input: none │ │ input: diff │ │ input: │ │ │ │ │ │ │ │ review_ │ │ │ │ │ │ │ │ feedback │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘- Implement (model A): The implementation model fixes the bug. The git diff is captured.
- Review (model B): The reviewer model receives the diff and produces feedback.
- Revise (model A): The implementation model receives the feedback and revises.
- Evaluate: Hidden tests run against the final repository state.
All phases run in the same Docker container so repository state persists. Each phase gets its own gateway trace ID for per-phase cost attribution.
Task design
Define a multi_phase task with a phases list. Each phase has a name, model_role, task_prompt, and optional input:
tasks:- id: my-multi-phase-task name: Bugfix with adversarial review track: multi_phase task_prompt: "Fix the bug" # Required but ignored when phases is set test_command: python -m pytest tests/ test_patch: | diff --git a/tests/test_hidden.py ... phases: - name: implement model_role: implementation task_prompt: |- Fix the off-by-one bug in src/solution.py... input: none timeout_seconds: 300
- name: review model_role: review task_prompt: |- You are an adversarial code reviewer. Review the diff for correctness, security, and edge cases... input: diff timeout_seconds: 300
- name: revise model_role: implementation task_prompt: |- Address the reviewer's feedback. If no issues were found, make no changes... input: review_feedback timeout_seconds: 300See tasks/multi-phase-bugfix-001.yaml for a complete example.
Phase input types
input |
What the phase receives |
|---|---|
none |
Nothing from prior phases. |
diff |
Git diff from the prior implementation phase. |
output |
Stdout + stderr from the prior phase. |
review_feedback |
Stdout + stderr from a prior review phase. |
The injected content is appended to the phase’s task_prompt in a delimited section.
Run design
Assign role: implementation and role: review to your models in the run config:
models: - name: claude-sonnet-5 provider: anthropic api_key_env: ANTHROPIC_API_KEY role: implementation
- name: claude-opus-5 provider: anthropic api_key_env: ANTHROPIC_API_KEY role: reviewThe matrix expands to one cell per implementation × review model pair. With 2 implementation models and 1 review model, you get 2 cells per harness per repeat.
See runs/sample-multi-phase.yaml for a complete example.
Per-phase cost attribution
Each phase gets a trace ID of {cell_id}__phase-{phase.name}. The gateway captures token usage and cost per trace ID, and the runner saves a breakdown to the phase_results SQLite table:
SELECT phase_name, model, total_cost, input_tokens, output_tokensFROM phase_resultsWHERE cell_id = ?ORDER BY id ASC;This lets you answer questions like:
- How much did the review phase cost vs. the implementation phase?
- Did the reviewer’s token usage justify the quality improvement?
- Would a cheaper reviewer model achieve similar results?
Common pitfalls
- Forgetting a review model: If your task has a
reviewphase but no model withrole: review,build_matrix()raises aValueError. - Duplicate phase names: Phase names must be unique within a task (they’re used in trace IDs and file paths).
- Expecting per-phase
test_command: Tests run only once, after all phases complete. There is no intermediate test step. task_promptis still required: Even though it’s ignored whenphasesis set, the top-leveltask_promptfield is required by the schema.- Container env isolation: Each phase receives its own API key and base URL via
docker exec --env. The container starts with a minimal env — no API keys are baked in. This prevents leaking one phase’s credentials into another. - Pipeline abort: If any phase exits non-zero, the pipeline stops. Implementation-phase changes are committed before the exit-code check; review phases produce no repo changes. The cell is marked as failed in the results store.