orizu 0.5.30 → 0.5.32

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.
Files changed (82) hide show
  1. package/dist/app-commands.d.ts +2 -0
  2. package/dist/app-commands.d.ts.map +1 -1
  3. package/dist/app-commands.js +10 -4
  4. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  5. package/dist/gepa-engine-dispatch.js +16 -1
  6. package/dist/help.js +7 -7
  7. package/dist/hosted-agent-prompt.d.ts +14 -0
  8. package/dist/hosted-agent-prompt.d.ts.map +1 -0
  9. package/dist/hosted-agent-prompt.js +17 -0
  10. package/dist/hosted-gepa-cli.d.ts.map +1 -1
  11. package/dist/hosted-gepa-cli.js +7 -1
  12. package/dist/hosted-harness-claude.d.ts.map +1 -1
  13. package/dist/hosted-harness-claude.js +4 -1
  14. package/dist/hosted-harness-opencode.d.ts +1 -0
  15. package/dist/hosted-harness-opencode.d.ts.map +1 -1
  16. package/dist/hosted-harness-opencode.js +27 -7
  17. package/dist/hosted-harness.d.ts +30 -1
  18. package/dist/hosted-harness.d.ts.map +1 -1
  19. package/dist/hosted-harness.js +5 -0
  20. package/dist/hosted-optimization-prebaked-marker.d.ts +9 -0
  21. package/dist/hosted-optimization-prebaked-marker.d.ts.map +1 -0
  22. package/dist/hosted-optimization-prebaked-marker.js +22 -0
  23. package/dist/hosted-tool-hints.d.ts +56 -0
  24. package/dist/hosted-tool-hints.d.ts.map +1 -0
  25. package/dist/hosted-tool-hints.js +105 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -5
  28. package/dist/instruction-set-gepa-launch.d.ts +21 -2
  29. package/dist/instruction-set-gepa-launch.d.ts.map +1 -1
  30. package/dist/instruction-set-gepa-launch.js +123 -3
  31. package/dist/markdown-report-input.d.ts.map +1 -1
  32. package/dist/markdown-report-input.js +1 -1
  33. package/dist/optimizations-promote-cli.d.ts.map +1 -1
  34. package/dist/optimizations-promote-cli.js +19 -0
  35. package/dist/preview-runtime.d.ts +7 -0
  36. package/dist/preview-runtime.d.ts.map +1 -1
  37. package/dist/preview-runtime.js +49 -5
  38. package/dist/provider-refusals.js +2 -2
  39. package/dist/report-markdown-blankness.d.ts +6 -0
  40. package/dist/report-markdown-blankness.d.ts.map +1 -1
  41. package/dist/report-markdown-blankness.js +47 -4
  42. package/dist/seed-against-production.d.ts +11 -0
  43. package/dist/seed-against-production.d.ts.map +1 -0
  44. package/dist/seed-against-production.js +1 -0
  45. package/dist/setup-and-skills-commands.d.ts.map +1 -1
  46. package/dist/setup-and-skills-commands.js +123 -9
  47. package/dist/skill-installer.d.ts +32 -0
  48. package/dist/skill-installer.d.ts.map +1 -1
  49. package/dist/skill-installer.js +228 -1
  50. package/package.json +1 -1
  51. package/src/app-commands.ts +13 -3
  52. package/src/gepa-engine-dispatch.ts +18 -1
  53. package/src/help.ts +7 -7
  54. package/src/hosted-agent-prompt.ts +18 -0
  55. package/src/hosted-gepa-cli.ts +7 -1
  56. package/src/hosted-harness-claude.ts +4 -1
  57. package/src/hosted-harness-opencode.ts +28 -7
  58. package/src/hosted-harness.ts +32 -1
  59. package/src/hosted-optimization-prebaked-marker.ts +20 -0
  60. package/src/hosted-tool-hints.ts +153 -0
  61. package/src/index.ts +2 -5
  62. package/src/instruction-set-gepa-launch.ts +161 -3
  63. package/src/markdown-report-input.ts +3 -1
  64. package/src/optimizations-promote-cli.ts +20 -2
  65. package/src/preview-runtime.ts +44 -5
  66. package/src/provider-refusals.ts +2 -2
  67. package/src/report-markdown-blankness.ts +59 -4
  68. package/src/seed-against-production.ts +10 -0
  69. package/src/setup-and-skills-commands.ts +160 -8
  70. package/src/skill-installer.ts +315 -0
  71. package/vendor/skills/orizu/SKILL.md +3 -3
  72. package/vendor/skills/orizu/flows/first-win.md +3 -1
  73. package/vendor/skills/orizu/flows/promote.md +2 -1
  74. package/vendor/skills/orizu/references/building-apps.md +2 -0
  75. package/vendor/skills/orizu/references/building-judges.md +138 -8
  76. package/vendor/skills/orizu/references/cli-reference.md +2 -2
  77. package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +1 -2
  78. package/vendor/skills/orizu/references/optimization-reports.md +1 -1
  79. package/vendor/skills/orizu/references/optimization-with-gepa.md +22 -4
  80. package/vendor/skills/orizu/references/primer.md +2 -2
  81. package/vendor/skills/orizu/references/prompt-control-plane.md +6 -1
  82. package/vendor/skills/orizu/scripts/test-app.mjs +53 -4
@@ -18,13 +18,17 @@ The downstream judge-building steps are identical for all three forms. Also
18
18
  arrive with:
19
19
 
20
20
  - one or more specific binary **failure modes**, such as
21
- `correctly_identified_issue` or `escalated_when_required`;
21
+ `correctly_identified_issue` or `escalated_when_required`, or one pairwise or
22
+ multi-class decision (see “Multi-class and pairwise judges”);
22
23
  - the original dataset row for every label; and
23
24
  - multiply labeled rows for measuring human–human agreement.
24
25
 
25
26
  If the labels are Likert scores or an “overall quality” bundle, return to the
26
27
  First win's conditional ground-truth annotation stage and rewrite the rubric as one binary question
27
28
  per failure mode before building a judge.
29
+ Pairwise labels (`a`, `b`, or `tie`) and other fixed-category labels are
30
+ already decision-shaped: keep them and follow “Multi-class and pairwise
31
+ judges” below.
28
32
 
29
33
  - A **judge** is the evaluator, often an LLM instruction set plus a runner.
30
34
  - A **scorer** is the metric contract stored in Orizu. It names the score,
@@ -42,6 +46,8 @@ positive rate)** is the fraction of human-labeled failures the judge catches.
42
46
  correctly leaves unflagged.
43
47
 
44
48
  Measure kappa, TPR, and TNR separately for every failure mode on judge-test rows.
49
+ For a pairwise or multi-class judge, TPR and TNR are undefined; use kappa plus
50
+ each label's recall and precision (see “Multi-class and pairwise judges”).
45
51
  Always show the confusion matrix, sample count, class balance, and scenario-class
46
52
  breakdown beside them. A judge that flags everything can have perfect TPR and
47
53
  useless TNR; a judge that passes everything has the inverse failure. If a
@@ -128,7 +134,8 @@ def has_case_reference(input: dict, output: dict) -> bool:
128
134
  ```
129
135
 
130
136
  For a nuanced failure mode, make the LLM judge return one binary decision and a
131
- short reason. Its instructions should contain:
137
+ short reason (for pairwise or multi-class labels, one label from the fixed set;
138
+ see “Multi-class and pairwise judges”). Its instructions should contain:
132
139
 
133
140
  1. the binary question;
134
141
  2. specific pass and fail criteria;
@@ -154,6 +161,114 @@ Frame every scorer output so higher means better; for the binary row score,
154
161
  declaring `higher_is_better: true`, because that gives optimization and
155
162
  reflection mixed directional signals.
156
163
 
164
+ ### Multi-class and pairwise judges
165
+
166
+ When reviewers compared two outputs and picked `a`, `b`, or `tie`, or chose one
167
+ of a fixed set of categories, the judge predicts that same label. Its strict
168
+ JSON result is `{"label": "a", "reason": "…"}` with `label` exactly one of the
169
+ allowed values. Orizu does not check judge labels: the runner passes
170
+ `model_response` through unchanged and `cohens_kappa` accepts any label as a new
171
+ class. Your judge runner must validate the label and turn anything else into a
172
+ row error.
173
+
174
+ For pairwise judges, the same runner controls position bias; GEPA changes the
175
+ instructions, not the runner, so no other component can. The default is to
176
+ pick a random-looking but fixed order for each row, map the verdict back to the
177
+ dataset's own `a` and `b` before writing `model_response`, and record the order
178
+ shown, so the scorer compares labels directly. Derive the order from a hash of
179
+ the row's own content, not from `random.random()`: GEPA calls the runner
180
+ separately for the seed and for every candidate, so a fresh draw each call
181
+ would show the same row in different orders to different candidates, and
182
+ position bias would then decide part of which candidate wins. The candidate
183
+ runner receives the dataset row's fields, not Orizu's row id, and Python's
184
+ built-in `hash()` changes between processes, so use a stable digest. Showing each pair in both orders instead doubles
185
+ judge calls against the run budget; if you do it, fold the two verdicts into
186
+ the single per-row output: the same verdict in both orders keeps that verdict,
187
+ and different verdicts become `tie`. The core of the default runner, which reads the candidate-runner contract
188
+ (`payload["row"]` is the flat dataset row):
189
+
190
+ ```python
191
+ import hashlib, json, os
192
+
193
+ ALLOWED = {"a", "b", "tie"}
194
+ payload = json.load(open(os.environ["ORIZU_RUNNER_INPUT_PATH"]))
195
+ row = payload["row"] # flat dataset row: {"reply_a": ..., "reply_b": ..., ...}
196
+ digest = hashlib.sha256(json.dumps(row, sort_keys=True).encode()).digest()
197
+ swapped = digest[0] % 2 == 1 # same order for this row in every call
198
+ first, second = (row["reply_b"], row["reply_a"]) if swapped else (row["reply_a"], row["reply_b"])
199
+ verdict = call_judge(instructions, first, second) # parsed JSON, any value
200
+ if not isinstance(verdict, dict) or verdict.get("label") not in ALLOWED:
201
+ output = {"model_response": None,
202
+ "error": f"Judge label must be one of {sorted(ALLOWED)}; got {verdict!r}"}
203
+ else:
204
+ if swapped and verdict["label"] != "tie":
205
+ verdict["label"] = "b" if verdict["label"] == "a" else "a"
206
+ verdict["shown_order"] = "b_first" if swapped else "a_first"
207
+ output = {"model_response": verdict, "error": None}
208
+ json.dump(output, open(os.environ["ORIZU_RUNNER_OUTPUT_PATH"], "w"))
209
+ ```
210
+
211
+ The GEPA alignment row scorer compares the candidate judge's label with the
212
+ human label and explains every miss, because reflection learns from that
213
+ feedback. A match scores 1 and anything else scores 0. Under the default GEPA
214
+ scorer contract, the scorer's `payload["row"]` is not the flat dataset row but
215
+ a wrapper (`prompt-control-plane.md`, “GEPA scorer-runner contract”): the
216
+ dataset row is under `source_row`, and `candidate_output` is exactly what the
217
+ judge runner wrote as `model_response`, passed through unchanged. With the
218
+ runner above that is the validated verdict object, or `None` when the runner
219
+ reported an error:
220
+
221
+ ```python
222
+ import json, os
223
+
224
+ ALLOWED = {"a", "b", "tie"}
225
+ payload = json.load(open(os.environ["ORIZU_RUNNER_INPUT_PATH"]))
226
+ wrapper = payload["row"] # GEPA wrapper, not the flat dataset row
227
+ row = wrapper["source_row"] # the same flat dataset row the judge runner saw
228
+ human = row["preferred"] # the dataset's human-label field
229
+ verdict = wrapper.get("candidate_output")
230
+ judge = verdict.get("label") if isinstance(verdict, dict) else None
231
+ error = None
232
+ if human not in ALLOWED:
233
+ score = 0.0
234
+ feedback = "Row not scored: the human label is outside the allowed set (a data problem, not a judge problem)."
235
+ error = f"Human label {human!r} is not one of {sorted(ALLOWED)}; fix the label field or ALLOWED"
236
+ elif wrapper.get("candidate_error"):
237
+ score = 0.0
238
+ feedback = "Row not scored: the judge runner reported an error (provider failure or invalid label), so there is no verdict to compare."
239
+ error = f"Judge produced no usable output: {wrapper['candidate_error']}"
240
+ elif judge not in ALLOWED:
241
+ score, feedback = 0.0, f"Judge output must be JSON with label one of {sorted(ALLOWED)}; got {verdict!r}"
242
+ elif judge == human:
243
+ score, feedback = 1.0, f"Matched reviewers ({human})."
244
+ else:
245
+ score, feedback = 0.0, f"Reviewers chose {human}; judge chose {judge}. Judge reason: {verdict.get('reason', '')}"
246
+ json.dump({"score": score, "feedback": feedback, "error": error},
247
+ open(os.environ["ORIZU_RUNNER_OUTPUT_PATH"], "w"))
248
+ ```
249
+
250
+ Every row still needs a numeric `score`. A row the scorer cannot judge (a
251
+ human label outside the allowed set, or a judge row the runner reported as an error) carries its
252
+ reason in `error`, and the run marks the row as errored. Its `feedback` must
253
+ still be a short, non-empty, factual note: reflection refuses a row with empty
254
+ feedback and stops the run. Keep that note about the missing verdict, not about
255
+ the judge's instructions, so reflection does not try to fix a provider failure
256
+ by rewriting the judge. A seed on which every row errors is refused
257
+ at launch.
258
+
259
+ Register it with `higher_is_better: true` and run GEPA exactly as in
260
+ “Optimize the judge instructions with GEPA” below. Compared with the binary
261
+ case, the label field and the allowed labels differ, and so does the kappa set
262
+ scorer: reuse the `cohens_kappa` builtin from `prompt-control-plane.md` with
263
+ `predicted_label` mapped to the judge's `label`, drop `positive_class`, and
264
+ replace the binary `diagnostics_schema` keys (`confusion_matrix`,
265
+ `flag_recall`, `ok_recall`, and the rest) with `labels` and `per_class`.
266
+ Without a positive class the scorer computes multi-class kappa and accuracy
267
+ over every label it sees and reports per-label precision and recall under
268
+ `per_class` only; it emits no `confusion_matrix` or `<label>_precision` keys.
269
+ Agree the trust bar on that kappa plus the per-label recall in `per_class`,
270
+ especially for `tie`, which is usually the rarest label.
271
+
157
272
  ## Split, iterate, and validate
158
273
 
159
274
  Build the labeled judge pool only from the application's train and validation
@@ -167,6 +282,9 @@ judge-test split is yours to define:
167
282
  | Judge dev / validation (`validation`) | ~40% | Iterate and compare candidates |
168
283
  | Judge test (`judge-test`) | ~40% | One alignment validation after the judge candidate is frozen |
169
284
 
285
+ A small judge-train partition is normal for GEPA: tens of rows are common. Do
286
+ not shrink validation or judge-test to enlarge it.
287
+
170
288
  Author `judge-split.json` with explicit row membership for all three judge
171
289
  partitions. Every `row_ids` entry must come from the application's train or
172
290
  validation partition; never include an application final-held-out row. Require
@@ -319,7 +437,9 @@ agreed judge trust bar, the scorer versions are registered, `runners exec`, `sco
319
437
  and the measured `scorers exec` run have all succeeded, and the human curator has
320
438
  accepted that score run. Gate decisions only on accepted evidence. A locally
321
439
  computed alignment report with no accepted Orizu score run does not clear the
322
- trust bar.
440
+ trust bar. Judge GEPA (“Optimize the judge instructions with GEPA” below) runs
441
+ inside this stage, before the bar is cleared; the accepted judge-test evidence
442
+ for the selected judge is what completes it.
323
443
 
324
444
  Re-run this evidence path after labels, judge instructions, runners, or scorer
325
445
  definitions change. Builtin set scorers, their dependency mapping, and aggregate
@@ -329,14 +449,21 @@ submission behavior are specified in `prompt-control-plane.md`.
329
449
 
330
450
  Judge building is itself an optimization problem. Optimize the LLM judge's own
331
451
  instructions against human labels; do not optimize it against its own previous
332
- decisions.
452
+ decisions. This run happens before the judge clears its trust bar, so the
453
+ “validated judges only” rule for application optimization does not gate it:
454
+ here the gate is the human labels plus the alignment scorer that compares the
455
+ candidate judge with them. Judge-test still decides, once, after the run,
456
+ whether the selected judge clears its bar. For an LLM judge built from human labels, this
457
+ run is the default way to reach the trust bar, not an optional extra; the data
458
+ guidance in “Run it” in `optimization-with-gepa.md` applies here too.
333
459
 
334
460
  Manage a composite judge as one `orizu instructions` instruction set. Components
335
461
  can separate the rubric, boundary examples, output contract, and failure-mode
336
462
  rules, while the complete component map remains the candidate. The candidate
337
463
  runner executes those candidate judge instructions on human-labeled train and
338
- validation rows. The row-mode alignment scorer compares the binary candidate
339
- decision with the human label and returns per-row feedback for reflection.
464
+ validation rows. The row-mode alignment scorer compares the candidate's
465
+ decision (binary, pairwise, or multi-class) with the human label and returns
466
+ per-row feedback for reflection.
340
467
 
341
468
  Materialize those components in `orizu.instruction-set.json` before GEPA. Create
342
469
  the stable instruction set once, or push a revised seed for a later optimization
@@ -476,11 +603,14 @@ in `prompt-control-plane.md` before choosing either contract.
476
603
 
477
604
  ## Exit criterion checklist
478
605
 
479
- - [ ] Each failure mode is binary and has adequate positive and negative rows.
606
+ - [ ] Each judge decision is either binary with adequate positive and negative
607
+ rows, or pairwise or multi-class with adequate rows for every label.
480
608
  - [ ] Human–human kappa is computed from multiply labeled rows; low agreement
481
609
  routes back to rubric and label repair.
482
610
  - [ ] Kappa, TPR, TNR, confusion matrix, sample size, class balance, and
483
- scenario-class results are reported per failure mode.
611
+ scenario-class results are reported per failure mode; for a pairwise or
612
+ multi-class judge, kappa, accuracy, and `per_class` precision and recall
613
+ stand in for TPR, TNR, and the confusion matrix.
484
614
  - [ ] The agent taught the decision-class tradeoffs, the user chose each
485
615
  judge trust bar, and the agreed value is recorded in the output.
486
616
  - [ ] Development rows were used for iteration and judge-test was used only
@@ -114,7 +114,7 @@ Behavior:
114
114
  - `skills path --json` returns where this skill's source lives (`root`, `skillMd`, `source`, `cliVersion`, `skillHash`) so you can read it without installing, and verify an installed copy matches the CLI.
115
115
  - `skills status` reports each install target as missing/current/stale/broken-link; `skills update` refreshes stale copies and repairs broken symlinks.
116
116
  - `install-skill` selects targets by agent name; `--scope global` (default) installs user-level (`~/.agents/skills`, `~/.claude/skills`), `--scope project` installs into the repo. Project-scope installs always copy; global installs symlink when the CLI path is stable. Advanced target IDs remain available via `--target`.
117
- - `orizu setup` is the guided onboarding flow (login → choose/create team → choose/create project → workspace contract → coding-agent skill → optional launch). Non-interactive setup requires authentication plus explicit team and project choose/create flags; run `orizu login --headless` first when needed. After interactive skill-installation consent, a destination picker starts with `Universal (.agents — Codex and others) — ~/.agents/skills/orizu` and shows each detected native target with its `~/.../orizu` destination on the same line before any write. Config-root detection requires a directory rather than a same-named regular file. Every visible destination starts selected, but every destination can be deselected. Universal and OpenCode are independent choices, so either can be installed without the other. Confirming an empty selection skips skill installation successfully. Non-interactive `--agent` semantics are unchanged: repeat `--agent` to opt into installs, each selected agent always includes Universal plus its native target only where needed, duplicate destinations are removed, and no agent flags writes no skills. Non-interactive setup never launches. Interactive setup can hand off to installed Claude, Codex, Pi, OpenCode, and Cursor commands. Available commands appear in that order, with the first available command selected by default. Other is always last and is the only option when no supported command is installed. Enter launches the highlighted agent; Esc skips agent launch and completes setup. A selected agent receives one positional prompt naming the selected `team/project` and `https://orizu.ai/llms.txt`, without shell interpolation. Selecting Other prints the complete prompt for copying without launching anything. `--handoff` explicitly prints the prompt; `--launch claude|codex|pi|opencode|cursor` explicitly selects an installed agent, still requires an interactive terminal, and asks for confirmation unless `--yes` is supplied. OpenCode launches as `opencode run <prompt>`. Cursor uses the `agent` executable, not the Cursor editor `cursor` command. The official [Cursor CLI overview](https://cursor.com/docs/cli/overview) and [parameter reference](https://cursor.com/docs/cli/reference/parameters), retrieved 2026-08-30, document direct `agent "prompt"` and `agent -p "prompt"` initial prompts; setup uses the direct positional form without a shell. Authenticated workspace setup still materializes every project in the team. The contract uses root `AGENTS.md`, `CLAUDE.md`, `Memory.md`, `orizu.team.json`, project manifests, primitive directories, and gitignore policy. `.orizu/` and `.logs/` are ignored cache/generated state, not the durable contract.
117
+ - `orizu setup` is the guided onboarding flow (login → choose/create team → choose/create project → workspace contract → coding-agent skill → optional launch). Non-interactive setup requires authentication plus explicit team and project choose/create flags; run `orizu login --headless` first when needed. After interactive skill-installation consent, a destination picker starts with `Universal (.agents — Codex and others) — ~/.agents/skills/orizu` and shows each detected native target with its `~/.../orizu` destination on the same line before any write. Config-root detection requires a directory rather than a same-named regular file. Every visible destination starts selected, but every destination can be deselected. Universal and OpenCode are independent choices, so either can be installed without the other. Confirming an empty selection skips skill installation successfully. Non-interactive `--agent` semantics are unchanged: repeat `--agent` to opt into installs, each selected agent always includes Universal plus its native target only where needed, duplicate destinations are removed, and no agent flags writes no skills. Non-interactive setup never launches. Interactive setup can hand off to installed Claude, Codex, Pi, OpenCode, and Cursor commands. Available commands appear in that order, with the first available command selected by default. Other is always last and is the only option when no supported command is installed. Enter launches the highlighted agent; Esc skips agent launch and completes setup. A selected agent receives one positional prompt naming the selected `team/project` and `https://orizu.ai/llms.txt`, without shell interpolation. Selecting Other prints the complete prompt for copying without launching anything. `--handoff` explicitly prints the prompt; `--launch claude|codex|pi|opencode|cursor` explicitly selects an installed agent, still requires an interactive terminal, and asks for confirmation unless `--yes` is supplied. OpenCode launches as `opencode run <prompt>`. Cursor uses the `agent` executable, not the Cursor editor `cursor` command. The official [Cursor CLI overview](https://cursor.com/docs/cli/overview) and [parameter reference](https://cursor.com/docs/cli/reference/parameters), retrieved 2026-08-30, document direct `agent "prompt"` and `agent -p "prompt"` initial prompts; setup uses the direct positional form without a shell. Authenticated workspace setup materializes only the project named by `--project` or created by `--create-project`, with no checklist, interactive or not. When the project is picked interactively instead, a checklist of the team's projects follows, all checked (Esc keeps them all), and setup materializes the checked ones plus the active project. `--fix` restores every team project. The contract uses root `AGENTS.md`, `CLAUDE.md`, `Memory.md`, `orizu.team.json`, project manifests, primitive directories, and gitignore policy. `.orizu/` and `.logs/` are ignored cache/generated state, not the durable contract.
118
118
 
119
119
  ### Teams
120
120
 
@@ -523,7 +523,7 @@ exist.
523
523
  Optimization trace commands:
524
524
 
525
525
  ```bash
526
- orizu optimizations run-gepa ... [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] [--candidate-selection-strategy pareto|current_best|epsilon_greedy] [--epsilon N] [--objective <text>] [--candidate-proposer skilled-proposer] [--candidate-proposer-config @file] [--proposal-max-calls N] [--proposal-max-tokens N] [--python <command>] [--num-threads auto|N] [--reflection-retry-attempts 3] [--reflection-http-timeout-seconds 180] [--log-dir logs] [--no-local-log]
526
+ orizu optimizations run-gepa ... (--candidate-version-id <id> | --instruction-set <set[/profile[@vN]]> [--model-config <identity>] [--profile-version <N>]) [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] [--candidate-selection-strategy pareto|current_best|epsilon_greedy] [--epsilon N] [--objective <text>] [--candidate-proposer skilled-proposer] [--candidate-proposer-config @file] [--proposal-max-calls N] [--proposal-max-tokens N] [--python <command>] [--num-threads auto|N] [--reflection-retry-attempts 3] [--reflection-http-timeout-seconds 180] [--log-dir logs] [--no-local-log]
527
527
  orizu optimizations run-gepa --hosted ... --budget auto|light|medium|heavy [--launch-intent-id <uuid>]
528
528
  orizu optimizations export <optimization-run-id> --out ./optimization.json
529
529
  orizu optimizations export <optimization-run-id> --json
@@ -353,8 +353,7 @@ reflection model is refused at launch without it
353
353
  `packages/orizu-gepa/src/orizu_gepa_connector/runtime.py:193-196`). Drop it only
354
354
  if you also pass an `openai/...` `--reflection-model`.
355
355
 
356
- `--instruction-set` and `--model-config` select exactly one Profile. The CLI
357
- uses only that Profile's Production Version. A missing Profile refuses with `instruction_set_profile_not_found`, and an unpromoted Profile refuses with `instruction_set_profile_not_promoted`, instead of using Default; the CLI also refuses the
356
+ `--instruction-set` and `--model-config` (or one `set/profile` specifier) select exactly one Profile. A plain `set/profile` starts the run from that Profile's Production Version. To start from any other version, promoted or not, name it with `set/profile@vN` or `--profile-version <N>`; its components must match the set's current shape exactly, or launch refuses and names the newest matching version. Production must match the shape too. A missing Profile refuses with `instruction_set_profile_not_found`, and an unpromoted Profile with no version named refuses with `instruction_set_profile_not_promoted`, instead of using Default; the CLI also refuses the
358
357
  legacy `--candidate-version-id` selector when the instruction-set selectors are present. `round-robin` updates one component per
359
358
  round; use `--component-selector all` only when every component should be
360
359
  reflected on each round.
@@ -68,7 +68,7 @@ Keep the three data roles separate:
68
68
  - Validation data ranks every candidate that received a full validation evaluation. A candidate comparison must use this data, because the optimizer is allowed to inspect validation results while choosing.
69
69
  - Use Final-held-out only for the untouched final check after selecting one candidate, and compare only the seed with that selected candidate. Do not rank candidates on Final-held-out or use its results to choose a different candidate.
70
70
 
71
- For a multi-component instruction-set run, the **seed** is the customer-authored profile version the run started from, and each candidate is a complete proposed profile version. A plain prompt and a set-of-one instruction set are both recorded with a `prompt_version_id` and no `instruction_set_profile_version_id`; that storage fact makes legacy promotion available but does not make it a valid shipping path for the set-of-one run. Report each scenario class from the ratified improvement plan separately, such as “refund requests with missing order details”; never replace scenario-class results with pass/fail buckets.
71
+ For a multi-component instruction-set run, the **seed** is the profile version the run started from (production unless the run named another version, promoted or not), and each candidate is a complete proposed profile version. A plain prompt and a set-of-one instruction set are both recorded with a `prompt_version_id` and no `instruction_set_profile_version_id`; that storage fact makes legacy promotion available but does not make it a valid shipping path for the set-of-one run. Report each scenario class from the ratified improvement plan separately, such as “refund requests with missing order details”; never replace scenario-class results with pass/fail buckets.
72
72
 
73
73
  Prefer the local log directory printed by `run-gepa`. Core files (conditional files are listed below):
74
74
 
@@ -2,9 +2,26 @@
2
2
 
3
3
  How to optimize an instruction-set profile against validated judges and scorers. Prepare the complete instruction-set manifest, follow the [Authority map](authority-map.md) for its mutation, and then run the bundled `orizu optimizations run-gepa` flow. Use this reference for GEPA mechanics, custom optimizer implementations, and optional DSPy context for customers already using DSPy.
4
4
 
5
+ ## Run it
6
+
7
+ Once a validated judge or scorer exists, launch `orizu optimizations run-gepa`. That run is the default end of First win, not an optional extra.
8
+
9
+ How much data is enough: tens to a few hundred labelled rows is normal; under 100 is common. GEPA learns from per-row scorer feedback, not from gradients, so data size alone is not a reason to skip the run. Neither is a seed that already looks close to the human ceiling, nor a hand-written "GEPA-style" rewrite. Run it and let the validation split choose. When a non-seed candidate is selected, the untouched Final-held-out comparison in Promote decides whether it helped. When GEPA keeps the seed, record the seed-selected outcome; when search produces no valid candidate, record the no-valid-candidate outcome. Both are valid results, not wasted runs, and Promote reports each through its own branch. If you still believe a run is not worth it, record the reason and get the human's approval before skipping, as `flows/first-win.md` describes.
10
+
11
+ Shortest path to a first run (each item matches "Orizu-tracked optimization" step 1–6 below):
12
+
13
+ 1. Create or push the instruction set from its manifest, and keep the profile version it reports. A new set's first version (v1) has no Production pointer, so name it at launch with `@v1` or `--profile-version 1`; never promote it just to make the run start, because moving Production is a human decision.
14
+ 2. Push the candidate runner (and, for an LLM evaluator, the judge rubric).
15
+ 3. Register a row scorer whose feedback explains each score.
16
+ 4. Snapshot a dataset version, then create one split set with train, validation, and a reserved Final-held-out partition. Pass only train and validation to `run-gepa`, and never pass Final-held-out as `--train-split` or `--val-split`: the CLI does not enforce that boundary.
17
+ 5. Push the optimizer with `orizu optimizers push`.
18
+ 6. Launch `orizu optimizations run-gepa` on the train and validation partitions with `--budget light`. Pass `--reflection-max-tokens <n>`: the default reflection model, `anthropic/claude-opus-4-7`, uses the Anthropic Messages protocol, and launch refuses without a cap. Scale the budget up only if the run is still finding better candidates.
19
+
5
20
  When First win loads this reference, stop after the optimization run records a selected candidate or no-valid-candidate outcome. Do not write the decision report or run a promotion command until `flows/promote.md` is active. Steps 8–9 below describe Promote-owned continuation mechanics, not First win work.
6
21
 
7
- Select the Profile explicitly with `--instruction-set <slug-or-exact-name> --model-config <identity>`. Those two selectors replace the legacy `--candidate-version-id` path and cannot be combined with it. The connector optimizes that Profile's complete Production component map. If the Profile is missing, launch refuses with `instruction_set_profile_not_found`; if it is unpromoted, launch refuses with `instruction_set_profile_not_promoted`. It never substitutes the Default Profile. `--component-selector round-robin` is the default and updates one component per round; `--component-selector all` updates every component per round. The runner receives a multi-component candidate as a component map, while a one-component set keeps the existing single-body runner contract. Git-pinned components and malformed component maps are refused before a run starts. Automatic promotion is refused for multi-component sets; write the report, obtain the human promotion decision, and follow the Authority map. Manifest create/push is the pre-run seed path, not a second accepted-candidate materialization; after a run, use the human one-shot or equivalent idempotent two-stage path in step 9.
22
+ ## Selecting the profile and seed
23
+
24
+ Select the Profile explicitly with `--instruction-set <slug-or-exact-name> --model-config <identity>`, or with one specifier such as `--instruction-set planner/openai/gpt-5.4`. These selectors replace the legacy `--candidate-version-id` path and cannot be combined with it. The run starts from its *seed*: that Profile's Production component map unless you name another version with `planner/openai/gpt-5.4@v3` or `--profile-version 3` (a version number only; `v3` also works). Any version of the Profile can be the seed, promoted or not, and the seed brings its own components and model settings. When the specifier and the flags both name the profile or the version, they must agree or launch refuses with `instruction_set_seed_conflict`. A named version that does not exist refuses with `instruction_set_profile_version_not_found` and names the newest version. The seed's components must match the set's current shape exactly, with none missing and none the set has since removed. Launch refuses a named version that does not with `instruction_set_profile_version_incomplete` and names the newest matching version, and refuses a Production that does not (locally with `instruction_set_profile_production_incomplete`; a hosted launch names the production version). If the Profile is missing or its model config is archived, launch refuses with `instruction_set_profile_not_found`; if no version is named and it is unpromoted, launch refuses with `instruction_set_profile_not_promoted`. It never substitutes the Default Profile. When you promote a candidate whose seed is older than the Profile's current Production, `optimizations promote` warns that the changes made since the seed are not in the candidate; tell the human before they decide. `--component-selector round-robin` is the default and updates one component per round; `--component-selector all` updates every component per round. The runner receives a multi-component candidate as a component map, while a one-component set keeps the existing single-body runner contract. Git-pinned components and malformed component maps are refused before a run starts. Automatic promotion is refused for multi-component sets; write the report, obtain the human promotion decision, and follow the Authority map. Manifest create/push is how a profile's first version is created before a run, not a second accepted-candidate materialization; after a run, use the human one-shot or equivalent idempotent two-stage path in step 9.
8
25
 
9
26
  ## Inputs
10
27
 
@@ -13,7 +30,7 @@ You should arrive here with:
13
30
  - A **dataset** of inputs to optimize against: usually the same exported labels, plus any harder cases you've added since.
14
31
  - An **instruction set** whose selected profile contains the starting component values for the LLM application you want to improve.
15
32
 
16
- If you don't have a validated judge, stop. Optimizing against an unvalidated judge means you'll hill-climb on a noisy or biased signal: Goodhart's law in action.
33
+ Validate the judge first (see `building-judges.md`): optimizing against an unvalidated judge hill-climbs a noisy or biased signal. Once it clears its trust bar, run GEPA. The validated judge, not the size of the dataset, is the gate. When the thing being optimized is the judge itself, there is no earlier judge to validate: the gate is the human labels plus the alignment scorer that compares the judge with them (see “Optimize the judge instructions with GEPA” in `building-judges.md`).
17
34
 
18
35
  ## Why GEPA-style optimization
19
36
 
@@ -342,9 +359,10 @@ Each pass through the loop reveals the next layer.
342
359
  ## Common pitfalls
343
360
 
344
361
  - Optimizing against an unvalidated judge. You'll improve the metric and degrade the system. Always validate first.
345
- - No Final-held-out comparison. "It's better, look at the metric" without Final-held-out is meaningless: GEPA will overfit if you let it.
362
+ - Skipping the Final-held-out comparison. "It's better, look at the metric" without Final-held-out is meaningless. The validation split selects the candidate and Final-held-out decides whether it helped; with both in place, a small dataset is not an overfitting reason to skip the run.
363
+ - Skipping the run, or replacing it with a hand-written "GEPA-style" rewrite. Neither produces a recorded run, candidates, or the evidence Promote needs.
346
364
  - Hiding regressions in the average. Track per-failure-mode metrics alongside the combined score.
347
- - Over-budgeting GEPA. Heavy budgets give diminishing returns and burn LM spend. Start with `auto="light"`, scale up only if needed.
365
+ - Over-budgeting GEPA. Heavy budgets give diminishing returns and burn LM spend. Start with `auto="light"` (`--budget light` for `run-gepa`) and scale up only if needed; a light budget on a small dataset is a normal first run.
348
366
  - Ignoring temperature. Run optimization with the same LM config (model, temperature) you use in production. Optimizing against gpt-4o at temp=0 doesn't transfer to gpt-4o-mini at temp=0.7.
349
367
  - Recreating Orizu logging by hand for instruction sets. Use `orizu optimizations run-gepa` unless the optimizer is genuinely custom.
350
368
 
@@ -13,7 +13,7 @@ Onboard → First win ───────────────→ Promote
13
13
  └─ after Promote: Recurse, Triage, or Expand by user interest
14
14
  ```
15
15
 
16
- First win builds the evidence and ends at a completed optimization run. Promote owns reporting, validation, and the human promotion decision. Recurse turns newly deployed traces into a later same-surface cycle; Triage starts from an incident and closes its eval gap before a fix; Expand chooses and plans another surface. Onboard owns pre-data inventory and plan ratification.
16
+ First win builds the evidence and ends at a completed optimization run, or at a skip the human approved and recorded. Promote owns reporting, validation, and the human promotion decision. Recurse turns newly deployed traces into a later same-surface cycle; Triage starts from an incident and closes its eval gap before a fix; Expand chooses and plans another surface. Onboard owns pre-data inventory and plan ratification.
17
17
 
18
18
  The CLI covers this operational loop. Start instruction sets with `orizu instructions`: one instruction set per agent or LLM experience, one profile per model config, and components owned by that profile. Human annotation happens through apps/tasks only for required ground truth, while judge/scorer logic and optimization execution run locally and report back through the instruction control plane. The active flow file owns ordering and its exit criterion; this primer explains why the stages exist rather than replacing that procedure.
19
19
 
@@ -205,7 +205,7 @@ Done locally, reported to Orizu:
205
205
  2. Register validated scorers.
206
206
  3. Run the bundled Orizu GEPA-style text optimizer, or a custom optimizer against the scorer set.
207
207
  4. Stream optimization events to Orizu and complete the run with a selected candidate or a recorded no-valid-candidate outcome.
208
- 5. Stop First win at that completed run. Route to Promote for the report, supported Final-held-out comparison, validation evidence, and human ship, gather-more-evidence, or do-not-promote decision. After Promote, use Recurse for a human-agreed cadence that turns traces from the currently deployed version into a new immutable dataset version.
208
+ 5. Stop First win at that completed run, or at a skip the human approved and recorded. Route to Promote for the report, supported Final-held-out comparison, validation evidence, and human ship, gather-more-evidence, or do-not-promote decision. After Promote, use Recurse for a human-agreed cadence that turns traces from the currently deployed version into a new immutable dataset version.
209
209
 
210
210
  Control-plane commands: `prompt-control-plane.md`. Detailed walkthrough: GEPA mechanics, optional DSPy context for customers already using it, and before/after comparison: `optimization-with-gepa.md`.
211
211
 
@@ -811,7 +811,12 @@ orizu --local optimizations run-gepa \
811
811
  Useful GEPA flags:
812
812
 
813
813
  - `--instruction-set <slug-or-exact-name> --model-config <identity>` selects a
814
- resolving instruction-set profile. `--component-selector round-robin|all`
814
+ resolving instruction-set profile. A specifier `set/profile` or
815
+ `set/profile@vN` names the profile without `--model-config`;
816
+ `@vN` or `--profile-version <N>` starts the run from that version (the
817
+ *seed*) instead of production. The seed must match the set's current shape
818
+ exactly (no missing or removed components); launch refuses one that does not
819
+ and names the newest version that matches. `--component-selector round-robin|all`
815
820
  controls which components GEPA proposes changing; `all` lets it optimize the
816
821
  complete component map. `--candidate-version-id` remains a compatibility
817
822
  input for a single legacy prompt version and is mutually exclusive with the
@@ -9,6 +9,7 @@
9
9
  * 1. App.tsx has a single named default export (function or class).
10
10
  * 2. Default export's first parameter destructures inputData, onComplete,
11
11
  * initialValues (and not the deprecated data, onSubmit).
12
+ * The default export must not be async.
12
13
  * 3. Imports resolve against the platform-provided component registry.
13
14
  * 4. input.json and output.json use only the supported JSON Schema subset:
14
15
  * type, required, properties, items, enum.
@@ -121,6 +122,48 @@ function warn(message) {
121
122
  issues.push({ level: "warn", message });
122
123
  }
123
124
 
125
+ /**
126
+ * Removes line and block comments, walking left to right so a `//` or `/*`
127
+ * inside a quoted string or template literal is left alone. A block comment
128
+ * keeps its newlines, so line numbers in later messages stay true.
129
+ * Regex literals are not tracked; a quote inside one can hide a comment.
130
+ */
131
+ function stripComments(code) {
132
+ let out = "";
133
+ let quote = "";
134
+ let i = 0;
135
+ while (i < code.length) {
136
+ const char = code[i];
137
+ const next = code[i + 1];
138
+ if (quote) {
139
+ out += char;
140
+ if (char === "\\" && next !== undefined) {
141
+ out += next;
142
+ i += 2;
143
+ continue;
144
+ }
145
+ // Single and double quoted strings end at the line's end; templates may span lines.
146
+ if (char === quote || (char === "\n" && quote !== "`")) quote = "";
147
+ i++;
148
+ } else if (char === '"' || char === "'" || char === "`") {
149
+ quote = char;
150
+ out += char;
151
+ i++;
152
+ } else if (char === "/" && next === "/" && code[i - 1] !== ":") {
153
+ while (i < code.length && code[i] !== "\n") i++;
154
+ } else if (char === "/" && next === "*") {
155
+ const close = code.indexOf("*/", i + 2);
156
+ const end = close === -1 ? code.length : close + 2;
157
+ out += code.slice(i, end).replace(/[^\n]/g, " ");
158
+ i = end;
159
+ } else {
160
+ out += char;
161
+ i++;
162
+ }
163
+ }
164
+ return out;
165
+ }
166
+
124
167
  function checkAppFile(path) {
125
168
  if (!existsSync(path)) {
126
169
  err(`App file not found: ${path}`);
@@ -129,13 +172,19 @@ function checkAppFile(path) {
129
172
  const src = readFileSync(path, "utf8");
130
173
 
131
174
  // Strip line comments and block comments to avoid false matches.
132
- const stripped = src
133
- .replace(/\/\*[\s\S]*?\*\//g, "")
134
- .replace(/(^|[^:])\/\/[^\n]*/g, "$1");
175
+ const stripped = stripComments(src);
176
+
177
+ // App code renders as a client component; React 19 cannot render an async one.
178
+ if (/export\s+default\s+async\s+function\b/.test(stripped)) {
179
+ err(
180
+ "App components cannot be async; return the element synchronously and load data in an effect."
181
+ );
182
+ return;
183
+ }
135
184
 
136
185
  // 1. Find a named default export.
137
186
  const fnMatch = stripped.match(
138
- /export\s+default\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)/
187
+ /export\s+default\s+function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)/
139
188
  );
140
189
  const classMatch = stripped.match(
141
190
  /export\s+default\s+class\s+([A-Za-z_$][\w$]*)/