Optimize Claude Code agent skills using GEPA (Greedy Evolutionary Prompt Adaptation) with real execution scoring.
Each candidate SKILL.md is tested on actual tasks via claude -p, OTEL traces are scored by an LLM judge against expected findings, and GEPA's Pareto frontier selects improvements that don't regress other tasks.
pip install -r requirements.txtRequired credentials:
- Vertex AI:
gcloud auth application-default login(for GEPA reflection LM) - LLM judge API key (e.g.,
export OPENAI_API_KEY=...for OpenAI models) - Claude CLI installed and authenticated
- Claude Code SessionEnd hook configured (
.claude/hooks/log-session-id.sh)
# Start MLflow
mlflow server --host 0.0.0.0 --port 5000 &
# Run optimization
python scripts/optimize.py <target-skill-dir> \
--config config.yaml \
--project-root <project-root>Output saved to <target-skill-dir>_Optimized/ with SKILL.md and benchmark.json.
GEPA select task → reflect on feedback → mutate SKILL.md → run claude -p trials
→ match OTEL traces by session_id → LLM judge scores → Pareto accept/reject
Per iteration:
- GEPA picks a train task minibatch (epoch-shuffled)
- Runs N trials, scores traces, builds CRITICAL/WEAK/OK feedback per dimension
- Maps weak dimensions to relevant SKILL.md sections via keyword matching
- Reflection LM reads feedback + section mapping + reference files, proposes targeted edits
- Runs N trials with mutant on same tasks — accept if better
- If accepted, runs on held-out test tasks for validation
- Pareto frontier keeps the mutant only if it improves without regressing
quality = Σ(dim_score × weight) / (total_weight × 5.0) # from traces via LLM judge
static = conciseness + reduction + code_preserved # from SKILL.md text
overfit = min(0.15, count(task_specific_terms) × 0.01) # anti-memorization
blended = 0.75 × quality + 0.25 × static - overfit # final (0.0–1.0)
The LLM judge (configurable via judge.model, any litellm-supported model) reads the full OTEL trace spans (tool calls, outputs, reasoning). Each trace matched by session_id written by a Claude Code SessionEnd hook. Failed/unscored trials are excluded from stats with specific skip reasons logged to MLflow.
Multiple candidates kept — each best on different tasks. Mutants accepted only if they push the frontier forward without being worse on all tasks.
config.yaml # All configuration (models, scoring, tasks, paths)
requirements.txt # Python dependencies
scripts/
optimize.py # Main optimizer — SkillOptimizerAdapter + GEPA + scoring
run_parallel.py # Parallel trial runner — spawns N claude -p processes
start-traced.sh # OTEL tracing wrapper — exports TRIAL_ID for session matching
Edit config.yaml:
runner:
command: "claude -p" # agent CLI
skill_dir: ".claude/skills" # where skills live
optimization:
trial_timeout: 1800 # 30 min per trial
runs_per_task: 2 # N runs for LCB scoring
skillopt:
model: "vertex_ai/claude-opus-4-6"
max_iterations: 3 # GEPA reflect-mutate-check cycles
reflection_minibatch_size: 2 # train tasks per batch
pass_traces_to_reflection: true # include trace conversations in reflection feedback
scoring:
quality_weight: 0.75
static_weight: 0.25
judge:
model: "openai/gpt-4o" # any litellm-supported model
quality_dimensions:
task_completion: { weight: 3.0, definition: "..." }
correctness: { weight: 3.0, definition: "..." }
# ...
task_suite:
- name: "task-1"
prompt: "Your task prompt"
split: train
- name: "task-2"
prompt: "Held-out task"
split: testEach task needs expected findings for reference-based scoring:
{
"skill_name": "my-skill",
"evals": [
{
"name": "task-1",
"prompt": "...",
"expected_findings": [
"Root cause: X failed due to Y",
"Remediation: do Z"
]
}
]
}Three types of MLflow runs are created during optimization:
One per claude -p session. Contains agent performance and judge scores.
| Metric | Range | Description |
|---|---|---|
quality_composite |
0-1 | Weighted average of all dimension scores: Σ(score × weight) / (total_weight × 5.0) |
quality_task_completion |
1-5 | Did the agent complete the task and produce expected outputs? |
quality_correctness |
1-5 | Are conclusions correct and supported by evidence? |
quality_reasoning_quality |
1-5 | Was the investigation logical and evidence-based? |
quality_tool_mastery |
1-5 | Were tools used appropriately and efficiently? |
quality_efficiency |
1-5 | Was the investigation focused without redundant steps? |
quality_communication |
1-5 | Is the output well-structured and actionable? |
quality_autonomy |
5.0 | Fixed (automated runs) |
quality_safety |
5.0 | Fixed (read-only tools) |
tool_calls |
int | Number of tool invocations in the session |
llm_requests |
int | Number of LLM turns |
total_tokens |
int | Total tokens consumed |
cache_read_tokens |
int | Tokens served from prompt cache |
Tags: task_name, scored_from (trace), trial_id, type (trial or trial-skipped), skip_reason (if skipped)
One per evaluate() call. Aggregates trial scores with LCB noise elimination.
| Metric | Description |
|---|---|
task_<name>_mean |
Average quality composite across N runs for this task |
task_<name>_std |
Standard deviation between runs |
task_<name>_ci95 |
95% confidence interval: 1.96 × std / √N |
task_<name>_lcb |
Lower confidence bound: mean - ci95 |
task_<name>_blended |
Final score: 0.75 × lcb + 0.25 × static - overfit |
task_<name>_n |
Number of runs scored (excludes skipped trials) |
quality_lcb_mean |
Average LCB across all tasks |
blended_mean |
Average blended score — what GEPA uses for comparison |
static_score |
Skill file quality (conciseness, structure, code preservation) |
overfit_penalty |
Penalty for task-specific terms found in skill |
trials_skipped |
Number of trials excluded from stats |
dim_<name>_mean |
Average dimension score across all runs |
dim_<name>_std |
Dimension score variance |
One per optimization invocation. Tracks Pareto frontier and best candidates.
| Metric | Description |
|---|---|
base_program_full_valset_score |
Seed skill's blended score on held-out test tasks |
best_score_on_valset |
Best candidate's aggregate valset score |
iteration |
GEPA cycle count (reflects-mutate-check cycles completed) |
total_metric_calls |
Total individual task evaluations consumed |
subsample_score |
Current-best's sum of blended scores on last train batch |
new_subsample_score |
Last mutant's sum of blended scores on same train batch |
selected_program_candidate |
Which candidate GEPA selected for this cycle |
valset_pareto_front_agg |
Best aggregate score across the Pareto frontier |
linear_pareto_front_program_idx |
Which candidate leads the frontier |
Decision logic: If new_subsample_score > subsample_score → mutant accepted → valset recheck → Pareto gate.
Apache-2.0