orizu 0.5.21 → 0.5.23

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 (87) hide show
  1. package/README.md +46 -15
  2. package/dist/cleanup.d.ts +6 -0
  3. package/dist/cleanup.d.ts.map +1 -0
  4. package/dist/cleanup.js +27 -0
  5. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  6. package/dist/gepa-engine-dispatch.js +20 -1
  7. package/dist/gepa-python-paths.d.ts.map +1 -1
  8. package/dist/gepa-python-paths.js +25 -6
  9. package/dist/help.d.ts.map +1 -1
  10. package/dist/help.js +37 -15
  11. package/dist/hosted-commands.d.ts +13 -0
  12. package/dist/hosted-commands.d.ts.map +1 -0
  13. package/dist/hosted-commands.js +10 -0
  14. package/dist/hosted-gepa-cli.d.ts +11 -0
  15. package/dist/hosted-gepa-cli.d.ts.map +1 -0
  16. package/dist/hosted-gepa-cli.js +110 -0
  17. package/dist/hosted-gepa-numeric-values.d.ts +3 -0
  18. package/dist/hosted-gepa-numeric-values.d.ts.map +1 -0
  19. package/dist/hosted-gepa-numeric-values.js +49 -0
  20. package/dist/hosted-provider-settings.d.ts +3 -0
  21. package/dist/hosted-provider-settings.d.ts.map +1 -0
  22. package/dist/hosted-provider-settings.js +69 -0
  23. package/dist/hosted-session-cli.d.ts.map +1 -1
  24. package/dist/hosted-session-cli.js +6 -1
  25. package/dist/http.d.ts +1 -0
  26. package/dist/http.d.ts.map +1 -1
  27. package/dist/http.js +7 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +72 -74
  30. package/dist/instruction-set-loader.d.ts.map +1 -1
  31. package/dist/instruction-set-loader.js +57 -5
  32. package/dist/instruction-sets-cli.d.ts +5 -2
  33. package/dist/instruction-sets-cli.d.ts.map +1 -1
  34. package/dist/instruction-sets-cli.js +134 -24
  35. package/dist/optimizations-promote-cli.d.ts +3 -0
  36. package/dist/optimizations-promote-cli.d.ts.map +1 -1
  37. package/dist/optimizations-promote-cli.js +75 -7
  38. package/dist/runner-dir-verify.d.ts.map +1 -1
  39. package/dist/runner-dir-verify.js +4 -7
  40. package/dist/skilled-proposer-bake-report.d.ts +2 -0
  41. package/dist/skilled-proposer-bake-report.d.ts.map +1 -0
  42. package/dist/skilled-proposer-bake-report.js +1 -0
  43. package/dist/skilled-proposer-config.d.ts +2 -0
  44. package/dist/skilled-proposer-config.d.ts.map +1 -0
  45. package/dist/skilled-proposer-config.js +212 -0
  46. package/dist/skilled-proposer-launch.d.ts +26 -0
  47. package/dist/skilled-proposer-launch.d.ts.map +1 -1
  48. package/dist/skilled-proposer-launch.js +103 -6
  49. package/package.json +1 -1
  50. package/scripts/vendor-gepa-python.mjs +81 -12
  51. package/src/cleanup.ts +37 -0
  52. package/src/gepa-engine-dispatch.ts +21 -1
  53. package/src/gepa-python-paths.ts +24 -6
  54. package/src/help.ts +37 -15
  55. package/src/hosted-commands.ts +4 -0
  56. package/src/hosted-gepa-cli.ts +107 -0
  57. package/src/hosted-gepa-numeric-values.ts +60 -0
  58. package/src/hosted-provider-settings.ts +69 -0
  59. package/src/hosted-session-cli.ts +4 -1
  60. package/src/http.ts +8 -0
  61. package/src/index.ts +112 -89
  62. package/src/instruction-set-loader.ts +55 -5
  63. package/src/instruction-sets-cli.ts +173 -19
  64. package/src/optimizations-promote-cli.ts +85 -7
  65. package/src/runner-dir-verify.ts +11 -6
  66. package/src/skilled-proposer-bake-report.ts +1 -0
  67. package/src/skilled-proposer-config.ts +191 -0
  68. package/src/skilled-proposer-launch.ts +96 -7
  69. package/src/skilled-proposer-venv-manager.mjs +44 -27
  70. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
  71. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
  72. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +7 -2
  73. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
  74. package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
  75. package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +15 -0
  76. package/vendor/skills/orizu-cli/SKILL.md +94 -168
  77. package/vendor/skills/orizu-cli/references/assess-and-plan.md +107 -0
  78. package/vendor/skills/orizu-cli/references/building-judges.md +442 -180
  79. package/vendor/skills/orizu-cli/references/cli-reference.md +69 -39
  80. package/vendor/skills/orizu-cli/references/dataset-design.md +100 -0
  81. package/vendor/skills/orizu-cli/references/eval-strategy.md +64 -0
  82. package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +115 -0
  83. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +150 -82
  84. package/vendor/skills/orizu-cli/references/optimization-reports.md +196 -104
  85. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +57 -37
  86. package/vendor/skills/orizu-cli/references/primer.md +43 -18
  87. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +199 -131
@@ -1,22 +1,28 @@
1
1
  # Migrating an existing GEPA setup into Orizu
2
2
 
3
3
  For a customer already running official GEPA from a plain script — inline
4
- `trainset`/`valset`, a seed prompt in a dict, their own `metric(row, output)`.
4
+ `trainset`/`valset`, seed component values in a dict, and their own
5
+ `metric(row, output)`. The dict becomes one instruction-set profile: its keys
6
+ become the set's fixed shape, and its values become that profile's components.
7
+ Use `orizu instructions` as the only creation and update path for those
8
+ customer-owned instructions. Prepare the complete manifest, then follow the
9
+ [Authority map](../SKILL.md#authority-map) for its mutations.
5
10
 
6
11
  The ordering rule: **nothing is optimized until parity is proven.** The last
7
12
  step before `run-gepa` is always `orizu scorers verify-parity`, which runs the
8
13
  migrated scorer runner under the exact payload `run-gepa` sends it and the
9
14
  customer's original metric over the same rows, and exits 0 only if they agree.
10
15
 
11
- Commands are shown with `--local`; drop it against a hosted server.
16
+ Commands are shown with `--local`; drop it against a hosted server and follow
17
+ the Authority map for surface-specific execution and hand-offs.
12
18
 
13
19
  ## 0. Read their script
14
20
 
15
21
  Note the row shape and which field is the stable id, `trainset`/`valset`
16
- membership, the seed candidate text, the metric's `module:function`, and what
17
- it returns (a float, a dict with `score`, or GEPA's `EvaluationResult` — all
18
- three are accepted; `objective_scores` is ignored). If rows have no stable id,
19
- add one now: every step below pairs on it.
22
+ membership, the seed component map, the metric's `module:function`, and what it
23
+ returns (a float, a dict with `score`, or GEPA's `EvaluationResult` — all three
24
+ are accepted; `objective_scores` is ignored). If rows have no stable id, add
25
+ one now: every step below pairs on it.
20
26
 
21
27
  ## 1. Set up, then export the rows
22
28
 
@@ -24,32 +30,44 @@ add one now: every step below pairs on it.
24
30
  orizu setup --team <team-slug>
25
31
  ```
26
32
 
27
- One dataset holds every row; the split set decides train vs validation. Dump
28
- rows verbatim from their script, and mirror their lists exactly — a resampled
29
- split makes every later number incomparable with the numbers they have.
30
-
31
- Build the dataset from the **id-deduplicated union**: a row reused in both
32
- `TRAINSET` and `VALSET` would otherwise be written twice, and the upload route
33
- rejects duplicate canonical row ids (`app/api/datasets/route.ts:206-211`,
34
- "csvData must contain unique canonical row ids"). The partition lists keep the
35
- original ids independently, so a shared row can still appear in both.
33
+ One dataset holds every row; require four disjoint, nonempty partitions:
34
+ `TRAINSET`, `VALSET`, `PARITYSET`, and `FINAL_HELD_OUT`. Mirror existing
35
+ train/validation membership exactly. Here, `parity` is a migration-only
36
+ auxiliary partition, not an application partition; the canonical J3
37
+ application doctrine remains train, validation, and final-held-out. Source and
38
+ approve representative `PARITYSET` rows before reserving final-held-out, then
39
+ reserve `FINAL_HELD_OUT` only from the remaining rows. Apply J3's coverage
40
+ discipline:
41
+ every named scenario class must have nonzero train, validation, and
42
+ final-held-out counts. Stop when that coverage, ordering, or four-way
43
+ disjointness cannot be proven; a resampled or overlapping split makes later
44
+ comparisons invalid.
36
45
 
37
46
  ```python
38
47
  import json
39
- seen, rows = set(), []
40
- for row in TRAINSET + VALSET:
41
- if row["id"] in seen:
42
- continue
43
- seen.add(row["id"])
44
- rows.append(row)
48
+ partitions = {
49
+ "train": TRAINSET,
50
+ "validation": VALSET,
51
+ "parity": PARITYSET,
52
+ "final-held-out": FINAL_HELD_OUT,
53
+ }
54
+ if any(not partition for partition in partitions.values()):
55
+ raise ValueError("train, validation, parity, and final-held-out must be nonempty")
56
+ partition_ids = {
57
+ name: [row["id"] for row in partition]
58
+ for name, partition in partitions.items()
59
+ }
60
+ all_ids = [row_id for ids in partition_ids.values() for row_id in ids]
61
+ if len(all_ids) != len(set(all_ids)):
62
+ raise ValueError("train, validation, parity, and final-held-out must be disjoint")
63
+ rows = [row for partition in partitions.values() for row in partition]
45
64
  with open("dataset.jsonl", "w") as handle:
46
65
  for row in rows:
47
66
  handle.write(json.dumps(row) + "\n")
48
67
  with open("split.json", "w") as handle:
49
68
  json.dump({"name": "default", "strategy": "predefined", "seed": 42, "partitions": [
50
- {"name": "train", "row_ids": [row["id"] for row in TRAINSET]},
51
- {"name": "validation", "row_ids": [row["id"] for row in VALSET]},
52
- {"name": "test", "row_ids": []},
69
+ {"name": name, "row_ids": row_ids}
70
+ for name, row_ids in partition_ids.items()
53
71
  ]}, handle)
54
72
  ```
55
73
 
@@ -59,45 +77,56 @@ with open("split.json", "w") as handle:
59
77
  orizu --local datasets upload --file ./dataset.jsonl \
60
78
  --project <team>/<project> --name "GEPA migration rows" --json
61
79
 
62
- orizu --local datasets versions create <dataset-id-or-name> \
63
- --project <team>/<project> --label v1 --json # -> dataset_version_id
64
-
65
- orizu --local datasets splits create <dataset-version-id> \
80
+ orizu --local datasets splits create <upload-returned-dataset-version-id> \
66
81
  --from-file ./split.json --json # -> split_set_id
67
82
  ```
68
83
 
84
+ Upload creates the initial `v1` snapshot. Capture its returned `dataset_version_id` and use that ID directly for split creation; reserve `datasets versions create` for later snapshots with a different label.
85
+
69
86
  `--from-file` preserves their partitions; the ratio flags
70
87
  (`--train/--validation/--test`) resample and must not be used here.
71
88
 
72
- ## 3. Candidate runner and seed prompt
89
+ ## 3. Candidate runner and seed instruction set
73
90
 
74
- The candidate runner does what `task_lm` did: candidate prompt + row -> output.
91
+ The candidate runner does what `task_lm` did: candidate profile + row ->
92
+ output. A one-component profile keeps the single-body runner contract. For a
93
+ multi-component profile, read the complete map from
94
+ `input["instruction_set"]["components"]`; components belong only to this
95
+ profile and are never shared across profiles or sets.
75
96
 
76
97
  ```bash
77
98
  orizu --local runners push ./candidate-runner \
78
- --project <team>/<project> --name gepa-candidate-runner --label default --json
99
+ --project <team>/<project> --name gepa-candidate-runner --json
79
100
 
80
- orizu --local prompts push ./prompt \
81
- --project <team>/<project> --runner-version <candidate-runner-version-id> --json
101
+ orizu --local instructions create ./orizu.instruction-set.json \
102
+ --project <team>/<project> \
103
+ --runner-version <candidate-runner-version-id> \
104
+ --model-config <provider/model> --json
82
105
  ```
83
106
 
84
- `./prompt` holds `orizu.prompt.json` plus the body file it names. Only `name`
85
- is enforced (`app/api/cli/prompts/route.ts:220-221`); `body_file` defaults to
86
- `prompt.md`, `body_kind` to `text` (`packages/cli/src/index.ts:3966-3985`) and
87
- `provider_settings` to `{}`. Write all four anyway — a migration whose model
88
- settings are implicit is a migration nobody can reproduce:
107
+ The agent-created runner version is unlabeled. No standalone runner-label command exists. Following the [Authority map](../SKILL.md#authority-map), if a human accepts its exact ID as default, the human curator runs `orizu --local runners pull gepa-candidate-runner --project <team>/<project> --version <candidate-runner-version-id> --out ./candidate-runner-default-handoff`, leaves that fresh exact-version directory unchanged, then runs `orizu --local runners push ./candidate-runner-default-handoff --project <team>/<project> --name gepa-candidate-runner --label default --json`. The manifest
108
+ carries the set's `name`, human-readable `description`, fixed `shape`, and component values. Put each original dict
109
+ value in a file without changing its bytes. This one-component example maps
110
+ the old `system` key to `system.md`:
89
111
 
90
112
  ```json
91
113
  {
92
- "name": "gepa-seed-prompt",
93
- "body_file": "prompt.md",
94
- "body_kind": "text",
95
- "provider_settings": { "model": "claude-sonnet-4-6", "temperature": 0 }
114
+ "name": "gepa-agent-instructions",
115
+ "description": "Instructions for the migrated GEPA application",
116
+ "shape": ["system"],
117
+ "components": [
118
+ { "key": "system", "path": "./system.md" }
119
+ ]
96
120
  }
97
121
  ```
98
122
 
99
- `prompt.md` carries their seed candidate text verbatim. Returns the seed
100
- `prompt_version_id`.
123
+ For a multi-component seed, list every original key once in `shape`, in stable
124
+ order, and add one component entry per key. `--model-config` selects the
125
+ profile whose seed is created; it must name an existing model config with the
126
+ same settings used by the original script. The JSON result includes the set's
127
+ stable `slug` and its default profile version. Record the slug — later reads,
128
+ optimization, archiving, and restoration should use it even if the display
129
+ name changes.
101
130
 
102
131
  ## 4. Wrap their metric in a scorer runner
103
132
 
@@ -155,19 +184,18 @@ def metric(row, model_output): # what verify-parity calls
155
184
 
156
185
  ```bash
157
186
  orizu --local runners push ./scorer-runner \
158
- --project <team>/<project> --name gepa-scorer-runner --label default --json
187
+ --project <team>/<project> --name gepa-scorer-runner --json
159
188
 
160
- orizu --local prompts push ./scorer-prompt \
189
+ orizu --local judges push ./judge \
161
190
  --project <team>/<project> --runner-version <scorer-runner-version-id> --json
162
191
 
163
192
  orizu --local scorers register --project <team>/<project> \
164
193
  --name gepa-migrated-metric --manifest ./scorer.manifest.json \
165
- --prompt-version <scorer-prompt-version-id> \
166
- --runner-version <scorer-runner-version-id> \
167
- --label production --json # -> scorer_version_id
194
+ --prompt-version <judge-version-id> \
195
+ --runner-version <scorer-runner-version-id> --json # -> scorer_version_id
168
196
  ```
169
197
 
170
- `scorer.manifest.json` needs all four fields below;
198
+ Both agent-created versions are unlabeled. For accepted versions, a human curator materializes the exact runner with `orizu --local runners pull gepa-scorer-runner --project <team>/<project> --version <scorer-runner-version-id> --out ./scorer-runner-default-handoff`, leaves that fresh directory unchanged, runs `orizu --local runners push ./scorer-runner-default-handoff --project <team>/<project> --name gepa-scorer-runner --label default --json`, then runs `orizu --local scorers labels set gepa-migrated-metric production --version <scorer-version-id> --project <team>/<project> --json`. `scorer.manifest.json` needs all four fields below;
171
199
  `app/api/cli/scorers/route.ts` rejects a manifest missing `implementation_kind`
172
200
  or `metric_key`, and `mode` must be `row` or `set`:
173
201
 
@@ -180,29 +208,41 @@ or `metric_key`, and `mode` must be `row` or `set`:
180
208
  }
181
209
  ```
182
210
 
183
- `prompt_runner` is the only kind `runners exec --scorer-version` and
184
- `verify-parity` can execute, and it requires a backing prompt version. For a
185
- pure-code metric with no judge text, `./scorer-prompt` is a short prompt
186
- version documenting the rubric; the runner never reads its body.
211
+ `prompt_runner` is the only implementation kind that `runners exec
212
+ --scorer-version` and `verify-parity` can execute, and the scorer contract
213
+ currently names its backing judge artifact with `--prompt-version`. That flag
214
+ receives the `prompt_version_id` returned by `orizu judges push`; do not replace
215
+ the judge command with a standalone prompt mutation.
187
216
 
188
- ## 5. Produce `outputs.jsonl` by running the seed once
217
+ For an LLM metric, `./judge` contains the evaluator rubric the scorer runner
218
+ executes. For a pure-code metric, keep a short judge rubric that documents the
219
+ deterministic rule and expected output, even though the runner does not read
220
+ its body. The judge is the evaluator artifact; the scorer is the metric
221
+ contract that gives its numeric result a name, mode, and direction.
189
222
 
190
- A scorer scores *(row, output)*, so parity needs one candidate output per row.
223
+ ## 5. Freeze the original setup's outputs
191
224
 
192
- ```bash
193
- orizu --local runners exec \
194
- --prompt-version <seed-prompt-version-id> \
195
- --runner-version <candidate-runner-version-id> \
196
- --dataset-version <dataset-version-id> \
197
- --split-set <split-set-id> --split validation \
198
- --out ./results.jsonl
225
+ A scorer scores *(row, output)*, so parity needs one candidate output per row
226
+ in its development corpus. Use the customer's existing script to run its
227
+ original `task_lm` once with the unchanged seed component map on train,
228
+ validation, and the mandatory `parity` partition, and record the exact output
229
+ passed to `metric(row, output)`. Write `outputs-train.jsonl`,
230
+ `outputs-validation.jsonl`, and `outputs-parity.jsonl`. Each JSONL row is:
199
231
 
200
- jq -c '{row_id: .row_id, model_output: .model_response}' ./results.jsonl > ./outputs.jsonl
232
+ ```json
233
+ {"row_id":"<the row's stable id>","model_output":"<the exact output>"}
201
234
  ```
202
235
 
203
- `outputs.jsonl` must cover exactly the rows being checked: a missing or extra
204
- `row_id`, a duplicate, or a non-string `model_output` is refused before
205
- anything runs.
236
+ Do not produce this evidence by running the migrated scorer or by starting an
237
+ optimization. The output is a fixed subject shared by the original metric and
238
+ the migrated scorer, so a mismatch isolates the scorer migration rather than
239
+ mixing in candidate-runner or provider drift. Keep final-held-out outside this
240
+ evidence. After scorer parity is proven, characterize the candidate runner
241
+ separately against the same parity rows before starting GEPA.
242
+
243
+ Each parity output file must cover exactly the rows being checked: a
244
+ missing or extra `row_id`, a duplicate, or a non-string `model_output` is
245
+ refused before anything runs.
206
246
 
207
247
  ## 6. Prove parity
208
248
 
@@ -211,7 +251,15 @@ orizu --local scorers verify-parity \
211
251
  --scorer-version <scorer-version-id> \
212
252
  --dataset-version <dataset-version-id> \
213
253
  --split-set <split-set-id> --split validation \
214
- --outputs ./outputs.jsonl \
254
+ --outputs ./outputs-validation.jsonl \
255
+ --original metric:metric \
256
+ --json
257
+
258
+ orizu --local scorers verify-parity \
259
+ --scorer-version <scorer-version-id> \
260
+ --dataset-version <dataset-version-id> \
261
+ --split-set <split-set-id> --split parity \
262
+ --outputs ./outputs-parity.jsonl \
215
263
  --original metric:metric \
216
264
  --json
217
265
  ```
@@ -237,9 +285,9 @@ orizu --local scorers verify-parity \
237
285
  the registered runner version; drifted bytes exit 2.
238
286
  - `--limit <n>` checks the first N rows of the partition — a smoke check, not
239
287
  the proof: it always reports `parity: false` with `scope: {compared, total}`,
240
- so a limited run can never be mistaken for a full one. The same full
241
- `outputs.jsonl` works: outputs must cover the limited rows and belong to the
242
- split, and need not be trimmed to N.
288
+ so a limited run can never be mistaken for a full one. The full partition's
289
+ output file works: outputs must cover the limited rows, belong to the split,
290
+ and need not be trimmed to N.
243
291
  - A repeated option takes its LAST value, matching run-gepa's scalar options.
244
292
  - If a sampled row already has the candidate field as a key, the command exits 2
245
293
  rather than overwriting a value the original metric reads.
@@ -252,12 +300,17 @@ rows, unverified runner dir, original metric not importable).
252
300
  Do not continue on exit 1 or 2. A scorer that disagrees with their metric
253
301
  optimizes the wrong thing, and every number afterwards is wrong.
254
302
 
255
- **Prove parity on BOTH partitions before declaring the scorer migrated.**
256
- `run-gepa` scores `train` and `validation`, and training rows drive GEPA's
257
- proposals: a scorer that agrees on validation but diverges on a training row
258
- that exercises a different schema branch changes the experiment's result while
259
- passing this gate. Repeat steps 5 and 6 with `--split train` (its own
260
- `outputs.jsonl` from the same seed run) and require exit 0 on both.
303
+ **Prove parity on train, validation, and parity before declaring the scorer
304
+ migrated.** `run-gepa` scores train and validation, and training rows drive
305
+ GEPA's proposals: a scorer that agrees on validation but diverges on a
306
+ different schema branch changes the experiment's result while passing this
307
+ gate. Repeat the command with `--split train` and `outputs-train.jsonl`; the
308
+ explicit `parity` command above covers the independently selected corpus.
309
+ Require exit 0 on all three parity runs.
310
+
311
+ Keep final-held-out untouched until J6. It is exercised exactly once, at the
312
+ final seed-vs-selected comparison, by the already-parity-proven scorer and
313
+ supplies no scorer-development feedback.
261
314
 
262
315
  A `parity: true` report means every row of that partition was compared and
263
316
  agreed. A limited run reports `parity: false` with
@@ -268,12 +321,14 @@ agreed. A limited run reports `parity: false` with
268
321
 
269
322
  ```bash
270
323
  orizu --local optimizers push ./optimizer \
271
- --project <team>/<project> --name gepa-optimizer --label gepa-v1 --json
324
+ --project <team>/<project> --name gepa-optimizer --json
272
325
 
273
326
  orizu --local optimizations run-gepa \
274
327
  --project <team>/<project> \
275
328
  --optimizer-version-id <optimizer-version-id> \
276
- --candidate-version-id <seed-prompt-version-id> \
329
+ --instruction-set <instruction-set-slug> \
330
+ --model-config <provider/model> \
331
+ --component-selector round-robin \
277
332
  --runner-version-id <candidate-runner-version-id> \
278
333
  --candidate-runner-dir ./candidate-runner \
279
334
  --scorer-version-id <scorer-version-id> \
@@ -294,10 +349,23 @@ reflection model is refused at launch without it
294
349
  `packages/orizu-gepa/src/orizu_gepa_connector/runtime.py:193-196`). Drop it only
295
350
  if you also pass an `openai/...` `--reflection-model`.
296
351
 
352
+ `--instruction-set` and `--model-config` select exactly one profile. The CLI
353
+ uses that profile's production version when it has one and otherwise uses the
354
+ set default; it refuses the legacy `--candidate-version-id` selector when the
355
+ instruction-set selectors are present. `round-robin` updates one component per
356
+ round; use `--component-selector all` only when every component should be
357
+ reflected on each round.
358
+
297
359
  `./optimizer` needs `manifest.json` with `"optimizer_family": "gepa"`. See
298
360
  `references/optimization-with-gepa.md` for reflection-model and budget flags.
299
361
  Pass `--scorer-candidate-field <field>` here too if you passed it to
300
362
  `verify-parity`: the payload the runner sees must be the one parity was proven
301
- under. Afterwards, `orizu optimizations promote <run-id> --candidate <id>
302
- --label production` promotes the winner — and with parity proven, its score is
303
- comparable to the seed score their own script reports.
363
+ under. Select one candidate on validation evidence, then reserve one final-held-out seed-versus-selected-candidate comparison for the promotion decision.
364
+ Write and attach the optimization report, then obtain the human decision. Following the [Authority map](../SKILL.md#authority-map), in the simpler one-shot path a human curator runs `orizu --local optimizations promote <run-id> --candidate <candidate-id> --label production --project <team>/<project> --json` after acceptance. In the equivalent two-stage path for separate materialization, the coding agent materializes once with the unlabeled command below.
365
+
366
+ ```bash
367
+ orizu --local optimizations promote <run-id> --candidate <candidate-id> \
368
+ --project <team>/<project> --json
369
+ ```
370
+
371
+ A human curator re-runs the same promotion as `orizu --local optimizations promote <run-id> --candidate <candidate-id> --label production --project <team>/<project> --json`. The idempotent finalizer finds the existing profile version by run and candidate provenance, moves production to it, and creates no duplicate profile version or candidate-promoted event.