build-evals — the eval builder
!cat .forge/state.json 2>/dev/null || echo NO_STATE
Generate the complete evals/ tree for a target plugin. In new mode the plugin does not
exist yet: tasks derive from design/CONTRACT.md (the PDR), not from code. The suite this
skill emits becomes the definition of "done" for the entire build — treat every task as a
contract clause, not a test.
This skill drafts and lints the suite. It never arms, freezes, or scores it — those are
/plugin-forge:arm-evals and bin/forge-eval. It is model-invocable by design (decision D2):
the forge conductor chains it via the Skill tool; safety comes from the state guard below,
not from invocation control.
State guard (new mode only)
Apply this table to the injected state line above before doing anything else in new mode
(also the default when no argument is given). retrofit and mine skip this guard entirely —
they run standalone against any plugin, with or without a forge pipeline (see their sections).
| Injected state |
Action |
NO_STATE |
REFUSE. Say: "No forge pipeline in this project. Run /plugin-forge:forge to start one, or use /plugin-forge:build-evals retrofit <plugin-path> for an existing plugin." Stop. |
phase is interview or contract |
REFUSE. Say: "The contract is not approved yet — evals derive from it. Run /plugin-forge:forge resume." Stop. |
phase is evals |
Proceed with new mode. |
phase is armed, building, smoke, verify, or ship |
REFUSE edits. The suite is frozen (sha256 in .forge/freeze.json); guard hooks will block writes anyway. Say: "The suite is frozen. Append the desired change to .forge/eval-change-requests.md, then run /plugin-forge:arm-evals to apply it, re-gate, and re-freeze as a new version." Stop. |
Mode: new (pipeline phase)
Work through the steps in order. Load the referenced file before executing its step —
each reference is binding, not advisory.
1. Map the surface from the contract
Read design/CONTRACT.md. Extract, per capability row:
- the chosen composition (skills, hooks, agents, MCP servers, scripts) and its archetype
- the C5 Testability column: grader type, observable signal, and planned task IDs —
this column IS the task backlog; build-evals exists to make those IDs real
- the C1 Trigger column: every auto-invoked component (model-invoked skill, hook,
agent with proactive description) needs positive AND negative trigger tasks
- external dependencies (MCP servers, APIs, corpora) and whether the contract chose live
or mock for each
Also read design/INTENT.md for the runtime-environment answers (headless/cloud/local)
— they constrain what graders can observe.
2. Interview — propose clusters, recommend, WAIT
Never one-shot the suite (interviewing beats one-shot generation — LangChain's central
empirical finding). Use AskUserQuestion to:
- Propose 3–5 testable ability clusters derived from the PDR rows, each with a one-line
description, its archetype, and a rough task count. Recommend one ordering and say why.
WAIT for the user's choice before generating anything.
- For every external dependency, ask live-vs-mock. Default recommendation: mock fixture
stubs for write-heavy, paid, or rate-limited tools; live only for cheap read-only calls
the user explicitly accepts. Record the decision per dependency — the harness stores
which mode each result came from.
For scientific or business domains where fixture corpora need domain-real content, offer
to run the /plugin-forge:skill-forge research pipeline to ground fixtures in real sources instead of
inventing plausible-looking data (decision D10).
3. Scaffold
Copy the scaffold from ${CLAUDE_PLUGIN_ROOT}/templates/evals/ into the target plugin as
evals/, and create the sibling evals-holdout/ next to it (NOT inside evals/). The
scaffold provides registry.yaml, targets/{default,no-plugin,crowded}.yaml,
suites/{capability,regression,triggers}.yaml, judges/{fact,rubric,battle}.yaml,
labels/labels.jsonl, and tasks/_example/. Delete tasks/_example/ after real tasks
exist.
Then vendor the harness runner (stdlib-only, so the suite outlives plugin-forge):
mkdir -p <plugin_dir>/evals/bin && cp "${CLAUDE_PLUGIN_ROOT}/scripts/harness/run.py" <plugin_dir>/evals/bin/run.py && chmod +x <plugin_dir>/evals/bin/run.py
4. Author tasks
Follow references/task-authoring.md for everything about tasks: the exact task.yaml
schema, the 20–50 task budget, the two-experts bar, the REQUIRED reference solution, the
goal-condition grammar for instruction.md, fixture rules, the holdout split, and
elicitation_answers. Every task ID planned in the contract's C5 column must exist in
evals/tasks/ or evals-holdout/tasks/ when this step ends.
5. Choose graders
Follow references/grader-menu.md: deterministic-first hierarchy (workspace → transcript
→ state_check → judge → trigger, plus the simulate_user driver), archetype routing, the
mandatory numeric tolerance, the never-assert-tool-ORDER rule, and the crowded-listing
requirement for trigger tasks.
6. Author judges
Only where the grader menu genuinely runs out of deterministic options. Follow
references/judge-authoring.md: modelgraded-spec YAML shape, one rubric dimension per
isolated judge call, Unknown always legal, pinned cheap judge model different from the
target model, anti-echo phrasing. Seed evals/labels/labels.jsonl with the unlabeled
sample skeleton — calibration itself is arm-evals' gate, not this skill's.
7. Generate the elicitation auto-answer hook (when applicable)
If the target plugin bundles or drives an MCP server that uses elicitation, headless eval
runs will hang or die — no dialog exists under claude -p. For every such server:
- set
elicitation_answers in each task.yaml that exercises it (see task-authoring.md)
- emit
evals/hooks/elicitation-auto-answer.sh (bash, set -euo pipefail, executable) and
evals/hooks/elicitation-settings.json registering it as an exec-form Elicitation hook:
"command": "${CLAUDE_PLUGIN_ROOT}/evals/hooks/elicitation-auto-answer.sh" — harness runs
merge this settings file; production users never load it. The script reads the elicitation
request JSON from stdin, looks up the requested fields in
.forge-eval/elicitation-answers.json (staged into the workspace by the harness from the
task's elicitation_answers), and prints the matching answers object to stdout with exit 0.
No match → one-line stderr message and exit 2, so the trial fails loudly instead of hanging.
8. Lint the suite
Run every mechanical lint in references/antipatterns.md (L01–L16) and fix all findings
before proceeding. L01 (numeric tolerance) and L02 (task-text/grader consistency) are the
builder meta-eval contract: plugin-forge's own dogfood suite feeds this skill adversarial
specs engineered to reproduce the CORE-Bench and METR grader bugs, and passes only if the
emitted suite provably cannot contain them. Do not hand a suite to arm-evals with any lint
red.
9. Smoke-run drafts and hand off
An eval that has never run is not delivered:
- Run
forge-eval reference --task <ID> for at least 3 representative tasks (one per
archetype used) and show the user the graded output. Use forge-eval run --dry-run for a
cost estimate of the full suite; state it plainly — trials × tasks × judges is real money,
and --max-cost-usd exists for a reason.
- Present the suite summary: task count per suite (capability/regression/triggers), holdout
count, judge dimensions, live-vs-mock map, estimated full-run cost.
- Tell the user the exact next command:
/plugin-forge:arm-evals — the four arming gates
(reference-green, red baseline, judge calibration, freeze) all run there.
Do not modify .forge/state.json. The phase advances to armed only when arm-evals
freezes the suite.
Mode: retrofit
Standalone — no forge pipeline, no state guard, works on any installed or in-repo plugin.
- Map the plugin surface from files (there is no PDR): every
skills/*/SKILL.md and
command frontmatter (descriptions = trigger contracts), agents/*.md, hooks/hooks.json
(matchers = firing contracts), .claude-plugin/plugin.json, .mcp.json. Derive one
ability cluster per user-facing capability and one trigger axis per auto-invoked
component. MCP tool checks must use plugin-scoped names:
mcp__plugin_<plugin>_<server>__<tool>.
- Then run steps 2–9 above unchanged, writing
evals/ and evals-holdout/ into the
plugin's own directory. Where step 1 of new mode says CONTRACT.md, use the surface map;
flag any capability whose observable signal is unclear as "unverifiable — needs a design
decision" instead of inventing a grader for it.
- Recommend arming: retrofit suites get the same four gates via
/plugin-forge:arm-evals <plugin-path> before their scores mean anything.
Mode: mine
Standalone — converts captured production failures into tasks (the flywheel).
- Read traces from the generated plugin's trace-capture hook output:
${CLAUDE_PLUGIN_DATA}/traces/*.jsonl (tool args, results, errors per session). If no
traces exist, say so and stop — mine has no other input; suggest confirming the
trace-capture hook is installed.
- Cluster failures (repeated errors, user corrections, wrong outputs) and propose each
cluster as a candidate task via AskUserQuestion — same interview discipline: propose,
recommend, WAIT.
- For each accepted failure, author a task per
references/task-authoring.md with the
real failing input as instruction.md seed, a fixture reconstructing the state, and a
reference solution showing the correct behavior. New tasks go to evals/tasks/ in the
regression suite (they protect against a now-known failure).
- A frozen suite that gains tasks is a NEW suite: finish by instructing the user to run
/plugin-forge:arm-evals to re-gate and re-freeze (registry version bumps, scores stay
comparable). If guard hooks block the writes, that is the freeze working — route the
change through .forge/eval-change-requests.md as the guard message says.
Standing rules (all modes)
- Deterministic-first, always. A judge where a file check would do is a bug.
- Every task ships a reference solution. No exceptions — 0% pass usually means a broken
task, and only the reference proves otherwise.
- Positive AND negative trigger cases for every auto-invoked component; one-sided suites
train over-triggering.
- Golden answers, key-fact lists, and reference solutions live under
reference/, which
the harness stages OUTSIDE the agent-visible worktree. Never copy any of it into
fixtures/ or instruction.md.
- Holdout tasks (
evals-holdout/) are never named, quoted, or summarized in any session
the builder loop can read.
- Track cost honestly:
tracked_metrics are recorded, never graded; suite cost estimates
go to the user before, not after, an expensive run.
- Never emit deprecated shapes: no legacy decision/approve hook JSON, no legacy mount-path
skill references, and MCP tool names always in the full plugin-scoped form
mcp__plugin_<plugin>_<server>__<tool> — never the short server-only form.
References
references/task-authoring.md — task budget, schema, instruction grammar, fixtures, holdout, elicitation fixtures
references/grader-menu.md — grader hierarchy, archetype routing, trigger + crowded target, simulate_user
references/judge-authoring.md — modelgraded spec shape, judge rules, calibration handoff
references/antipatterns.md — reward-hacking catalog with the mechanical lints L01–L16
1---2name: build-evals3description: Generate a runnable eval suite (tasks, graders, judges, targets) for a Claude Code plugin BEFORE it is built. Use for "build evals", "eval suite for my plugin", "test my plugin", "retrofit evals", "mine traces into tasks". In the forge pipeline this is the evals phase; retrofit and mine modes run standalone against any existing plugin.4---56# build-evals — the eval builder78!`cat .forge/state.json 2>/dev/null || echo NO_STATE`910Generate the complete `evals/` tree for a target plugin. In `new` mode the plugin does not11exist yet: tasks derive from `design/CONTRACT.md` (the PDR), not from code. The suite this12skill emits becomes the definition of "done" for the entire build — treat every task as a13contract clause, not a test.1415This skill drafts and lints the suite. It never arms, freezes, or scores it — those are16`/plugin-forge:arm-evals` and `bin/forge-eval`. It is model-invocable by design (decision D2):17the forge conductor chains it via the Skill tool; safety comes from the state guard below,18not from invocation control.1920## State guard (new mode only)2122Apply this table to the injected state line above before doing anything else in `new` mode23(also the default when no argument is given). `retrofit` and `mine` skip this guard entirely —24they run standalone against any plugin, with or without a forge pipeline (see their sections).2526| Injected state | Action |27|---|---|28| `NO_STATE` | REFUSE. Say: "No forge pipeline in this project. Run `/plugin-forge:forge` to start one, or use `/plugin-forge:build-evals retrofit <plugin-path>` for an existing plugin." Stop. |29| `phase` is `interview` or `contract` | REFUSE. Say: "The contract is not approved yet — evals derive from it. Run `/plugin-forge:forge resume`." Stop. |30| `phase` is `evals` | Proceed with new mode. |31| `phase` is `armed`, `building`, `smoke`, `verify`, or `ship` | REFUSE edits. The suite is frozen (sha256 in `.forge/freeze.json`); guard hooks will block writes anyway. Say: "The suite is frozen. Append the desired change to `.forge/eval-change-requests.md`, then run `/plugin-forge:arm-evals` to apply it, re-gate, and re-freeze as a new version." Stop. |3233## Mode: new (pipeline phase)3435Work through the steps in order. Load the referenced file before executing its step —36each reference is binding, not advisory.3738### 1. Map the surface from the contract3940Read `design/CONTRACT.md`. Extract, per capability row:4142- the chosen composition (skills, hooks, agents, MCP servers, scripts) and its archetype43- the **C5 Testability** column: grader type, observable signal, and planned task IDs —44 this column IS the task backlog; build-evals exists to make those IDs real45- the **C1 Trigger** column: every auto-invoked component (model-invoked skill, hook,46 agent with proactive description) needs positive AND negative trigger tasks47- external dependencies (MCP servers, APIs, corpora) and whether the contract chose live48 or mock for each4950Also read `design/INTENT.md` for the runtime-environment answers (headless/cloud/local)51— they constrain what graders can observe.5253### 2. Interview — propose clusters, recommend, WAIT5455Never one-shot the suite (interviewing beats one-shot generation — LangChain's central56empirical finding). Use AskUserQuestion to:57581. Propose 3–5 testable ability clusters derived from the PDR rows, each with a one-line59 description, its archetype, and a rough task count. Recommend one ordering and say why.60 WAIT for the user's choice before generating anything.612. For every external dependency, ask live-vs-mock. Default recommendation: mock fixture62 stubs for write-heavy, paid, or rate-limited tools; live only for cheap read-only calls63 the user explicitly accepts. Record the decision per dependency — the harness stores64 which mode each result came from.6566For scientific or business domains where fixture corpora need domain-real content, offer67to run the `/plugin-forge:skill-forge` research pipeline to ground fixtures in real sources instead of68inventing plausible-looking data (decision D10).6970### 3. Scaffold7172Copy the scaffold from `${CLAUDE_PLUGIN_ROOT}/templates/evals/` into the target plugin as73`evals/`, and create the sibling `evals-holdout/` next to it (NOT inside `evals/`). The74scaffold provides `registry.yaml`, `targets/{default,no-plugin,crowded}.yaml`,75`suites/{capability,regression,triggers}.yaml`, `judges/{fact,rubric,battle}.yaml`,76`labels/labels.jsonl`, and `tasks/_example/`. Delete `tasks/_example/` after real tasks77exist.7879Then vendor the harness runner (stdlib-only, so the suite outlives plugin-forge):8081```82mkdir -p <plugin_dir>/evals/bin && cp "${CLAUDE_PLUGIN_ROOT}/scripts/harness/run.py" <plugin_dir>/evals/bin/run.py && chmod +x <plugin_dir>/evals/bin/run.py83```8485### 4. Author tasks8687Follow `references/task-authoring.md` for everything about tasks: the exact `task.yaml`88schema, the 20–50 task budget, the two-experts bar, the REQUIRED reference solution, the89goal-condition grammar for `instruction.md`, fixture rules, the holdout split, and90`elicitation_answers`. Every task ID planned in the contract's C5 column must exist in91`evals/tasks/` or `evals-holdout/tasks/` when this step ends.9293### 5. Choose graders9495Follow `references/grader-menu.md`: deterministic-first hierarchy (workspace → transcript96→ state_check → judge → trigger, plus the simulate_user driver), archetype routing, the97mandatory numeric `tolerance`, the never-assert-tool-ORDER rule, and the crowded-listing98requirement for trigger tasks.99100### 6. Author judges101102Only where the grader menu genuinely runs out of deterministic options. Follow103`references/judge-authoring.md`: modelgraded-spec YAML shape, one rubric dimension per104isolated judge call, Unknown always legal, pinned cheap judge model different from the105target model, anti-echo phrasing. Seed `evals/labels/labels.jsonl` with the unlabeled106sample skeleton — calibration itself is arm-evals' gate, not this skill's.107108### 7. Generate the elicitation auto-answer hook (when applicable)109110If the target plugin bundles or drives an MCP server that uses elicitation, headless eval111runs will hang or die — no dialog exists under `claude -p`. For every such server:112113- set `elicitation_answers` in each task.yaml that exercises it (see task-authoring.md)114- emit `evals/hooks/elicitation-auto-answer.sh` (bash, `set -euo pipefail`, executable) and115 `evals/hooks/elicitation-settings.json` registering it as an exec-form Elicitation hook:116 `"command": "${CLAUDE_PLUGIN_ROOT}/evals/hooks/elicitation-auto-answer.sh"` — harness runs117 merge this settings file; production users never load it. The script reads the elicitation118 request JSON from stdin, looks up the requested fields in119 `.forge-eval/elicitation-answers.json` (staged into the workspace by the harness from the120 task's `elicitation_answers`), and prints the matching answers object to stdout with exit 0.121 No match → one-line stderr message and exit 2, so the trial fails loudly instead of hanging.122123### 8. Lint the suite124125Run every mechanical lint in `references/antipatterns.md` (L01–L16) and fix all findings126before proceeding. L01 (numeric tolerance) and L02 (task-text/grader consistency) are the127builder meta-eval contract: plugin-forge's own dogfood suite feeds this skill adversarial128specs engineered to reproduce the CORE-Bench and METR grader bugs, and passes only if the129emitted suite provably cannot contain them. Do not hand a suite to arm-evals with any lint130red.131132### 9. Smoke-run drafts and hand off133134An eval that has never run is not delivered:135136- Run `forge-eval reference --task <ID>` for at least 3 representative tasks (one per137 archetype used) and show the user the graded output. Use `forge-eval run --dry-run` for a138 cost estimate of the full suite; state it plainly — trials × tasks × judges is real money,139 and `--max-cost-usd` exists for a reason.140- Present the suite summary: task count per suite (capability/regression/triggers), holdout141 count, judge dimensions, live-vs-mock map, estimated full-run cost.142- Tell the user the exact next command: `/plugin-forge:arm-evals` — the four arming gates143 (reference-green, red baseline, judge calibration, freeze) all run there.144145Do not modify `.forge/state.json`. The phase advances to `armed` only when arm-evals146freezes the suite.147148## Mode: retrofit <plugin-path>149150Standalone — no forge pipeline, no state guard, works on any installed or in-repo plugin.1511521. Map the plugin surface from files (there is no PDR): every `skills/*/SKILL.md` and153 command frontmatter (descriptions = trigger contracts), `agents/*.md`, `hooks/hooks.json`154 (matchers = firing contracts), `.claude-plugin/plugin.json`, `.mcp.json`. Derive one155 ability cluster per user-facing capability and one trigger axis per auto-invoked156 component. MCP tool checks must use plugin-scoped names:157 `mcp__plugin_<plugin>_<server>__<tool>`.1582. Then run steps 2–9 above unchanged, writing `evals/` and `evals-holdout/` into the159 plugin's own directory. Where step 1 of new mode says CONTRACT.md, use the surface map;160 flag any capability whose observable signal is unclear as "unverifiable — needs a design161 decision" instead of inventing a grader for it.1623. Recommend arming: retrofit suites get the same four gates via163 `/plugin-forge:arm-evals <plugin-path>` before their scores mean anything.164165## Mode: mine166167Standalone — converts captured production failures into tasks (the flywheel).1681691. Read traces from the generated plugin's trace-capture hook output:170 `${CLAUDE_PLUGIN_DATA}/traces/*.jsonl` (tool args, results, errors per session). If no171 traces exist, say so and stop — mine has no other input; suggest confirming the172 trace-capture hook is installed.1732. Cluster failures (repeated errors, user corrections, wrong outputs) and propose each174 cluster as a candidate task via AskUserQuestion — same interview discipline: propose,175 recommend, WAIT.1763. For each accepted failure, author a task per `references/task-authoring.md` with the177 real failing input as `instruction.md` seed, a fixture reconstructing the state, and a178 reference solution showing the correct behavior. New tasks go to `evals/tasks/` in the179 regression suite (they protect against a now-known failure).1804. A frozen suite that gains tasks is a NEW suite: finish by instructing the user to run181 `/plugin-forge:arm-evals` to re-gate and re-freeze (registry version bumps, scores stay182 comparable). If guard hooks block the writes, that is the freeze working — route the183 change through `.forge/eval-change-requests.md` as the guard message says.184185## Standing rules (all modes)186187- Deterministic-first, always. A judge where a file check would do is a bug.188- Every task ships a reference solution. No exceptions — 0% pass usually means a broken189 task, and only the reference proves otherwise.190- Positive AND negative trigger cases for every auto-invoked component; one-sided suites191 train over-triggering.192- Golden answers, key-fact lists, and reference solutions live under `reference/`, which193 the harness stages OUTSIDE the agent-visible worktree. Never copy any of it into194 `fixtures/` or `instruction.md`.195- Holdout tasks (`evals-holdout/`) are never named, quoted, or summarized in any session196 the builder loop can read.197- Track cost honestly: `tracked_metrics` are recorded, never graded; suite cost estimates198 go to the user before, not after, an expensive run.199- Never emit deprecated shapes: no legacy decision/approve hook JSON, no legacy mount-path200 skill references, and MCP tool names always in the full plugin-scoped form201 `mcp__plugin_<plugin>_<server>__<tool>` — never the short server-only form.202203## References204205- `references/task-authoring.md` — task budget, schema, instruction grammar, fixtures, holdout, elicitation fixtures206- `references/grader-menu.md` — grader hierarchy, archetype routing, trigger + crowded target, simulate_user207- `references/judge-authoring.md` — modelgraded spec shape, judge rules, calibration handoff208- `references/antipatterns.md` — reward-hacking catalog with the mechanical lints L01–L16