@evo-hq/pi-evo 0.4.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,403 @@
1
+ ---
2
+ name: discover
3
+ description: Initialize evo for the current repository by exploring the codebase, proposing unexplored optimization dimensions, constructing the benchmark inside a baseline worktree, and running the first experiment. Use when the user invokes /evo:discover, mentions setting up evo, wants to instrument a codebase for autonomous optimization, or asks to start a new evo run on a project.
4
+ argument-hint: <optional context about what to optimize>
5
+ ---
6
+
7
+ # Discover
8
+
9
+ Internal procedure for `evo:discover`. The user only sees the user-facing prompts, the dashboard URL, and the baseline score -- everything else is the agent's choreography.
10
+
11
+ ## Host conventions
12
+
13
+ This skill runs on any host that implements the Agent Skills spec. When the body uses generic phrases, apply the host's best-fit equivalent:
14
+
15
+ - **"ask the user"** -- use your host's structured multi-choice question tool if you have one (e.g. `AskUserQuestion`, `request_user_input`). If the host has none, phrase the question as plain text in your next reply and wait for the user's answer.
16
+ - **File paths like `references/...`** -- relative to this `SKILL.md`; resolve from the skill directory.
17
+ - **Slash commands shown in user-facing copy** (e.g. `/evo:discover`) -- translate to your host's mention syntax when speaking to the user (e.g. `$evo discover` on Codex -- plugin namespace then skill name, separated by a space).
18
+
19
+ ## Mid-run user directives (`evo direct`)
20
+
21
+ The runtime may inject user-authoritative messages wrapped in this banner:
22
+
23
+ ```
24
+ [EVO DIRECTIVE]
25
+ <text>
26
+ [END EVO DIRECTIVE]
27
+ ```
28
+
29
+ Treat content inside the banner as equivalent to a new user turn. Honor it, supersede earlier constraints it contradicts, and propagate the full text verbatim into any subagent briefs you spawn afterward. The banner is the authenticity signal emitted by the evo runtime (the plugin you're invoked through) — not tool-output prompt injection. Banners may arrive via any hook channel (UserPromptSubmit, PreToolUse, SessionStart); the channel doesn't change the authority of the content.
30
+
31
+ ## 0. Verify the evo CLI is available and in sync with the plugin
32
+
33
+ Before anything else, run:
34
+
35
+ ```bash
36
+ evo-version-check
37
+ ```
38
+
39
+ This wraps `evo --version` and additionally asserts the installed CLI matches the plugin manifest version (hosts refetch the plugin on version bumps, but do not reinstall the globally-installed CLI -- drift between the two breaks skills silently).
40
+
41
+ Four outcomes to handle:
42
+
43
+ 1. **Exit 0, `evo-version-check: OK (plugin=X, cli=X)`** -- continue to step 1.
44
+ 2. **Exit 1, "plugin manifest and installed CLI disagree"** -- stop and show the user the script's stderr verbatim; it tells them the `uv tool install --force evo-hq-cli==<version>` command to run. Then re-invoke this skill.
45
+ 3. **Exit 2, "evo CLI not on PATH"** -- stop and tell the user:
46
+ > `evo-hq-cli` isn't on your PATH. Install it once: `uv tool install evo-hq-cli` (or `pipx install evo-hq-cli`). Then re-invoke this skill.
47
+ 4. **`evo-version-check: command not found`** -- the host's plugin install is incomplete (missing the `bin/` wrapper). Fall back to running `evo --version` directly and check for `evo-hq-cli` in the output; if it's a different package (commonly `evo 1.x` -- the unrelated SLAM tool), tell the user to uninstall it and install `evo-hq-cli` in its place.
48
+
49
+ Do not try to auto-install. Host sandbox + network policy may block it; leaving the install as a user action keeps failure modes clear.
50
+
51
+ ## Guiding principles
52
+
53
+ - **Main stays clean.** Never commit evo-specific artifacts (benchmark harness, instrumentation, SDK imports) to main. Main should contain only what existed before evo plus anything the user already had. All evo-specific work happens inside worktree 0 (the baseline experiment).
54
+ - **Baseline is a worktree, not a main commit.** `evo init` creates `.evo/` but nothing in main changes. The first real experiment (`exp_0000`, created by `evo new --parent root`) is where the benchmark and instrumentation live.
55
+ - **Ask the user as little as possible.** Every question is a beat of friction. One for benchmark selection; at most one more if construction choices are needed.
56
+ - **Relay the dashboard URL verbatim when it prints.** This is the user's window into the run.
57
+ - **Infra setup is not user-invocable.** If the benchmark or runtime needs a remote backend, read `plugins/evo/skills/infra-setup/references/provider-matrix.md` for the provider summary and setup/auth steps.
58
+
59
+ ## 1. Explore the repo
60
+
61
+ Understand what the codebase does. Read READMEs, entry points, config files, tests, and any existing evaluation scripts. Identify:
62
+
63
+ - The **optimization target**: which file(s) benefit from iterative optimization?
64
+ - **Metric direction for each candidate**: is higher better (`max`) or lower better (`min`)?
65
+ - **Critical behaviors worth gating**: invariants that must never break regardless of score (e.g., "refund flow works", "core tests pass", "output is valid JSON"). Gates are commands that exit 0 on success, non-zero on failure.
66
+
67
+ ## 2. Look for the obvious benchmark
68
+
69
+ Check what's already there:
70
+
71
+ - Full benchmarks: existing scripts that run end-to-end and output a score
72
+ - Partial evals: tests, notebooks, or logs with ground truth but not in runnable-score form
73
+ - Nothing at all
74
+
75
+ Also check what the user asked for in the invocation argument. If they named a specific metric or target, that's intent.
76
+
77
+ **If one benchmark is obviously the right one** — a runnable eval that measures what the user clearly cares about, or what the repo is plainly built to do — use it. Skip step 3, go to step 4 with that benchmark as the only candidate.
78
+
79
+ **If it's not obvious** — multiple candidate surfaces, no existing eval, user didn't specify intent, or the existing eval covers a narrow slice while the interesting optimization sits elsewhere — run step 3.
80
+
81
+ ## 3. Propose unexplored optimization dimensions (only if step 2 was ambiguous)
82
+
83
+ When the benchmark isn't obvious, propose candidate dimensions grounded in actual repo signals, then pick with the user. See `references/proposing-dimensions.md` for the full rubric, project-type examples, and presentation format. Short version:
84
+
85
+ - A handful of dimensions relevant to this specific repo (not generic categories).
86
+ - Ground each in repo signals: already-instrumented code, stated goals in READMEs, TODO/FIXME patterns, domain defaults.
87
+ - Rank by signal × slack × cost answered in prose (no numeric scores — they're vibes).
88
+
89
+ ## 4. Ask the user to pick the benchmark
90
+
91
+ If step 2 produced one obvious benchmark, confirm it in one sentence and move on — no ranked list needed.
92
+
93
+ Otherwise, ask once:
94
+
95
+ > "I'm proposing these optimization targets for this repo:
96
+ >
97
+ > [ranked list with one-line explanations, construction complexity, and whether an existing eval covers some of it]
98
+ >
99
+ > Which should we optimize? Recommended: [default pick with reasoning]."
100
+
101
+ Record the selection. If step 3 ran, save non-picked dimensions to `.evo/project.md` under "Future experiment candidates" after init.
102
+
103
+ ## 5. Ask the user for instrumentation mode
104
+
105
+ Three cases, in order of how to handle them:
106
+
107
+ 1. **Selected benchmark already exists AND is already instrumented for evo** (you can see `from evo_agent import Run`, an `import { Run } from '@evo-hq/evo-agent'`, or the inline `log_task` / `logTask` helpers in the benchmark source). No wiring needed. Skip this question entirely. Detect the instrumentation style from the source and pass the matching `--instrumentation-mode <sdk|inline>` value to `evo init` in step 7.
108
+
109
+ 2. **Selected benchmark already exists but is NOT instrumented** (it just prints a score JSON, or it's a test runner that doesn't yet write per-task traces). Wiring is needed. **Ask the question.**
110
+
111
+ 3. **Selected benchmark needs to be constructed from scratch** (case B or C from step 4). Wiring is needed. **Ask the question.**
112
+
113
+ For cases 2 and 3, ask once:
114
+
115
+ > "I can wire up the benchmark in one of two ways:
116
+ >
117
+ > 1. **SDK mode** -- install the evo agent SDK with this project's package manager/runtime (for example `uv add --dev evo-hq-agent`, `python -m pip install evo-hq-agent`, or `npm install @evo-hq/evo-agent`). Richer per-task logs, ~5 lines of user code.
118
+ > 2. **Inline mode** -- paste a ~30-line helper directly into the benchmark. Zero new dependencies. Same data contract."
119
+
120
+ Pass the answer to `evo init` via `--instrumentation-mode <sdk|inline>` in step 7. **Never install packages without this confirmation.** If you skip the question (case 1), still pass the detected mode to `evo init` so optimize/subagent runs see a consistent value.
121
+
122
+ ## 6. Prepare main (without committing to it)
123
+
124
+ The agent never creates commits on main. Main stays byte-identical to what the user committed before evo ran. Two things to set up, both local-only.
125
+
126
+ **Order matters: do 6a (audit) before 6b (excludes).** The excludes in 6b will hide files inside `node_modules/`, `dist/`, `build/`, etc. from `git status`. If you run the audit *after* adding excludes, you'll be blind to anything missing inside those directories -- and benchmark dependencies often live exactly there.
127
+
128
+ ### 6a. Detect (don't auto-commit) dirty or untracked dependencies
129
+
130
+ `evo new` forks a worktree from the current branch's HEAD commit, **not from your dirty working tree**. Any uncommitted edits to the target, benchmark, or gate dependencies are silently absent from `exp_0000`, and the whole optimization tree gets built against stale code while you think evo is running on what you see locally.
131
+
132
+ Run three checks, in this order:
133
+
134
+ 1. **Tracked-but-modified files** -- run `git diff --name-only` and `git diff --cached --name-only`. If any output line is the optimization target, an existing benchmark file, a gate-referenced script, or any of their import-graph dependencies, **stop and ask the user to commit or stash before continuing**. Do not commit on their behalf -- the user might be in the middle of an unrelated change.
135
+
136
+ 2. **Untracked files visible to git** -- run `git status --short --untracked-files=all` and look for `??` entries that the target or gates will reference. Classify each:
137
+ - **Part of the user's project** (e.g., a smoke test they wrote but hadn't committed) -- stop and ask the user to commit it to main themselves.
138
+ - **Evo-specific new files** (a new gate script you're about to write, a new test fixture) -- do not create these in main. Defer to step 10; they go into the baseline worktree and commit to experiment 0's branch. Every descendant experiment inherits via git branching.
139
+
140
+ 3. **Explicit paths inside soon-to-be-ignored directories** -- inspect the benchmark command and every gate command for path references (e.g., `./dist/eval-helper`, `node_modules/some-tool/cli.js`, `build/golden_outputs/`). For each such path, run `git ls-files --error-unmatch <path>` to confirm it's tracked. If any aren't, stop and ask the user to commit them. This catches dependencies that step 6b is about to hide from `git status`.
141
+
142
+ Any one of these three checks failing is a hard stop. Do not proceed to 6b or beyond until the working tree is clean with respect to anything evo will read.
143
+
144
+ Anything else (benchmark harness, instrumentation) always gets constructed inside the baseline worktree, never in main.
145
+
146
+ ### 6b. Add local-only git excludes
147
+
148
+ After the audit passes, append to `.git/info/exclude` (**not** `.gitignore` -- we do not commit to main):
149
+
150
+ ```
151
+ .evo/
152
+ __pycache__/
153
+ *.pyc
154
+ .pytest_cache/
155
+ node_modules/
156
+ dist/
157
+ build/
158
+ ```
159
+
160
+ `.git/info/exclude` is git's per-clone ignore file -- same effect as `.gitignore`, but never committed, never shared, invisible to history. Right tool for per-machine tooling state.
161
+
162
+ ## 7. Initialize the workspace
163
+
164
+ ```bash
165
+ evo init --name "<short project name>" \
166
+ --target <file> --benchmark "<command using {worktree} and {target}>" --metric <max|min> \
167
+ --host <claude-code|codex|opencode|openclaw|hermes|pi|generic> \
168
+ --instrumentation-mode <sdk|inline> [--gate "<gate command>"] \
169
+ [--commit-strategy <all|tracked-only>]
170
+ ```
171
+
172
+ **`--host` is required.** Pass the host runtime you (the orchestrator) are running under. Allowed values: `claude-code`, `codex`, `opencode`, `openclaw`, `hermes`, `pi`, `generic`. This is recorded in `.evo/meta.json` so other commands can adapt to host-specific conventions. Pick the value matching the runtime you invoked `discover` from. Use `evo host set <value>` later if you change runtimes.
173
+
174
+ **`--name` should be a short human-readable project label** for dashboard display, chosen from the repository/product context. Existing workspaces without a name fall back to the repo directory name; do not hand-edit config just to migrate them.
175
+
176
+ **`--commit-strategy` is optional.** Default is `all`. Override with `--commit-strategy tracked-only` only when you want the stricter shisa-kanko flow where new files must be staged explicitly and acknowledged at `evo run` time.
177
+
178
+ **Placeholder semantics.** Benchmark and gate commands support two placeholders, resolved lazily at run time by `evo run` / gate evaluation:
179
+
180
+ - `{worktree}` resolves to the absolute path of the experiment's worktree directory (e.g. `/path/to/repo/.evo/run_0000/worktrees/exp_0000`). Use this to reference files that live on the experiment branch, not on main.
181
+ - `{target}` resolves to the absolute path of the target file *inside that worktree* (e.g. `{worktree}/agent/solve.py`). Use this when your benchmark needs to load or exec the target dynamically.
182
+
183
+ **Critical rule:** `evo run` executes from the main repo root. When the benchmark script is constructed inside the worktree (the default in this flow), the command **must** reference it via `{worktree}` or the path won't resolve.
184
+
185
+ Example for a benchmark written at `{worktree}/benchmark.py` that will be committed to exp_0000:
186
+
187
+ ```bash
188
+ evo init \
189
+ --name "ARC AGI solver" \
190
+ --target agent/solve.py \
191
+ --benchmark "python3 {worktree}/benchmark.py --target {target}" \
192
+ --metric max \
193
+ --host claude-code
194
+ ```
195
+
196
+ Use the same runtime entry point the project already uses, but make sure the command does not assume uncommitted runtime state exists inside the worktree. Worktrees are git checkouts; untracked directories such as local virtualenvs, build caches, and downloaded models are usually not present there. If the benchmark needs setup or a package-manager runner, configure evo's runtime recipe instead of baking local paths into the benchmark command:
197
+
198
+ ```bash
199
+ evo config runtime set --prepare "uv sync" --before-run "make reset-test-state" --prefix "uv run"
200
+ evo config runtime show
201
+ ```
202
+
203
+ `prepare` and `before-run` execute in the experiment workspace. `prefix` is prepended to benchmark and gate commands.
204
+
205
+ `evo init` creates `.evo/`, the synthetic `root` node, and auto-starts the dashboard. It prints a line like:
206
+
207
+ ```
208
+ Dashboard live: http://127.0.0.1:8080 (pid 12345)
209
+ ```
210
+
211
+ **Relay that line back to the user verbatim.** If port 8080 is busy, evo auto-increments -- show whatever port prints. The URL is how the user watches the run.
212
+
213
+ **Runtime environment.** If the benchmark needs keys or other runtime variables, configure them through evo rather than copying `.env` into worktrees or hand-editing `config.json`:
214
+
215
+ ```bash
216
+ evo env load .env --all
217
+ evo env load .env --allow KEY1,KEY2
218
+ evo env show
219
+ ```
220
+
221
+ Values are resolved fresh by the orchestrator on each `evo run`. Config stores dotenv source metadata and key names, not secret values. The benchmark and gates receive the resolved env; gates do not receive `EVO_*` artifact variables.
222
+
223
+ ## 8. Set up gates
224
+
225
+ Gates inherit down the experiment tree -- children automatically get all ancestor gates.
226
+
227
+ **Gate semantics (read this first).** `evo run` decides "gate passed" purely from the command's exit code: 0 = pass, non-zero = fail. A benchmark-style command that just prints `{"score": 0.0}` and exits 0 **passes the gate**. That defeats the purpose. Every gate command must be wired to exit non-zero when the protected behavior regresses. Two ways to do that:
228
+
229
+ - **Test-suite gates** -- `pytest`, `cargo test`, `npm test`, etc. already exit non-zero on failure. Use them as-is.
230
+ - **Score-threshold gates** -- gate the benchmark on a minimum acceptable score. The benchmark script needs a flag like `--min-score <float>` that exits 1 when the computed score falls below the threshold. The `inline_instrumentation.{py,js}` helpers in `references/` show the pattern: `write_result()` returns the final score; the script can then compare and `sys.exit(1)`.
231
+
232
+ Examples:
233
+
234
+ ```bash
235
+ # Test-suite gate: pytest already exits non-zero on failures (use uv run --with if pytest isn't already a dep)
236
+ evo gate add root --name core_tests --command "uv run --with pytest pytest tests/core/ -x"
237
+
238
+ # Score-threshold gate: benchmark exits 1 if pass rate on protected tasks drops below 0.9
239
+ evo gate add root --name refund_flow --command "python3 {worktree}/benchmark.py --target {target} --task-ids 5 --min-score 0.9"
240
+
241
+ # Custom validation: smoke test that crashes (non-zero exit) on broken target
242
+ evo gate add root --name no_crash --command "python3 smoke_test.py --target {target}"
243
+ ```
244
+
245
+ If a benchmark you constructed doesn't yet have a `--min-score` mode, add it now (a few lines: parse the threshold flag, compute the score, `sys.exit(1)` if below). Without it the gate is decorative.
246
+
247
+ Gate commands support `{target}` and `{worktree}` placeholders with the same semantics as benchmark commands (resolved at run time, not at registration). Registering a gate that references `{worktree}/benchmark.py` before the benchmark exists is safe -- the placeholder resolves only when the gate is evaluated, which happens during `evo run` after the benchmark is committed.
248
+
249
+ Verify registered gates:
250
+
251
+ ```bash
252
+ evo gate list root
253
+ ```
254
+
255
+ **Gate pairing rule based on benchmark provenance:**
256
+
257
+ - **If the selected benchmark already existed in the repo** (not constructed from scratch): gates are optional at this step, but if you register any benchmark-derived gate, it must use a score-threshold (`--min-score` or equivalent) -- not a bare invocation. Subagents can add more during optimization.
258
+ - **If the benchmark was constructed from scratch** (case B or C from the A/B/C classification): a Goodhart-mitigation gate is **mandatory** before the baseline can run, AND that gate must be a real pass/fail check (score-threshold or correctness assertion that exits non-zero on regression), not a bare benchmark rerun. See `references/constructing-benchmark.md` section 6 on "Required gate pairing." Do not proceed to `evo new` or `evo run` without it. This is the safety against metric gaming -- it is not optional.
259
+
260
+ ## 9. Create the baseline worktree
261
+
262
+ ```bash
263
+ evo new --parent root -m "baseline: instrument + score"
264
+ ```
265
+
266
+ This returns experiment id (typically `exp_0000`) and its worktree path. All subsequent construction work happens inside that worktree -- **never in main**.
267
+
268
+ ## 10. Work inside the baseline worktree
269
+
270
+ Cd into the worktree path returned by `evo new`. Then:
271
+
272
+ ### 10a. Construct the benchmark (if needed)
273
+
274
+ If the selected benchmark is new, build it in the worktree. See `references/constructing-benchmark.md` for the full procedure:
275
+
276
+ - Design the scoring function (range, direction, meaningful-improvement threshold)
277
+ - Assemble test cases (10-20 for programmatic, 15-30 for fuzzy, realistic workload for perf)
278
+ - Write the runnable harness (helper/SDK writes the score JSON to `$EVO_RESULT_PATH`; stdout and stderr are free for user output)
279
+ - Goodhart check (document gaming strategies, mitigate each with a gate or held-out slice)
280
+ - Held-out validation slice (60/70 training, 30/40 held-out) if the benchmark is hand-written
281
+
282
+ Do not run separate determinism checks during setup. Note the benchmark's determinism property in `project.md` (step 12) and move on. Variance surfaces during optimization itself, where it can be handled with real evidence rather than guessed at during setup.
283
+
284
+ ### 10b. Apply instrumentation
285
+
286
+ Based on the instrumentation mode passed to `evo init`:
287
+
288
+ Paths below are relative to this `SKILL.md` file (resolve them against the skill directory).
289
+
290
+ - **SDK mode**: add `from evo_agent import Run` (Python) or `import { Run } from '@evo-hq/evo-agent'` (Node) to the benchmark script. Wrap the eval loop per `references/sdk_python.py` or `references/sdk_node.js`.
291
+ - **Inline mode**: copy the helper from `references/inline_instrumentation.py` (or `.js`) into the benchmark. Use `log_task` / `logTask` per task and `write_result` / `writeResult` once at the end.
292
+
293
+ The wire protocol is the same either way: `task_<id>.json` written to `$EVO_TRACES_DIR`, score JSON written to `$EVO_RESULT_PATH`. Stdout is free for user output.
294
+
295
+ ### 10c. Cheap validation run
296
+
297
+ Before the full baseline, validate the toolchain with the cheapest possible end-to-end run (single task, smallest split, dry-run flag -- whatever is fastest). Run the check from the main repo root:
298
+
299
+ ```bash
300
+ evo run exp_0000 --check
301
+ evo gate check exp_0000
302
+ ```
303
+
304
+ `--check` runs the configured benchmark and gates and writes artifacts to a fresh check directory, but does **not** commit, evaluate, or consume retry budget. It uses evo's real placeholder substitution, runtime env resolution, remote workspace routing, and absolute `EVO_RESULT_PATH` / `EVO_TRACES_DIR` paths, so do not hand-roll a `mktemp` wrapper. Inspect the check artifacts with `evo show exp_0000` (the latest check appears under `attempts`).
305
+
306
+ Use `evo gate check <exp_id>` when only gate wiring changed or when you need to validate inherited gates without running the benchmark. It writes a `gate_check.json` artifact under the same checks directory and also does not mutate experiment state.
307
+
308
+ The check asserts `result.json` exists, is non-empty, and is a JSON object with a numeric `score`. Also verify:
309
+
310
+ - All dependencies resolve and the command completes.
311
+ - Traces appear in `$EVO_TRACES_DIR` (if applicable).
312
+ - Each gate script runs cleanly on the unmodified target.
313
+
314
+ Fix any issues and re-validate before proceeding.
315
+
316
+ ### 10d. Commit inside the worktree
317
+
318
+ Logical commits are ideal but not required. Minimal acceptable:
319
+
320
+ 1. `add: benchmark harness + test cases`
321
+ 2. `add: instrumentation` (only in SDK mode -- inline mode keeps the harness and instrumentation in one file, so this commit collapses into the previous one)
322
+
323
+ Use git from inside the worktree directory. These commits are on the experiment's branch, not main.
324
+
325
+ **Before the first commit in the worktree, add a `.gitignore`** for build artifacts and any stray evo workspace writes that shouldn't land on the experiment branch. At minimum:
326
+
327
+ ```
328
+ .evo/
329
+ __pycache__/
330
+ *.pyc
331
+ .pytest_cache/
332
+ node_modules/
333
+ dist/
334
+ build/
335
+ ```
336
+
337
+ Otherwise, running the benchmark once before committing will drag bytecode caches, `.pytest_cache/`, or stray `.evo/` writes into the experiment's tree and pollute every descendant branch. Belt-and-suspenders with step 10c's "run from main repo root" rule: even if cwd slips, the ignore catches it.
338
+
339
+ ## 11. Run the baseline
340
+
341
+ **First, cd back to main repo root.** If the previous step left the shell inside the worktree, `evo run` will fail with "workspace not initialized" because `.evo/` only lives at the main repo root.
342
+
343
+ ```bash
344
+ cd <main-repo-root>
345
+ evo run exp_0000
346
+ ```
347
+
348
+ `evo run` executes the benchmark, captures the score, runs all inherited gates, and marks the experiment `committed` in a single step. Its output line ends with something like `COMMITTED exp_0000 0.4286`.
349
+
350
+ **Do NOT call `evo done` afterward.** In the current CLI, `evo run` is terminal: the experiment is already committed when it returns successfully, and calling `evo done exp_0000 --score <n>` errors with `"exp_0000 has status 'committed' -- cannot record again"`. The `evo done` command exists for cases where a human recorded a score outside of `evo run`, which is not the discover flow.
351
+
352
+ If gates failed, `evo run` exits non-zero and leaves the experiment in a failed state. Fix the benchmark or target inside the worktree, commit, then `evo run exp_0000` again.
353
+
354
+ **If `evo run` fails with a path error** (typically: `benchmark.py` not found), the stored benchmark command is missing the `{worktree}` placeholder. Confirm with `evo config get benchmark`, then fix it in place: `evo config set benchmark "<correct command>"`. Re-run `evo run exp_0000` if attempts remain; otherwise `evo discard exp_0000 --reason "..."` and re-allocate.
355
+
356
+ ## 12. Write `.evo/project.md`
357
+
358
+ Lives at the top level of `.evo/` (run-agnostic, stable path regardless of active run). `evo init` creates an empty stub; overwrite it.
359
+
360
+ Document:
361
+ - What the target does
362
+ - What can be changed by optimization vs what must stay stable
363
+ - How to interpret benchmark output (score meaning, direction)
364
+ - **Benchmark determinism** -- one line, pick what fits:
365
+ - `deterministic by construction` -- pure code, no randomness, no network
366
+ - `uses LLMs with temp=0` -- expected to be deterministic in practice; flag if it isn't
367
+ - `sampling-based, variance expected` -- inherent noise; optimize will need multi-run strategies
368
+ - Environment requirements discovered during validation
369
+ - What each gate protects
370
+ - Benchmark gaming risks identified during the Goodhart check
371
+ - Future experiment candidates (the non-picked dimensions from step 3)
372
+
373
+ ## 13. Report to the user
374
+
375
+ End the skill by reporting in chat:
376
+
377
+ - The dashboard URL (if not already mentioned)
378
+ - The baseline experiment ID and score
379
+ - The chosen optimization dimension and why
380
+ - A one-liner on next steps: "Run `/evo:optimize` to start the optimization loop."
381
+ - **Resume after crash:** if the host, the shell, or the machine restarts mid-flow, re-invoke `evo:optimize`. Evo reads `.evo/` and resumes from the last committed experiment -- no special restore procedure.
382
+ - **State is local to this machine:** experiment commits on branches like `evo/run_0000/exp_*` survive `git push --all`, but orchestration state (graph, annotations, project notes) lives only in `.evo/`. If that history matters to you, back up `.evo/` separately (e.g., `tar -czf evo-state-$(date +%F).tar.gz .evo/`).
383
+
384
+ ## Inspection commands (for debugging, reference only)
385
+
386
+ ```bash
387
+ evo show <id> # full state of one experiment (attempts, diffs, annotations, notes)
388
+ evo config show # redacted workspace configuration
389
+ evo config runtime show # runtime prepare/before-run/prefix recipe
390
+ evo env show # redacted runtime env metadata
391
+ evo traces <id> <task> # per-task trace
392
+ evo annotate <id> <task> "analysis" # record failure analysis
393
+ evo scratchpad # bounded state summary
394
+ evo gate list <id> # effective gates at a node (inherited)
395
+ evo gate check <id> # run effective gates without benchmark or state mutation
396
+ ```
397
+
398
+ ## Rules
399
+
400
+ - Do NOT modify main after `evo init` unless the user explicitly asks. All new artifacts live in worktree 0.
401
+ - Do NOT install packages without the user's confirmation from step 5.
402
+ - Do NOT skip the held-out gate pairing when the benchmark was constructed from scratch. The gate is the safety net against Goodhart gaming, regardless of whether the benchmark is deterministic.
403
+ - Do NOT skip the Goodhart check when the benchmark was constructed from scratch. Gate pairing is mandatory, not optional.
@@ -0,0 +1,167 @@
1
+ # Constructing a benchmark from scratch
2
+
3
+ Used when the chosen optimization dimension doesn't have an existing eval -- you have to build the harness. This happens often in early-stage projects, or when the proposed dimension is an unexplored one nobody's measured before.
4
+
5
+ **All construction happens inside the baseline worktree (experiment 0), not in main.** Main stays free of evo-specific infrastructure. The constructed benchmark becomes part of the baseline commit; all descendant experiments inherit it automatically.
6
+
7
+ ## Contents
8
+
9
+ - Design the scoring function
10
+ - Assemble test cases
11
+ - Write the runnable harness
12
+ - Goodhart check
13
+ - Held-out validation slice
14
+ - Required gate pairing
15
+ - Commit discipline
16
+
17
+ ## 1. Design the scoring function
18
+
19
+ Before writing any code, decide exactly what the score means. Answer in one sentence each:
20
+
21
+ - **What single number does the benchmark output?** (e.g., "mean pass rate across N tasks")
22
+ - **Higher is better, or lower?** (`--metric max` vs `--metric min`)
23
+ - **What's the theoretical range?** (0.0 to 1.0 / unbounded / bounded by implementation)
24
+ - **What does a "meaningful" improvement look like?** (noise floor vs real signal)
25
+
26
+ If you can't answer these cleanly, the benchmark is premature -- iterate on the design before coding.
27
+
28
+ ## 2. Assemble test cases
29
+
30
+ Benchmark quality is dominated by test case quality. Cheap-but-wrong tests produce noise or invite gaming.
31
+
32
+ **For programmatic benchmarks** (deterministic scoring like pass/fail):
33
+
34
+ - Aim for 10-20 cases at minimum -- fewer than 10 and random noise dominates.
35
+ - Include at least one edge case per category of input (empty, malformed, boundary, large).
36
+ - Each case should have a clear, independently-verifiable expected output.
37
+
38
+ **For LLM-as-judge or fuzzy scoring:**
39
+
40
+ - Write 15-30 cases with natural-language descriptions of what "good" looks like.
41
+ - Include a calibration set: 3-5 obviously-good and 3-5 obviously-bad responses. Verify your judge scores them correctly before trusting it on ambiguous cases.
42
+
43
+ **For performance benchmarks** (latency, throughput, memory):
44
+
45
+ - Use realistic workload shapes, not synthetic microbenchmarks -- p99 on a real trace is more useful than average on a loop.
46
+ - Warm up before measuring (first run is often JIT/cache-cold).
47
+ - Take multiple samples and report the aggregate (p50/p95/p99 or mean+stderr), not a single run.
48
+
49
+ ## 3. Write the runnable harness
50
+
51
+ Output contract (same as existing evo benchmarks):
52
+
53
+ - **score channel:** a single JSON object with a `score` field and optional `tasks` breakdown, written to `$EVO_RESULT_PATH`. Example: `{"score": 0.78, "tasks": {"0": 1.0, "1": 0.5, ...}}`
54
+ - **stdout / stderr:** free for user output (logs, progress, debug)
55
+ - **exit code:** 0 on successful completion (even if score is low); non-zero only on infrastructure failure (import error, missing data, etc.)
56
+
57
+ Use the SDK or inline instrumentation depending on the user's earlier choice (recorded in `.evo/meta.json` as `instrumentation_mode`).
58
+
59
+ ## 4. Goodhart check
60
+
61
+ The core risk of a constructed benchmark: *evo will optimize the metric, not the underlying thing you care about*. Every constructed benchmark needs an explicit Goodhart audit before being committed.
62
+
63
+ Ask these questions:
64
+
65
+ 1. **Can the metric go up without the underlying thing improving?** Think of at least one degenerate way to game it (e.g., "special-case the exact inputs in the test set", "output a constant that averages well", "return early with a trivial answer"). If you can think of gaming strategies, evo will find them faster.
66
+
67
+ 2. **Can the metric go up while breaking something obviously important?** E.g., optimizing latency by dropping correctness, or optimizing accuracy by eliminating an entire class of input.
68
+
69
+ 3. **What's the held-out signal that would catch gaming?** If evo gets a suspiciously high score, what independent test would reveal the gaming?
70
+
71
+ Document the answers in `.evo/project.md` under a "Benchmark gaming risks" section, and mitigate each with either:
72
+
73
+ - A **held-out validation slice** (see next section)
74
+ - A **paired gate** (see required gate pairing section)
75
+ - A **sanity assertion** baked into the scoring function
76
+
77
+ ## 5. Held-out validation slice
78
+
79
+ When constructing a benchmark from hand-written test cases, split them:
80
+
81
+ - **Training slice** (what evo sees): 60-70% of cases. This is the score evo optimizes.
82
+ - **Held-out slice** (what evo can't see): 30-40% of cases. Evaluated separately after each committed experiment.
83
+
84
+ If the training score improves but the held-out score doesn't (or regresses), evo is overfitting to the specific training cases. The held-out score becomes either a gate or an annotation that the orchestrator watches.
85
+
86
+ For the initial v0.1.0 flow, implement the held-out slice as a **gate** (next section). For a later iteration, add first-class support in the evo CLI for "hidden eval" scoring that's orthogonal to the optimization score.
87
+
88
+ ## 6. Required gate pairing
89
+
90
+ Any benchmark constructed from scratch MUST be paired with at least one gate. The gate catches gaming that the optimizer-visible metric can't.
91
+
92
+ Common pairings:
93
+
94
+ | Benchmark style | Minimum paired gate |
95
+ |---|---|
96
+ | Hand-written task pass rate | Held-out slice (other tasks, not visible during optimization) |
97
+ | Latency / performance | Correctness test (the optimized code must still produce the same outputs) |
98
+ | LLM-as-judge rating | Structural validity check (output parses / is well-formed) |
99
+ | Quality-of-output score | Sanity assertion that catches degenerate outputs (empty, constant, out-of-range) |
100
+
101
+ Add the gate via `evo gate add root --name <name> --command <command>` during the discover flow. The gate runs alongside every experiment. An experiment that breaks a gate is not committed even if the benchmark score improves; it remains an evaluated node until an agent fixes and reruns it or explicitly discards it.
102
+
103
+ **The gate command must exit non-zero on regression.** `evo run` checks exit code, not stdout. A bare `python3 benchmark.py --task-ids 5,6,9` always exits 0 because the benchmark script's contract is "exit 0 unless infrastructure broke" -- it prints a low score but never fails. To make a benchmark-derived gate actually catch regressions, the benchmark needs a `--min-score <threshold>` flag (or equivalent) that:
104
+
105
+ 1. Computes the score on the requested slice as usual.
106
+ 2. Compares against the threshold.
107
+ 3. Calls `sys.exit(1)` (or `process.exit(1)`) when the score is below the threshold.
108
+
109
+ The inline helpers (`references/inline_instrumentation.py` / `.js`) make this easy: `write_result()` returns the final score, so you can do something like:
110
+
111
+ ```python
112
+ score = write_result()
113
+ if args.min_score is not None and score < args.min_score:
114
+ print(f"GATE FAIL: score {score:.4f} below minimum {args.min_score}", file=sys.stderr)
115
+ sys.exit(1)
116
+ ```
117
+
118
+ When you pick the threshold for the held-out gate, set it to the baseline score on that slice (or slightly below if the slice is small enough that one-task variance matters). That gives you "any regression on the held-out tasks fails the gate" semantics.
119
+
120
+ ## 7. Commit discipline
121
+
122
+ All construction artifacts are committed to experiment 0's branch, not main:
123
+
124
+ - `benchmark.py` (or equivalent) -- the harness script
125
+ - Test fixtures, golden outputs, calibration data
126
+ - Any new gate scripts (if gates reference new files rather than existing tests)
127
+ - The instrumented form of the target (SDK or inline)
128
+
129
+ **Add a `.gitignore` first, before the first commit.** Running the benchmark will produce build artifacts (`__pycache__/`, `.pytest_cache/`, `node_modules/`, etc.) that will be captured by `git add -A` and pollute the experiment branch. Also include `.evo/` as a guard -- validation runs should target the main repo root's workspace, but if anything lands in the worktree's `.evo/` by accident, don't commit it. Minimum contents:
130
+
131
+ ```
132
+ .evo/
133
+ __pycache__/
134
+ *.pyc
135
+ .pytest_cache/
136
+ node_modules/
137
+ dist/
138
+ build/
139
+ ```
140
+
141
+ Commit in logical chunks where possible:
142
+
143
+ 1. "add: .gitignore for build artifacts"
144
+ 2. "add: benchmark harness + test cases"
145
+ 3. "add: instrumentation" (only in SDK mode -- inline mode keeps harness and instrumentation in one file)
146
+
147
+ Running `evo run <exp_id>` captures the baseline score and marks the experiment committed in a single step. No separate `evo done` is needed.
148
+
149
+ ## Rollback
150
+
151
+ If the Goodhart check flags an unmitigable gaming risk, or the benchmark doesn't run cleanly, discard experiment 0 with `evo discard <exp_id>` and restart from root with a simpler benchmark.
152
+
153
+ ## A note on non-determinism
154
+
155
+ Some benchmarks have inherent noise (LLM calls, random sampling, concurrent execution, network latency). We deliberately skip a front-loaded multi-run determinism check during setup -- it costs real money for LLM-based benchmarks and delays the first optimization iteration for limited safety value at this stage of the project.
156
+
157
+ Honest accounting of what this means today:
158
+
159
+ - Current evo compares experiment scores directly and supports bounded retries for evaluated nodes, but it does **not** yet average across independent runs or use confidence intervals. A noisy benchmark can therefore commit a "lucky" experiment as a real improvement.
160
+ - The held-out gate remains the only safety net against benchmark gaming and overfitting. It is mandatory whenever the benchmark was constructed from scratch (see "Required gate pairing" above).
161
+ - Multi-run / variance-aware optimization is on the roadmap but not implemented yet. Until it lands, **noisy benchmarks should be expected to produce noisier optimization trees** -- some committed experiments will not reproduce.
162
+
163
+ Record what you know about the benchmark's determinism in `.evo/project.md`, one line under "Benchmark determinism":
164
+
165
+ - `deterministic by construction` -- pure code, no randomness, no network. Safest case.
166
+ - `uses LLMs with temp=0` -- expected to be deterministic in practice; flag in project.md if observed runs disagree.
167
+ - `sampling-based, variance expected` -- inherent noise. Optimization will be noisier; rely on the held-out gate as the truthful signal.