@evo-hq/pi-evo 0.5.0-alpha.7 → 0.5.0-alpha.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-hq/pi-evo",
3
- "version": "0.5.0-alpha.7",
3
+ "version": "0.5.0-alpha.8",
4
4
  "description": "Evo plugin for pi-coding-agent: optimize/discover/subagent skills + mid-run inject extension.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -2,7 +2,7 @@
2
2
  name: discover
3
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
4
  argument-hint: <optional context about what to optimize>
5
- evo_version: 0.5.0-alpha.7
5
+ evo_version: 0.5.0-alpha.8
6
6
  ---
7
7
 
8
8
  # Discover
@@ -11,7 +11,9 @@ Internal procedure for `evo:discover`. The user only sees the user-facing prompt
11
11
 
12
12
  ## Evo surface
13
13
 
14
- What you can invoke / dispatch / read. Each line is a triggering condition: if you're about to do X, pull/dispatch/read this. Don't preload -- act when the trigger fires.
14
+ General guidance on the skills and tools available in evo. Each line is a triggering condition: if you're about to do X, pull/dispatch/read this. Don't preload -- act when the trigger fires.
15
+
16
+ **Always have a sense of the skill before jumping into its references.** A skill body carries the decision-making; references are concrete contracts that assume a decision has been made.
15
17
 
16
18
  ```
17
19
  evo plugin
@@ -114,20 +116,20 @@ evo --version
114
116
  The output must be exactly:
115
117
 
116
118
  ```
117
- evo-hq-cli 0.5.0-alpha.7
119
+ evo-hq-cli 0.5.0-alpha.8
118
120
  ```
119
121
 
120
122
  Three outcomes:
121
123
 
122
124
  1. **Matches exactly** — continue to step 1.
123
125
  2. **Reports a different version** (`evo-hq-cli 0.4.2`, etc.) — the host refetched a newer/older skill bundle than the CLI on PATH. Drift breaks skills silently. Stop and tell the user:
124
- > Your installed evo CLI is on a different version than this skill (`0.5.0-alpha.7`). Run:
126
+ > Your installed evo CLI is on a different version than this skill (`0.5.0-alpha.8`). Run:
125
127
  > ```
126
- > uv tool install --force evo-hq-cli==0.5.0-alpha.7
128
+ > uv tool install --force evo-hq-cli==0.5.0-alpha.8
127
129
  > ```
128
130
  > Then re-invoke this skill.
129
131
  3. **`command not found`, or reports a different package** (commonly `evo 1.x` — the unrelated SLAM tool) — the CLI isn't installed. Tell the user:
130
- > `evo-hq-cli` isn't on your PATH. Install it: `uv tool install evo-hq-cli==0.5.0-alpha.7` (or `pipx install evo-hq-cli==0.5.0-alpha.7`). Then re-invoke this skill.
132
+ > `evo-hq-cli` isn't on your PATH. Install it: `uv tool install evo-hq-cli==0.5.0-alpha.8` (or `pipx install evo-hq-cli==0.5.0-alpha.8`). Then re-invoke this skill.
131
133
 
132
134
  Do not try to auto-install. Host sandbox + network policy may block it; leaving the install as a user action keeps failure modes clear.
133
135
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: infra-setup
3
3
  description: Non-user-invocable provider/setup reference for evo backend switching, prerequisite checks, and auth/install guidance.
4
- evo_version: 0.5.0-alpha.7
4
+ evo_version: 0.5.0-alpha.8
5
5
  ---
6
6
 
7
7
  # Infra Setup
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: optimize
3
- description: Run the evo optimization loop with parallel subagents until interrupted.
3
+ description: Drive structured autoresearch iteration after evo:discover and the baseline commit -- scan-subagent cross-cutting analysis between rounds, frontier-based parent selection, ideator dispatch on stall, verifier pre/post hooks, annotation discipline. Width is set via subagents=N (1 for serial workloads, larger for parallel); the loop's structural value applies at any width.
4
4
  argument-hint: "[subagents=N] [budget=N] [stall=N]"
5
- evo_version: 0.5.0-alpha.7
5
+ evo_version: 0.5.0-alpha.8
6
6
  ---
7
7
 
8
8
  Run the `evo` optimization loop. Each round, the orchestrator writes structured briefs and spawns subagents that execute within them. Each subagent is semi-autonomous: it reads the pointer traces, forms the concrete edit, runs experiments, and can iterate within its branch. Runs until interrupted or the stall limit is reached.
@@ -102,6 +102,18 @@ evo defaults get subagents-only --json
102
102
 
103
103
  As your **very first actions, before the loop**, resolve each and arm it: run `evo autonomous on` / `evo subagents-only on` when it resolves on, or `evo autonomous off` / `evo subagents-only off` when an explicit instruction or stored default turned it off. If a behavior resolves off — whether from the user's instruction this run or a stored default — say so in your opening message (e.g. "autonomous off — running one round at a time, as you asked") so it's never invisible.
104
104
 
105
+ **Orchestrator driver (Claude Code only).** evo can drive the loop two ways: the prose loop below (default, every host), or a deterministic **dynamic workflow** (Claude Code only, opt-in). Resolve which as part of your very first actions:
106
+
107
+ 1. `evo host show` — must be `claude-code` for the workflow driver. If it prints `<not set>` (a pre-host workspace), determine your actual runtime from your own context (system prompt, env such as `CLAUDECODE=1`, self-identity): **only if you are genuinely Claude Code**, do the one-time host migration now (`evo host set claude-code`) and continue; if you are any other runtime, do NOT stamp the host here — leave it for Step 0.1 to record and use the prose loop. Any non-`claude-code` host uses the prose loop.
108
+ 2. `evo config get default-orchestrator` — `workflow` selects the workflow driver; anything else (including unset) resolves to `prose`. An explicit user instruction this run still wins.
109
+
110
+ If host is `claude-code` **and** the value is `workflow` **and** the Workflow tool is available in this session, do NOT drive the loop turn-by-turn. Launch the bundled workflow once instead:
111
+
112
+ - Call the **Workflow** tool with `scriptPath: ${CLAUDE_PLUGIN_ROOT}/skills/optimize/workflows/evo-optimize.js` and `args: {pluginRoot: "${CLAUDE_PLUGIN_ROOT}", subagents: <N>, budget: <N>, stall: <N>}`, using the round sizing you resolved above. **Pass all four keys explicitly — never omit one.** For `stall`, use the user's `/optimize stall=N` override if given, else the default 5. (The workflow's stop condition is the stall limit, so a dropped `stall` silently reverts it to 5.)
113
+ - Report the returned `runId` and tell the user to watch progress with `/workflows`. The workflow runs the round loop itself (orient → mandatory scan + cross-history axis check → ideators on stall/periodic → briefs → fan-out + verify → collect → frontier-select → stall); you do **not** execute "The Loop" section below.
114
+
115
+ Otherwise — any non-`claude-code` host, `default-orchestrator` unset/`prose`, or the Workflow tool unavailable — ignore this and follow **The Loop** below as the canonical driver. The workflow is only an execution strategy over the same `evo` CLI; gates, frontier, dashboard, and recovery are identical either way.
116
+
105
117
  **Autonomous mode.** Off lets you stop naturally at a turn boundary — finish a round, report, and stop. On arms the stop-nudge: at every turn boundary you are re-prompted to keep driving the loop until the **stall** limit is hit or the user interrupts. Without it, the loop does NOT force-continue across turn boundaries. To stop an autonomous run, the user runs `evo autonomous off` or `evo exit-optimize-mode`.
106
118
 
107
119
  **Subagents-only mode.** Off, the orchestrator may edit files directly — the optimization protocol still pushes edits through subagents (you write briefs; they edit in their worktrees), but a one-off orchestrator edit is not blocked. On arms the deny-gate: orchestrator file-mutation tools (Edit/Write, mutating Bash) are denied on an alternating cadence — 1st violation blocked, 2nd allowed, 3rd blocked, and so on — each block nudging you to delegate the edit to a subagent. It is a nudge, not a hard block: an edit can still land on an even-numbered attempt. Subagent edits (sessions with an `exp_id`) are never gated. To lift it, the user runs `evo subagents-only off` or `evo exit-optimize-mode`.
@@ -0,0 +1,568 @@
1
+ /*
2
+ * evo-optimize.js — Claude Code dynamic-workflow driver for the /evo:optimize round loop.
3
+ *
4
+ * This is the CODE form of plugins/evo/skills/optimize/SKILL.md "The Loop". It is an
5
+ * opt-in, Claude-Code-only driver; the prose skill remains the canonical, host-agnostic
6
+ * default. The workflow encodes the loop CONTROL: while/stall, mandatory scan + cross-history
7
+ * axis check, research escalation (ideators on stall / every ~5 commits), brief + diversity,
8
+ * fan-out + verify, collect + frontier-select. All domain work goes through the `evo` CLI inside
9
+ * agents — the script itself never touches the filesystem/shell.
10
+ *
11
+ * Treat this as a TEMPLATE: launch it as-is for the standard loop, or adapt the prompts /
12
+ * batch sizing / model routing per repo. The firm parts (mandatory scan, verify-before-
13
+ * count, stall, no-budget-in-condition) are the structure; brief content is adjustable.
14
+ *
15
+ * args (passed by optimize/SKILL.md Step 0.2):
16
+ * { pluginRoot, subagents, budget, stall }
17
+ * - pluginRoot : absolute path of the evo plugin (${CLAUDE_PLUGIN_ROOT}); used so nodes
18
+ * can Read the subagent skill by path (deterministic protocol loading).
19
+ * - subagents : round width N
20
+ * - budget : per-subagent iteration budget
21
+ * - stall : consecutive no-improve rounds before stopping
22
+ *
23
+ * Schemas are inlined (the workflow runtime is not relied on to resolve relative imports).
24
+ */
25
+
26
+ export const meta = {
27
+ name: 'evo-optimize',
28
+ description: 'Deterministic evo tree-search loop over the evo CLI (orient, scan, ideate-on-stall, brief, fan-out, verify, collect).',
29
+ phases: [
30
+ { title: 'Orient', detail: 'read state + select frontier parents to extend' },
31
+ { title: 'Scan', detail: 'mandatory parallel cross-cutting scan + structural aggregation (incl. cross-history axis check)' },
32
+ { title: 'Ideate', detail: 'research escalation: parallel ideators on stall / every ~5 commits' },
33
+ { title: 'Brief', detail: 'write one non-overlapping brief per subagent (reconciling ideator proposals)' },
34
+ { title: 'Optimize', detail: 'parallel optimization subagents (evo new/run)' },
35
+ { title: 'Verify', detail: 'validity audit + benchmark-noise confirm' },
36
+ { title: 'Collect', detail: 'prune dead lineages, record cross-cutting notes' },
37
+ ],
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Schemas (inlined)
42
+ // ---------------------------------------------------------------------------
43
+ const STATE = {
44
+ type: 'object',
45
+ // direction + evaluatedIds are required: the stall comparator needs direction (min vs max),
46
+ // and the mandatory scan keys off evaluatedIds (the state node must return [] when there are none).
47
+ required: ['bestScore', 'ceiling', 'frontier', 'direction', 'evaluatedIds'],
48
+ properties: {
49
+ bestScore: { type: 'number' },
50
+ bestExpId: { type: 'string' },
51
+ ceiling: { type: 'number' },
52
+ direction: { enum: ['max', 'min'] },
53
+ frontier: {
54
+ type: 'array',
55
+ items: {
56
+ type: 'object',
57
+ properties: { id: { type: 'string' }, score: { type: 'number' }, rank: { type: 'integer' } },
58
+ required: ['id'],
59
+ },
60
+ },
61
+ evaluatedIds: { type: 'array', items: { type: 'string' } },
62
+ committedCount: { type: 'integer' }, // total committed experiments (drives the periodic ideator trigger)
63
+ verifyRepeats: { type: 'integer' }, // benchmark noise profile (1 = deterministic, no confirm-loop)
64
+ summary: { type: 'string' }, // short scratchpad summary for subagent context
65
+ },
66
+ }
67
+
68
+ const FINDINGS = {
69
+ type: 'object',
70
+ required: ['findings'],
71
+ properties: {
72
+ findings: {
73
+ type: 'array',
74
+ items: {
75
+ type: 'object',
76
+ required: ['description', 'experiment_ids'],
77
+ properties: {
78
+ description: { type: 'string' },
79
+ experiment_ids: { type: 'array', items: { type: 'string' } },
80
+ evidence: { type: 'array', items: { type: 'string' } },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ }
86
+
87
+ const PATTERNS = {
88
+ type: 'object',
89
+ required: ['patterns'],
90
+ properties: {
91
+ patterns: {
92
+ type: 'array',
93
+ items: {
94
+ type: 'object',
95
+ required: ['kind', 'label'],
96
+ properties: {
97
+ kind: { type: 'string', enum: ['single', 'intersection', 'improver', 'axis-warning'] },
98
+ label: { type: 'string' },
99
+ experiment_ids: { type: 'array', items: { type: 'string' } },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ }
105
+
106
+ const BRIEFS = {
107
+ type: 'object',
108
+ required: ['briefs'],
109
+ properties: {
110
+ briefs: {
111
+ type: 'array',
112
+ items: {
113
+ type: 'object',
114
+ required: ['objective', 'parent', 'boundaries', 'pointerTraces'],
115
+ properties: {
116
+ objective: { type: 'string' },
117
+ parent: { type: 'string' },
118
+ boundaries: { type: 'string' },
119
+ pointerTraces: { type: 'array', items: { type: 'string' } },
120
+ hard: { type: 'boolean' },
121
+ },
122
+ },
123
+ },
124
+ },
125
+ }
126
+
127
+ const SUBAGENT_RESULT = {
128
+ type: 'object',
129
+ required: ['expIds', 'status', 'committedImprover'],
130
+ properties: {
131
+ expIds: { type: 'array', items: { type: 'string', pattern: '^exp_[0-9]+$' } }, // proof the protocol ran
132
+ bestExpId: { type: 'string' },
133
+ bestScore: { type: 'number' },
134
+ status: { type: 'string', enum: ['committed', 'evaluated', 'failed', 'none'] },
135
+ committedImprover: { type: 'boolean' },
136
+ gatesAdded: { type: 'array', items: { type: 'string' } },
137
+ learnings: { type: 'array', items: { type: 'string' } },
138
+ suggestions: { type: 'array', items: { type: 'string' } },
139
+ },
140
+ // NOTE: "a committed improver must carry bestExpId + a numeric bestScore" is intentionally NOT
141
+ // expressed as a JSON-Schema allOf/if/then here. The workflow StructuredOutput validator runs in
142
+ // strict mode and REJECTS allOf/if/then sub-schemas (verified empirically: the schema fails to
143
+ // compile and every agent call errors with "subagent completed without calling StructuredOutput").
144
+ // The improver requirement is enforced in the verify stage instead (the bestExpId / numeric
145
+ // bestScore guard), which is functionally equivalent for stall accounting and auditing.
146
+ }
147
+
148
+ const VERDICT = {
149
+ type: 'object',
150
+ required: ['valid'],
151
+ properties: {
152
+ valid: { type: 'boolean' },
153
+ reasons: { type: 'array', items: { type: 'string' } },
154
+ },
155
+ }
156
+
157
+ // Implement stage output: experiment allocated + edited in its worktree, NOT yet run.
158
+ const IMPL_RESULT = {
159
+ type: 'object',
160
+ required: ['expId', 'worktree'],
161
+ properties: {
162
+ expId: { type: 'string', pattern: '^exp_[0-9]+$' },
163
+ worktree: { type: 'string' },
164
+ summary: { type: 'string' },
165
+ },
166
+ }
167
+
168
+ // Pre-run verifier verdict (design-time cheating gate).
169
+ const PREVERDICT = {
170
+ type: 'object',
171
+ required: ['pass'],
172
+ properties: {
173
+ pass: { type: 'boolean' },
174
+ findings: { type: 'array', items: { type: 'string' } },
175
+ },
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Helpers (pure JS — control-plane only)
180
+ // ---------------------------------------------------------------------------
181
+ // `args` may arrive as an object OR as a JSON STRING — the Workflow `args` param is frequently
182
+ // threaded to the script verbatim as a string (confirmed empirically). Coerce so the four knobs
183
+ // resolve either way; then Number() so even stringified numbers ("1") coerce, with NaN || 5 -> 5.
184
+ const A = typeof args === 'string'
185
+ ? (() => { try { return JSON.parse(args) } catch (_) { return {} } })()
186
+ : (args || {})
187
+ const pr = A.pluginRoot || ''
188
+ const WIDTH = Number(A.subagents) || 5
189
+ const ITER = Number(A.budget) || 5
190
+ const LIMIT = Number(A.stall) || 5
191
+ // Fire ideators (research escalation) once the stall counter reaches this — strictly BELOW the hard
192
+ // stall limit, so the loop researches its way toward a new direction before it gives up.
193
+ const IDEATE_STALL = Math.max(1, Math.min(3, LIMIT - 1))
194
+ const IDEATE_EVERY_COMMITS = 5 // periodic research cadence (matches prose step 6b)
195
+ const PREVERIFY_MAX = 3 // pre-run verify <-> revise attempts before discarding a rigged edit
196
+ // Experiments per scan agent. Heuristic for the prose "small enough to read in one pass" rule —
197
+ // the workflow can't recursively self-partition like the prose loop, so this is fixed up front.
198
+ // Lower it for heavy traces (many tasks / long messages); raise it for tiny traces.
199
+ const SCAN_BATCH = 6
200
+
201
+ function betterResult(a, b, direction) {
202
+ if (!a) return b
203
+ if (!b) return a
204
+ const sa = typeof a.bestScore === 'number' ? a.bestScore : null
205
+ const sb = typeof b.bestScore === 'number' ? b.bestScore : null
206
+ if (sa === null) return b
207
+ if (sb === null) return a
208
+ return (direction === 'min' ? sb < sa : sb > sa) ? b : a
209
+ }
210
+
211
+ function chunk(arr, n) {
212
+ const out = []
213
+ for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n))
214
+ return out
215
+ }
216
+
217
+ // Compact label for a batch of ids: factor out the shared leading chars.
218
+ // e.g. ["exp_0003","exp_0004","exp_0005"] -> "exp_000[3,4,5]"
219
+ function commonPrefix(strs) {
220
+ if (!strs.length) return ''
221
+ let p = strs[0]
222
+ for (const s of strs) {
223
+ while (p && !s.startsWith(p)) p = p.slice(0, -1)
224
+ if (!p) break
225
+ }
226
+ return p
227
+ }
228
+ function batchLabel(b) {
229
+ if (!b.length) return 'frontier'
230
+ if (b.length === 1) return b[0]
231
+ const p = commonPrefix(b)
232
+ return p.length > 1 ? `${p}[${b.map((x) => x.slice(p.length)).join(',')}]` : b.join(',')
233
+ }
234
+
235
+ // Diversity check: drop briefs whose pointer-trace sets overlap heavily with an earlier one.
236
+ function dedupeBriefs(briefs) {
237
+ const kept = []
238
+ for (const b of briefs) {
239
+ const ptr = new Set(b.pointerTraces || [])
240
+ const clash = kept.some((k) => {
241
+ const o = new Set(k.pointerTraces || [])
242
+ const inter = [...ptr].filter((x) => o.has(x)).length
243
+ const overlap = inter / Math.max(1, Math.min(ptr.size, o.size))
244
+ return k.parent === b.parent && overlap >= 0.6
245
+ })
246
+ if (!clash) kept.push(b)
247
+ }
248
+ return kept
249
+ }
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Node prompt builders
253
+ // ---------------------------------------------------------------------------
254
+ function statePrompt() {
255
+ return [
256
+ 'Read-only. Do NOT edit files or run experiments. Run these and parse their output:',
257
+ '`evo scratchpad`, `evo frontier` (already prints a JSON envelope), `evo status`, `evo awaiting`.',
258
+ 'Also read `.evo/project.md` for the metric goal, direction, and the benchmark-determinism line.',
259
+ 'Return: bestScore + bestExpId; the theoretical ceiling (1.0 for max metric, 0.0 for min)',
260
+ 'and direction; the frontier nodes ALREADY ranked by the configured strategy',
261
+ '(id, score, rank) — preserve evo\'s ordering, do not re-rank; the list of evaluated-but-',
262
+ 'undecided experiment ids; committedCount (number of committed experiments, from `evo status`);',
263
+ 'verifyRepeats (from project.md: 1 if deterministic, 3 if sampling-based / variance-expected);',
264
+ 'and a 2-3 sentence scratchpad summary for subagent context.',
265
+ ].join(' ')
266
+ }
267
+
268
+ // Verbatim scan brief from optimize/SKILL.md step 3.
269
+ function scanBrief(batch) {
270
+ return [
271
+ 'You are a read-only evo scan sub-agent. Do not run experiments or edit code.',
272
+ '',
273
+ 'Start by reading `.evo/project.md` to understand the optimization goal and metric. All your findings should be relevant to this goal.',
274
+ '',
275
+ `Your batch: ${JSON.stringify(batch)}.`,
276
+ '',
277
+ 'For each experiment, read its `outcome.json` and `traces/task_*.json` under `.evo/run_*/experiments/<id>/attempts/NNN/`. Also consider `hypothesis` and prose `error` text.',
278
+ '',
279
+ 'Find patterns that will populate the next round\'s subagent briefs:',
280
+ '- Shared failure causes -- root-cause reasons recurring across 2+ experiments (the *why*, not the surface gate name).',
281
+ '- Wall patterns -- approaches or gates multiple experiments consistently fail on.',
282
+ '- Compound-failure standouts -- single experiments hitting multiple failure modes.',
283
+ '',
284
+ 'Evidence must be VERBATIM quotes from outcome.json fields, trace messages, or error text -- not paraphrases. If you cannot cite verbatim evidence for a finding, drop it. Evidence: short quotes (<200 chars), max 3 per finding.',
285
+ 'Return JSON only: {"findings":[{"description","experiment_ids":[],"evidence":[]}]}.',
286
+ ].join('\n')
287
+ }
288
+
289
+ function aggregatePrompt(ids) {
290
+ return [
291
+ 'Read-only. These experiments', JSON.stringify(ids), 'are a MIX of evaluated-but-undecided nodes',
292
+ 'and committed frontier nodes. Load each outcome.json under the active run dir',
293
+ '(`.evo/run_*/experiments/<id>/attempts/NNN/outcome.json`) in Python; the `outcome` field tells you which is which.',
294
+ 'From the EVALUATED ones aggregate: co-occurring gate_failures; shared zero-score task ids in',
295
+ 'benchmark.result.tasks; recurring substrings in error — emit each single-pattern set AND every',
296
+ 'pairwise intersection where >=2 experiments exhibit both (kind:"intersection").',
297
+ 'From the COMMITTED ones enumerate improvers (outcome=committed — evo already applied the metric',
298
+ 'direction when it committed; do NOT re-derive improvement from a raw score>parent comparison) as kind:"improver".',
299
+ 'CROSS-HISTORY AXIS CHECK (look beyond this batch): run `evo tree` and read the `hypothesis` of ALL committed',
300
+ 'experiments in the run. If 3+ STRUCTURALLY DISTINCT hypotheses (not parameter sweeps of one idea) committed at',
301
+ '~the same score (a plateau), the bottleneck is not where those hypotheses aimed — emit kind:"axis-warning" whose',
302
+ 'label names the saturated axis AND suggests the orthogonal axis (harness, score definition, input data, or a',
303
+ 'different mechanism) the next briefs should pivot to. At most one axis-warning.',
304
+ 'Return JSON only.',
305
+ ].join(' ')
306
+ }
307
+
308
+ function briefPrompt(state, findings, patterns, parents, ideated) {
309
+ return [
310
+ 'You are the evo orchestrator\'s brief writer.',
311
+ 'State summary:', state.summary || '',
312
+ '\nVerified scan findings:', JSON.stringify(findings),
313
+ '\nStructural patterns (incl. intersections, improvers, and any axis-warning):', JSON.stringify(patterns),
314
+ '\nSelected parent nodes:', JSON.stringify(parents.map((p) => p.id)),
315
+ ideated
316
+ ? '\nFRESH IDEATOR PROPOSALS may be available — read `.evo/run_*/ideator/proposals.jsonl` and reconcile BEFORE writing: skip any whose technique was already tried (`evo discards --like "<keyword>"`); score the rest by expected_score_uplift x confidence (frontier_extrapolation > failure_analysis > literature, all else equal); let the top 1-2 become brief objectives, citing the proposal\'s hypothesis/technique. Proposals are advisory — if none beat the in-graph scan findings, ignore them.'
317
+ : '',
318
+ '\nIf the patterns include an "axis-warning", the current axis is saturated — target the ORTHOGONAL axis it names rather than iterating the plateaued one.',
319
+ `\nWrite up to ${WIDTH} briefs (use the full round width of ${WIDTH} whenever you can find that many genuinely DISTINCT objectives — multiple briefs MAY branch from the SAME parent when fewer than ${WIDTH} frontier parents exist, as long as each attacks a different surface; do not pad with redundant briefs). One per subagent, each with four fields:`,
320
+ '1. objective -- one sentence naming WHERE in system behavior the gain hides, with evidence; NO file/function/edit names.',
321
+ '2. parent -- which experiment id to branch from (choose from the selected parents).',
322
+ '3. boundaries -- what NOT to try and why (discarded approaches, gates not to regress, what adjacent briefs this round do).',
323
+ '4. pointerTraces -- task ids to study first, one-line reason each.',
324
+ 'Mark hard:true on any brief needing deep trace analysis.',
325
+ 'The briefs MUST NOT collapse onto each other -- distinct objectives, non-overlapping pointer traces, different surfaces.',
326
+ 'Return JSON only.',
327
+ ].join(' ')
328
+ }
329
+
330
+ // IMPLEMENT — allocate + edit, but do NOT run (a pre-run verifier audits the edit first).
331
+ function implementPrompt(brief, parent, state) {
332
+ return [
333
+ `First, load and follow the evo subagent skill: Read ${pr}/skills/subagent/SKILL.md IN FULL and follow it as your operating protocol — do not skip it even if the brief looks simple.`,
334
+ `Allocate your experiment via \`evo new --parent ${parent}\`, then edit inside the returned worktree to implement the brief.`,
335
+ 'IMPORTANT: do NOT run `evo run` yet — a pre-run verifier audits your change first. Stop once the edit is complete.',
336
+ 'Do NOT edit benchmark, gate, or framework code; do NOT weaken/bypass any gate.',
337
+ '',
338
+ 'Your brief:',
339
+ `- Objective: ${brief.objective}`,
340
+ `- Parent node: ${parent}`,
341
+ `- Boundaries / anti-patterns: ${brief.boundaries}`,
342
+ `- Pointer traces: ${(brief.pointerTraces || []).join('; ')}`,
343
+ '',
344
+ 'Context (current state):', state.summary || '',
345
+ '',
346
+ 'Return the experiment id you created (exp_NNNN), its worktree path, and a 1-2 sentence change summary.',
347
+ ].join('\n')
348
+ }
349
+
350
+ // PRE-VERIFY — independent design-time cheating audit of the uncommitted edit (evo:verifier, pre-phase).
351
+ function preVerifyPrompt(expId, worktree) {
352
+ return [
353
+ `Read-only PRE-RUN verifier audit (design-time cheating) for experiment ${expId}.`,
354
+ `Inspect the UNCOMMITTED change in its worktree (\`git -C ${worktree} diff\` plus the edited files).`,
355
+ 'Set pass:false if the change does ANY of: leaks held-out / validation / gold answers into what the target sees;',
356
+ 'subsets or narrows the eval/benchmark command to only easy cases; adds a new artifact that needs a gate but has none;',
357
+ 'rests on a generic/empty hypothesis; short-circuits via a cache; fabricates artifacts; or edits benchmark/gate/framework code.',
358
+ 'Otherwise pass:true. Return {pass, findings:[]}; findings are concrete, fixable statements. Default pass:false if you cannot confirm it is clean.',
359
+ ].join(' ')
360
+ }
361
+
362
+ // REVISE — feed the verifier findings back to fix the edit (still no run).
363
+ function revisePrompt(expId, worktree, findings) {
364
+ return [
365
+ `The pre-run verifier FAILED experiment ${expId} for these design-time issues:`, JSON.stringify(findings || []),
366
+ `Revise the edit in its worktree (${worktree}) to address EVERY finding WITHOUT weakening the brief's objective or gaming the metric.`,
367
+ 'Do NOT run `evo run`. Do NOT edit benchmark/gate/framework code. Return a 1-2 sentence summary of the fix.',
368
+ ].join(' ')
369
+ }
370
+
371
+ // RUN — evaluate + commit the (pre-verified) experiment.
372
+ function runPrompt(expId) {
373
+ return [
374
+ `Run \`evo run ${expId}\` to evaluate and (if it improves and passes gates) commit it.`,
375
+ 'If it exits GATE_FAILED, do not fight the gate — report status=evaluated.',
376
+ `Return: expIds:["${expId}"]; status (committed|evaluated|failed|none); committedImprover = true ONLY if evo printed COMMITTED;`,
377
+ 'bestExpId + bestScore (required when committedImprover is true); any gates added; learnings.',
378
+ ].join(' ')
379
+ }
380
+
381
+ // DISCARD — pre-verify never satisfied; do not run a rigged experiment.
382
+ function discardPrompt(expId, findings) {
383
+ return [
384
+ `Pre-run verification could not be satisfied for ${expId} after ${PREVERIFY_MAX} revision attempts:`, JSON.stringify(findings || []),
385
+ `Annotate the experiment (\`evo annotate ${expId} "pre-verify failed: ..."\`), then run`,
386
+ `\`evo discard ${expId} --reason "pre-verify: design-time cheating not resolved"\` so a rigged experiment is never run or committed.`,
387
+ 'Return a one-line confirmation.',
388
+ ].join(' ')
389
+ }
390
+
391
+ // One ideator brief (failure_analysis | literature | frontier_extrapolation). Dispatched via
392
+ // agentType 'evo:ideator' so the agent gets the ideator system prompt + its tool set (incl.
393
+ // WebSearch/WebFetch for literature). It appends proposals to .evo/run_*/ideator/proposals.jsonl.
394
+ function ideatorPrompt(brief) {
395
+ return [
396
+ `brief=${brief}`,
397
+ '(workspace: infer from the current directory by walking up until you find `.evo/`.)',
398
+ 'Follow your ideator protocol: produce proposals for this brief and append them as JSONL to',
399
+ '`.evo/run_*/ideator/proposals.jsonl` in a single final write, then return a short JSON summary.',
400
+ ].join('\n')
401
+ }
402
+
403
+ function auditPrompt(expId) {
404
+ return [
405
+ `Read-only validity audit of experiment ${expId}.`,
406
+ 'Read its artifacts under `.evo/run_*/experiments/<id>/attempts/NNN/` (diff.patch, outcome.json, benchmark.log).',
407
+ 'Check: did it edit benchmark / gate / framework code? does the held-out slice still pass?',
408
+ 'is the score reproducible / not short-circuited by a cache? any constant-return or metric-gaming pattern?',
409
+ 'Return {valid:bool, reasons:[]}. Default valid:false if you cannot confirm.',
410
+ ].join(' ')
411
+ }
412
+
413
+ function collectPrompt(results, round) {
414
+ return [
415
+ `Round ${round} results:`, JSON.stringify(results.map((r) => ({ expIds: r.expIds, status: r.status, improver: r.committedImprover }))),
416
+ '\nRead each evaluated node\'s outcome.json (`.evo/run_*/experiments/<id>/attempts/NNN/outcome.json`) and spot shared failure modes the per-subagent summaries glossed over.',
417
+ 'Where a committed node has 3+ children that all regressed, run `evo prune <id> --reason "exhausted: ..."` (never `evo discard` a committed node).',
418
+ 'Record cross-cutting learnings with `evo set <id> --note "..."` and any workspace insight with `evo note "..."`.',
419
+ 'Return a one-line summary of what you pruned and noted.',
420
+ ].join(' ')
421
+ }
422
+
423
+ // Per-brief lane: implement -> pre-verify <-> revise loop -> run -> post-audit, repeated up to the
424
+ // iteration budget (deepening the branch each time a committed improver lands). The independent
425
+ // evo:verifier gates EACH run for design-time cheating BEFORE the experiment is evaluated; its
426
+ // findings are fed back to a revise agent on the same experiment until it passes or is discarded.
427
+ async function runBrief(brief, state) {
428
+ let parent = brief.parent
429
+ let best = null
430
+ for (let depth = 0; depth < ITER; depth++) {
431
+ const impl = await agent(implementPrompt(brief, parent, state), {
432
+ schema: IMPL_RESULT, model: brief.hard ? 'opus' : 'sonnet', phase: 'Optimize', label: `impl:${parent}#${depth}`,
433
+ })
434
+ if (!impl || !impl.expId) break
435
+
436
+ // pre-verify <-> revise feedback loop (design-time cheating gate)
437
+ let pv = null
438
+ for (let v = 0; v < PREVERIFY_MAX; v++) {
439
+ pv = await agent(preVerifyPrompt(impl.expId, impl.worktree), {
440
+ schema: PREVERDICT, agentType: 'evo:verifier', phase: 'Verify', label: `preverify:${impl.expId}#${v}`,
441
+ })
442
+ if (pv && pv.pass) break
443
+ if (v < PREVERIFY_MAX - 1) {
444
+ await agent(revisePrompt(impl.expId, impl.worktree, pv && pv.findings), {
445
+ model: brief.hard ? 'opus' : 'sonnet', phase: 'Optimize', label: `revise:${impl.expId}#${v}`,
446
+ })
447
+ }
448
+ }
449
+ if (!pv || !pv.pass) {
450
+ await agent(discardPrompt(impl.expId, pv && pv.findings), { phase: 'Verify', label: `discard:${impl.expId}` })
451
+ break // couldn't produce a clean edit on this branch — stop spending budget here
452
+ }
453
+
454
+ // run -> evaluate + commit
455
+ const r = await agent(runPrompt(impl.expId), { schema: SUBAGENT_RESULT, phase: 'Optimize', label: `run:${impl.expId}` })
456
+ if (!r) break
457
+
458
+ // post-run validity audit (evo:verifier, post-phase) on committed improvers
459
+ let valid = true
460
+ if (r.committedImprover) {
461
+ if (!r.bestExpId || typeof r.bestScore !== 'number') {
462
+ valid = false
463
+ } else {
464
+ const audit = await agent(auditPrompt(r.bestExpId), { schema: VERDICT, agentType: 'evo:verifier', phase: 'Verify', label: `audit:${r.bestExpId}` })
465
+ valid = !!(audit && audit.valid)
466
+ if (valid && (Number(state.verifyRepeats) || 1) > 1) {
467
+ log(`note: ${r.bestExpId} on a noisy benchmark (repeats=${state.verifyRepeats}); confirm-loop pending the evo rescore affordance — relying on the held-out gate`)
468
+ }
469
+ }
470
+ }
471
+ const scored = { ...r, valid }
472
+ best = betterResult(best, scored, state.direction)
473
+
474
+ if (valid && r.committedImprover && r.bestExpId) {
475
+ parent = r.bestExpId // deepen: extend the new commit on the next budget iteration
476
+ } else {
477
+ break // evaluated / failed / invalid — stop deepening this branch
478
+ }
479
+ }
480
+ return best
481
+ }
482
+
483
+ // ---------------------------------------------------------------------------
484
+ // The loop
485
+ // ---------------------------------------------------------------------------
486
+ let stall = 0
487
+ let round = 0
488
+ let lastIdeatedCommit = 0 // committedCount at the last ideator dispatch (periodic cadence)
489
+ let ideatedThisStall = false // fire ideators once per stall episode, not every stalled round
490
+
491
+ log(`evo-optimize start: subagents=${WIDTH} budget=${ITER} stall=${LIMIT} | argsType=${typeof args} A.subagents=${A.subagents} A.budget=${A.budget} A.stall=${A.stall}`)
492
+
493
+ while (stall < LIMIT) {
494
+ round += 1
495
+
496
+ phase('Orient')
497
+ const state = await agent(statePrompt(), { schema: STATE, agentType: 'Explore', model: 'haiku', phase: 'Orient', label: `state:r${round}` })
498
+ if (state.bestScore === state.ceiling) { log(`ceiling reached (best=${state.bestScore}) — stopping`); break }
499
+ const parents = (state.frontier || []).slice(0, WIDTH)
500
+ if (parents.length === 0) { log('no explorable frontier nodes — stopping'); break }
501
+
502
+ // N1 + N1.5 — mandatory parallel scan + structural aggregation (barrier).
503
+ // The scan runs EVERY round (hard rule). When there are no evaluated-undecided nodes yet
504
+ // (e.g. round 1, right after the baseline), fall back to scanning the committed frontier nodes
505
+ // so at least one scan agent still runs before briefs.
506
+ phase('Scan')
507
+ const evaluatedIds = state.evaluatedIds || []
508
+ const frontierIds = (state.frontier || []).map((f) => f.id).filter(Boolean)
509
+ const scanTargets = evaluatedIds.length ? evaluatedIds : frontierIds
510
+ const batches = chunk(scanTargets, SCAN_BATCH)
511
+ const scanThunks = batches.map((b) => () => agent(scanBrief(b), { schema: FINDINGS, agentType: 'Explore', phase: 'Scan', label: `scan ${b.length}: ${batchLabel(b)}` }))
512
+ // Aggregate sees BOTH evaluated-undecided nodes (for failure intersections) AND committed
513
+ // frontier nodes (so the improver enumeration has committed experiments to draw from).
514
+ const aggregateIds = [...new Set([...evaluatedIds, ...frontierIds])]
515
+ const aggThunk = aggregateIds.length
516
+ ? [() => agent(aggregatePrompt(aggregateIds), { schema: PATTERNS, agentType: 'Explore', phase: 'Scan', label: 'aggregate' })]
517
+ : []
518
+ const scanResults = (await parallel([...scanThunks, ...aggThunk])).filter(Boolean)
519
+ const findings = scanResults.flatMap((r) => (r && r.findings) ? r.findings : [])
520
+ const patterns = scanResults.flatMap((r) => (r && r.patterns) ? r.patterns : [])
521
+
522
+ // N1.7 — research escalation (6b): on stall (before the hard limit) or every ~5 commits, fire
523
+ // the three ideators in parallel. They append proposals to .evo/run_*/ideator/proposals.jsonl;
524
+ // parallel() blocks until all return (the "block until proposals land" the prose does via evo wait).
525
+ const commits = Number(state.committedCount) || 0
526
+ const stalledTrigger = stall >= IDEATE_STALL && !ideatedThisStall
527
+ const periodicTrigger = commits - lastIdeatedCommit >= IDEATE_EVERY_COMMITS
528
+ let ideated = false
529
+ if (stalledTrigger || periodicTrigger) {
530
+ phase('Ideate')
531
+ await parallel(['frontier_extrapolation', 'failure_analysis', 'literature'].map((b) => () =>
532
+ agent(ideatorPrompt(b), { agentType: 'evo:ideator', phase: 'Ideate', label: `ideate:${b}` })))
533
+ lastIdeatedCommit = commits
534
+ if (stalledTrigger) ideatedThisStall = true
535
+ ideated = true
536
+ log(`ideators fired (trigger: ${stalledTrigger ? 'stall' : 'periodic'}, stall=${stall}, commits=${commits})`)
537
+ }
538
+
539
+ // N2 — brief writer (judgment): reconciles ideator proposals (6c) + acts on axis-warning; JS diversity dedupe.
540
+ phase('Brief')
541
+ const briefOut = await agent(briefPrompt(state, findings, patterns, parents, ideated), { schema: BRIEFS, phase: 'Brief', label: `briefs:r${round}` })
542
+ const briefs = dedupeBriefs((briefOut && briefOut.briefs) || [])
543
+ if (briefs.length === 0) { log('no briefs produced — stopping'); break }
544
+
545
+ // N3..N4 — fan out one lane per brief; each lane: implement -> pre-verify<->revise -> run -> post-audit.
546
+ const results = (await parallel(briefs.map((b) => () => runBrief(b, state)))).filter(Boolean)
547
+
548
+ // N5 — collect: prune dead lineages, record notes.
549
+ phase('Collect')
550
+ await agent(collectPrompt(results, round), { phase: 'Collect', label: `collect:r${round}` })
551
+
552
+ // Loop control: stall resets only when this round produced a VERIFIED committed score that
553
+ // beats the PRIOR BEST in the metric direction. A committed improver that beat its own parent
554
+ // but not the global best does NOT reset stall (it's progress on a branch, not a new best).
555
+ // No budget in the condition.
556
+ const dir = state.direction || 'max'
557
+ const gains = results
558
+ .filter((r) => r.committedImprover && r.valid !== false && typeof r.bestScore === 'number')
559
+ .map((r) => r.bestScore)
560
+ const roundBest = gains.length ? (dir === 'min' ? Math.min(...gains) : Math.max(...gains)) : null
561
+ const improved = roundBest !== null && (dir === 'min' ? roundBest < state.bestScore : roundBest > state.bestScore)
562
+ stall = improved ? 0 : stall + 1
563
+ if (improved) ideatedThisStall = false // new best → a fresh stall episode may re-trigger ideators later
564
+ log(`round ${round}: improved=${improved} roundBest=${roundBest} prevBest=${state.bestScore} stall=${stall}/${LIMIT} spent=${budget.spent()}`)
565
+ }
566
+
567
+ log(`optimize workflow finished after ${round} round(s), final stall=${stall}/${LIMIT}`)
568
+ return { rounds: round, finalStall: stall }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: report
3
3
  description: Print the dashboard's dot chart (score over experiment order, status colors, best-path stair) inline in the terminal for every run in the workspace. Use when the user invokes /evo:report, asks for a quick score chart without opening the dashboard, or wants the scatter plot in chat output.
4
- evo_version: 0.5.0-alpha.7
4
+ evo_version: 0.5.0-alpha.8
5
5
  ---
6
6
 
7
7
  # Report
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: subagent
3
3
  description: Protocol that evo optimization subagents follow when dispatched from /optimize. Auto-loaded by spawned subagents via their host's skill loader. The orchestrator may also invoke this skill to understand the brief shape its dispatched subagents expect + what they're required to emit -- useful when writing briefs or debugging a subagent's behavior.
4
- evo_version: 0.5.0-alpha.7
4
+ evo_version: 0.5.0-alpha.8
5
5
  ---
6
6
 
7
7
  # Evo Subagent Protocol
@@ -14,13 +14,17 @@ What you can pull/dispatch/read as a subagent. Each line is a triggering conditi
14
14
 
15
15
  ```
16
16
  skills you may pull (Skill tool)
17
- └── evo:finetuning before writing or changing any train.py
17
+ └── evo:finetuning before writing or changing any train.py -- technique
18
+ choice, training recipe, observability, retry discipline.
18
19
 
19
20
  subagents you dispatch (Task tool, subagent_type=...)
20
- └── evo:verifier MANDATORY pre AND post every evo run.
21
- Pre: ~30s static analysis BEFORE the experiment runs
22
- (block on failure -- fix and retry).
23
- Post: result-validity audit AFTER it commits.
21
+ ├── evo:verifier MANDATORY pre AND post every `evo run`.
22
+ Pre: static analysis before the experiment runs
23
+ (block on failure -- fix and retry).
24
+ Post: result-validity audit after it commits.
25
+ └── evo:benchmark-reviewer POST-COMMIT only, mode=review-experiment --
26
+ per-task failure classification + annotations.
27
+ Skip on evaluated/discarded/failed outcomes.
24
28
 
25
29
  references (Read tool, on demand)
26
30
  ├── discover/references/
@@ -34,6 +38,9 @@ references (Read tool, on demand)
34
38
 
35
39
  └── finetuning/references/
36
40
  ├── glue.md train.py I/O contract evo expects
41
+ ├── observability.md wandb/trackio/mlflow wiring -- env-driven
42
+ │ detection, TRL report_to options, custom-loop
43
+ │ patterns. Read when writing a training script.
37
44
  ├── diagnostics.md per-failure-mode diagnostics
38
45
  ├── false-progress.md what doesn't count as improvement
39
46
  ├── trace-schema.md per-task trace JSON schema
@@ -271,6 +278,19 @@ The ack flag is required when the worktree has any untracked, non-gitignored fil
271
278
  - Structural (benchmark broken, evo misconfigured): report to orchestrator and stop.
272
279
  - Not worth fixing: `evo discard <id> --reason "..."`.
273
280
 
281
+ ### 6b. Review your own failures (committed experiments only)
282
+
283
+ After a `COMMITTED` outcome, before annotating yourself, spawn `evo:benchmark-reviewer` in review-experiment mode. It reads the per-task traces and the eval-runner log you just produced, classifies failures into a small taxonomy, and writes per-task annotations via `evo annotate <exp> --task K`. This is the data the next experiment's hypothesis is built on -- skip it and the orchestrator picks a frontier from `passed/failed` booleans with no diagnosis.
284
+
285
+ ```
286
+ Task(subagent_type="evo:benchmark-reviewer",
287
+ prompt="mode=review-experiment\nworkspace=<workspace path>\nexperiment_id=<your exp_id>")
288
+ ```
289
+
290
+ The returned JSON includes `failure_breakdown`, `top_failure_pattern`, and `next_step_signal`. Read it, include the breakdown + top pattern in your final handoff message, but **do not act on `next_step_signal` yourself** -- it's a hint for the next experiment, which isn't yours to design.
291
+
292
+ Skip this step for `EVALUATED` (regressed, will be discarded), `FAILED` (infra error), or `DISCARDED` outcomes -- there's no meaningful per-task data worth classifying.
293
+
274
294
  ### 7. Annotate
275
295
 
276
296
  ```bash