orizu 0.5.18 → 0.5.20

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 (144) hide show
  1. package/README.md +23 -0
  2. package/dist/diff-comments-cli.js +68 -0
  3. package/dist/gepa-engine-dispatch.js +261 -0
  4. package/dist/gepa-python-command.js +26 -0
  5. package/dist/gepa-python-paths.js +39 -0
  6. package/dist/help.js +4 -4
  7. package/dist/index.js +37 -63
  8. package/dist/markdown-report-input.js +15 -1
  9. package/dist/prompt-length-wire.js +30 -0
  10. package/dist/prompt-report-cli.js +2 -2
  11. package/dist/prompt-summary-table.js +37 -0
  12. package/dist/report-markdown-blankness.js +6 -0
  13. package/dist/report-replacement-warning.js +31 -0
  14. package/package.json +2 -2
  15. package/scripts/build-cli-dist.mjs +30 -0
  16. package/scripts/orizu-source-checkout.d.mts +1 -0
  17. package/scripts/orizu-source-checkout.mjs +10 -0
  18. package/scripts/vendor-gepa-python.mjs +187 -4
  19. package/src/diff-comments-cli.ts +125 -0
  20. package/src/gepa-engine-dispatch.ts +300 -0
  21. package/src/gepa-python-command.ts +31 -0
  22. package/src/gepa-python-paths.ts +53 -0
  23. package/src/help.ts +4 -4
  24. package/src/index.ts +46 -69
  25. package/src/markdown-report-input.ts +19 -1
  26. package/src/prompt-length-wire.ts +65 -0
  27. package/src/prompt-report-cli.ts +2 -2
  28. package/src/prompt-summary-table.ts +57 -0
  29. package/src/report-markdown-blankness.ts +6 -0
  30. package/src/report-replacement-warning.ts +35 -0
  31. package/vendor/gepa-python/LICENSE +7 -0
  32. package/vendor/gepa-python/manifest.json +107 -0
  33. package/vendor/gepa-python/src/gepa/__init__.py +22 -0
  34. package/vendor/gepa-python/src/gepa/adapters/__init__.py +0 -0
  35. package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/__init__.py +1 -0
  36. package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
  37. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/__init__.py +26 -0
  38. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/confidence_adapter.py +618 -0
  39. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/scoring.py +206 -0
  40. package/vendor/gepa-python/src/gepa/adapters/default_adapter/__init__.py +0 -0
  41. package/vendor/gepa-python/src/gepa/adapters/default_adapter/default_adapter.py +206 -0
  42. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/__init__.py +0 -0
  43. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/dspy_adapter.py +525 -0
  44. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/instruction_proposal.py +500 -0
  45. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/__init__.py +6 -0
  46. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
  47. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/full_program_adapter.py +269 -0
  48. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/__init__.py +56 -0
  49. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
  50. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
  51. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
  52. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
  53. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
  54. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
  55. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
  56. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
  57. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
  58. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
  59. package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/__init__.py +11 -0
  60. package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/langchain_adapter.py +261 -0
  61. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/__init__.py +37 -0
  62. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_adapter.py +693 -0
  63. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_client.py +364 -0
  64. package/vendor/gepa-python/src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py +879 -0
  65. package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/__init__.py +0 -0
  66. package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
  67. package/vendor/gepa-python/src/gepa/api.py +468 -0
  68. package/vendor/gepa-python/src/gepa/core/__init__.py +0 -0
  69. package/vendor/gepa-python/src/gepa/core/adapter.py +227 -0
  70. package/vendor/gepa-python/src/gepa/core/callbacks.py +551 -0
  71. package/vendor/gepa-python/src/gepa/core/data_loader.py +74 -0
  72. package/vendor/gepa-python/src/gepa/core/engine.py +1025 -0
  73. package/vendor/gepa-python/src/gepa/core/result.py +271 -0
  74. package/vendor/gepa-python/src/gepa/core/state.py +706 -0
  75. package/vendor/gepa-python/src/gepa/examples/__init__.py +0 -0
  76. package/vendor/gepa-python/src/gepa/examples/aime.py +24 -0
  77. package/vendor/gepa-python/src/gepa/examples/anymaths-bench/eval_default.py +111 -0
  78. package/vendor/gepa-python/src/gepa/examples/anymaths-bench/train_anymaths.py +177 -0
  79. package/vendor/gepa-python/src/gepa/examples/mcp_adapter/__init__.py +4 -0
  80. package/vendor/gepa-python/src/gepa/examples/mcp_adapter/mcp_optimization_example.py +456 -0
  81. package/vendor/gepa-python/src/gepa/examples/rag_adapter/__init__.py +9 -0
  82. package/vendor/gepa-python/src/gepa/examples/rag_adapter/rag_optimization.py +824 -0
  83. package/vendor/gepa-python/src/gepa/examples/terminal-bench/train_terminus.py +161 -0
  84. package/vendor/gepa-python/src/gepa/gepa_utils.py +117 -0
  85. package/vendor/gepa-python/src/gepa/gskill/__init__.py +1 -0
  86. package/vendor/gepa-python/src/gepa/gskill/gskill/__init__.py +0 -0
  87. package/vendor/gepa-python/src/gepa/gskill/gskill/cost_tracker.py +194 -0
  88. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/__init__.py +0 -0
  89. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code.py +1117 -0
  90. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code_skills.py +613 -0
  91. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/mini_swe_agent.py +440 -0
  92. package/vendor/gepa-python/src/gepa/gskill/gskill/experiment_logger.py +367 -0
  93. package/vendor/gepa-python/src/gepa/gskill/gskill/swe_fitness_fn.py +209 -0
  94. package/vendor/gepa-python/src/gepa/gskill/gskill/swe_harness.py +352 -0
  95. package/vendor/gepa-python/src/gepa/gskill/gskill/train_optimize_anything.py +830 -0
  96. package/vendor/gepa-python/src/gepa/image.py +104 -0
  97. package/vendor/gepa-python/src/gepa/lm.py +261 -0
  98. package/vendor/gepa-python/src/gepa/logging/__init__.py +0 -0
  99. package/vendor/gepa-python/src/gepa/logging/experiment_tracker.py +457 -0
  100. package/vendor/gepa-python/src/gepa/logging/logger.py +75 -0
  101. package/vendor/gepa-python/src/gepa/logging/utils.py +130 -0
  102. package/vendor/gepa-python/src/gepa/optimize_anything.py +1701 -0
  103. package/vendor/gepa-python/src/gepa/proposer/__init__.py +0 -0
  104. package/vendor/gepa-python/src/gepa/proposer/base.py +53 -0
  105. package/vendor/gepa-python/src/gepa/proposer/merge.py +403 -0
  106. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/__init__.py +0 -0
  107. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/base.py +64 -0
  108. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflection_lm.py +178 -0
  109. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflective_mutation.py +641 -0
  110. package/vendor/gepa-python/src/gepa/py.typed +0 -0
  111. package/vendor/gepa-python/src/gepa/strategies/__init__.py +0 -0
  112. package/vendor/gepa-python/src/gepa/strategies/acceptance.py +66 -0
  113. package/vendor/gepa-python/src/gepa/strategies/batch_sampler.py +103 -0
  114. package/vendor/gepa-python/src/gepa/strategies/candidate_selector.py +82 -0
  115. package/vendor/gepa-python/src/gepa/strategies/component_selector.py +36 -0
  116. package/vendor/gepa-python/src/gepa/strategies/eval_policy.py +64 -0
  117. package/vendor/gepa-python/src/gepa/strategies/instruction_proposal.py +153 -0
  118. package/vendor/gepa-python/src/gepa/strategies/proposal_sampling.py +128 -0
  119. package/vendor/gepa-python/src/gepa/strategies/proposal_selection.py +84 -0
  120. package/vendor/gepa-python/src/gepa/utils/__init__.py +63 -0
  121. package/vendor/gepa-python/src/gepa/utils/code_execution.py +626 -0
  122. package/vendor/gepa-python/src/gepa/utils/stdio_capture.py +145 -0
  123. package/vendor/gepa-python/src/gepa/utils/stop_condition.py +230 -0
  124. package/vendor/gepa-python/src/gepa/visualization.py +359 -0
  125. package/vendor/orizu-gepa/pyproject.toml +18 -0
  126. package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +8 -0
  127. package/vendor/orizu-gepa/src/orizu_gepa_connector/__main__.py +5 -0
  128. package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +132 -0
  129. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +652 -0
  130. package/vendor/orizu-gepa/src/orizu_gepa_connector/cli.py +7 -0
  131. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +177 -0
  132. package/vendor/orizu-gepa/src/orizu_gepa_connector/preflight.py +64 -0
  133. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +106 -0
  134. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +643 -0
  135. package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +38 -0
  136. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +202 -0
  137. package/vendor/orizu-gepa-python/pyproject.toml +1 -1
  138. package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
  139. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +49 -0
  140. package/vendor/skills/orizu-cli/SKILL.md +1 -1
  141. package/vendor/skills/orizu-cli/references/cli-reference.md +10 -0
  142. package/vendor/skills/orizu-cli/references/optimization-reports.md +1 -1
  143. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +1 -1
  144. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +23 -1
package/README.md CHANGED
@@ -25,6 +25,29 @@ Note: the one task we do _not_ let you perform from your CLI is any reviews assi
25
25
 
26
26
  By default, the CLI talks to `https://orizu.ai`.
27
27
 
28
+ ## Updating bundled GEPA
29
+
30
+ The CLI ships a verified, source-only copy of official GEPA so optimization
31
+ users never need `pip` or a network connection. To upgrade it, make a PR that
32
+ updates the version, release commit, and SHA-256 pin in
33
+ `scripts/vendor-gepa-python.mjs`, then re-runs the refresh command with the
34
+ matching downloaded wheel:
35
+
36
+ ```bash
37
+ node scripts/vendor-gepa-python.mjs --refresh --archive /path/to/gepa-<version>-py3-none-any.whl
38
+ ```
39
+
40
+ Commit the refreshed `gepa-python-source.zip` and run the CLI packaging test.
41
+ `npm pack` verifies that archive and materializes only the shipped source tree;
42
+ it never downloads GEPA.
43
+
44
+ For a development checkout, materialize that same verified source before local
45
+ work that imports GEPA:
46
+
47
+ ```bash
48
+ node scripts/vendor-gepa-python.mjs
49
+ ```
50
+
28
51
  ## Install
29
52
 
30
53
  Install the CLI globally with your package manager:
@@ -1,5 +1,6 @@
1
1
  import { extractErrorMessage, isJsonErrorResponsePayload, } from './error-response.js';
2
2
  import { sanitizeHumanInlineText } from './json-response.js';
3
+ import { formatFiniteLengthCount, } from './prompt-length-wire.js';
3
4
  const DETAIL_LEVELS = ['hunk', 'diff', 'full'];
4
5
  const CONTEXT_UNAVAILABLE_WITHOUT_REASON = ' Context unavailable (no reason supplied)';
5
6
  function isCandidatePair(pair) {
@@ -47,6 +48,72 @@ function humanLineNumber(ctx, line) {
47
48
  ? '-'
48
49
  : sanitizeHumanInlineText(ctx.sanitizeTerminalText, String(line + 1));
49
50
  }
51
+ function formatStats(stats) {
52
+ const tokens = formatFiniteLengthCount(stats.tokens);
53
+ return `${tokens === null ? '—' : `~${tokens}`} tokens · ` +
54
+ `${formatFiniteLengthCount(stats.lines) ?? '—'} lines · ` +
55
+ `${formatFiniteLengthCount(stats.chars) ?? '—'} chars · ` +
56
+ `${formatFiniteLengthCount(stats.words) ?? '—'} words`;
57
+ }
58
+ function formatDeltaValue(value, metric, signed = false) {
59
+ const formatted = formatFiniteLengthCount(value, signed);
60
+ if (formatted === null)
61
+ return '—';
62
+ return metric === 'tokens' ? `~${formatted}` : formatted;
63
+ }
64
+ function isLengthMetricDelta(value) {
65
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
66
+ return false;
67
+ }
68
+ const delta = value;
69
+ const isNullableFiniteNumber = (candidate) => candidate === null || (typeof candidate === 'number' && Number.isFinite(candidate));
70
+ return isNullableFiniteNumber(delta.removed) &&
71
+ isNullableFiniteNumber(delta.added) &&
72
+ isNullableFiniteNumber(delta.net);
73
+ }
74
+ function printLengthMeasurements(ctx, pair) {
75
+ function unavailableSuffix(reason) {
76
+ if (typeof reason !== 'string' || reason.length === 0)
77
+ return '';
78
+ return ` (${sanitizeHumanInlineText(ctx.sanitizeTerminalText, reason)})`;
79
+ }
80
+ if (!pair.lengthStats && !pair.lengthDelta) {
81
+ ctx.printLine(` Length: unavailable${unavailableSuffix(pair.lengthDeltaUnavailableReason ?? pair.degraded?.reason)}`);
82
+ return;
83
+ }
84
+ if (pair.lengthStats) {
85
+ ctx.printLine(pair.lengthStats.from
86
+ ? ` From: ${formatStats(pair.lengthStats.from)}`
87
+ : ` From: unavailable${unavailableSuffix(pair.lengthStats.fromUnavailableReason)}`);
88
+ ctx.printLine(pair.lengthStats.to
89
+ ? ` To: ${formatStats(pair.lengthStats.to)}`
90
+ : ` To: unavailable${unavailableSuffix(pair.lengthStats.toUnavailableReason)}`);
91
+ }
92
+ if (!pair.lengthDelta) {
93
+ ctx.printLine(` Length delta: unavailable${unavailableSuffix(pair.lengthDeltaUnavailableReason ?? pair.degraded?.reason)}`);
94
+ return;
95
+ }
96
+ const splitUnavailableReason = pair.lengthDelta.splitUnavailableReason;
97
+ if (typeof splitUnavailableReason === 'string' &&
98
+ splitUnavailableReason.length > 0) {
99
+ ctx.printLine(` Split unavailable: ${sanitizeHumanInlineText(ctx.sanitizeTerminalText, splitUnavailableReason)}`);
100
+ }
101
+ for (const [metric, label] of [
102
+ ['tokens', 'Tokens'],
103
+ ['lines', 'Lines'],
104
+ ['chars', 'Chars'],
105
+ ['words', 'Words'],
106
+ ]) {
107
+ const delta = pair.lengthDelta[metric];
108
+ if (!isLengthMetricDelta(delta)) {
109
+ ctx.printLine(` ${label}: unavailable`);
110
+ continue;
111
+ }
112
+ ctx.printLine(` ${label}: removed ${formatDeltaValue(delta.removed, metric)} · ` +
113
+ `added ${formatDeltaValue(delta.added, metric)} · ` +
114
+ `net ${formatDeltaValue(delta.net, metric, true)}`);
115
+ }
116
+ }
50
117
  function buildParams(ctx) {
51
118
  const runId = ctx.getArg('--run');
52
119
  const promptId = ctx.getArg('--prompt');
@@ -106,6 +173,7 @@ function printDiffComments(ctx, payload) {
106
173
  ctx.printLine(`${hasCandidateIds ? 'Candidates' : 'Versions'} ` +
107
174
  `${sanitizeHumanInlineText(ctx.sanitizeTerminalText, from)} → ` +
108
175
  `${sanitizeHumanInlineText(ctx.sanitizeTerminalText, to)}`);
176
+ printLengthMeasurements(ctx, pair);
109
177
  if (pair.degraded) {
110
178
  ctx.printLine(` Context unavailable: ${sanitizeHumanInlineText(ctx.sanitizeTerminalText, pair.degraded.reason)}`);
111
179
  }
@@ -0,0 +1,261 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ const VALUE_OPTIONS = [
3
+ { flag: '--optimizer-version-id', environment: 'ORIZU_OPTIMIZER_VERSION_ID' },
4
+ { flag: '--candidate-version-id', environment: 'ORIZU_PROMPT_VERSION_ID' },
5
+ { flag: '--runner-version-id', environment: 'ORIZU_RUNNER_VERSION_ID' },
6
+ { flag: '--candidate-runner-dir', environment: 'ORIZU_CANDIDATE_RUNNER_DIR' },
7
+ { flag: '--scorer-version-id', environment: 'ORIZU_SCORER_VERSION_ID' },
8
+ { flag: '--scorer-runner-version-id', environment: 'ORIZU_SCORER_RUNNER_VERSION_ID' },
9
+ { flag: '--scorer-runner-dir', environment: 'ORIZU_SCORER_RUNNER_DIR' },
10
+ { flag: '--scorer-input-contract', environment: 'ORIZU_SCORER_INPUT_CONTRACT' },
11
+ { flag: '--scorer-candidate-field', environment: 'ORIZU_SCORER_CANDIDATE_FIELD' },
12
+ { flag: '--dataset-version-id', environment: 'ORIZU_DATASET_VERSION_ID' },
13
+ { flag: '--split-set-id', environment: 'ORIZU_SPLIT_SET_ID' },
14
+ { flag: '--train-split', environment: 'ORIZU_TRAIN_SPLIT', allowEmpty: true },
15
+ { flag: '--val-split', environment: 'ORIZU_VALIDATION_SPLIT', allowEmpty: true },
16
+ { flag: '--budget', environment: 'ORIZU_BUDGET' },
17
+ { flag: '--max-iterations', environment: 'ORIZU_MAX_ITERATIONS' },
18
+ { flag: '--max-candidate-proposals', environment: 'ORIZU_MAX_CANDIDATE_PROPOSALS' },
19
+ { flag: '--max-full-evals', environment: 'ORIZU_MAX_FULL_EVALS' },
20
+ { flag: '--max-metric-calls', environment: 'ORIZU_MAX_METRIC_CALLS' },
21
+ { flag: '--minibatch-size', environment: 'ORIZU_MINIBATCH_SIZE' },
22
+ { flag: '--num-threads', environment: 'ORIZU_NUM_THREADS' },
23
+ { flag: '--candidate-selection-strategy', environment: 'ORIZU_CANDIDATE_SELECTION_STRATEGY' },
24
+ { flag: '--epsilon', environment: 'ORIZU_EPSILON' },
25
+ { flag: '--reflection-model', environment: 'ORIZU_REFLECTION_MODEL' },
26
+ { flag: '--reflection-temperature', environment: 'ORIZU_REFLECTION_TEMPERATURE' },
27
+ { flag: '--reflection-max-tokens', environment: 'ORIZU_REFLECTION_MAX_TOKENS' },
28
+ { flag: '--reflection-retry-attempts', environment: 'ORIZU_REFLECTION_RETRY_ATTEMPTS' },
29
+ { flag: '--reflection-http-timeout-seconds', environment: 'ORIZU_REFLECTION_HTTP_TIMEOUT_SECONDS' },
30
+ { flag: '--reflection-prompt-template', environment: 'ORIZU_REFLECTION_PROMPT_TEMPLATE', readFile: true },
31
+ { flag: '--reflection-provider-settings', environment: 'ORIZU_REFLECTION_PROVIDER_SETTINGS', readFile: true, json: true },
32
+ { flag: '--objective', environment: 'ORIZU_OBJECTIVE', allowEmpty: true },
33
+ { flag: '--seed', environment: 'ORIZU_SEED' },
34
+ { flag: '--promotion-label', environment: 'ORIZU_PROMOTION_LABEL', allowEmpty: true },
35
+ { flag: '--log-dir', environment: 'ORIZU_LOCAL_LOG_DIR' },
36
+ ];
37
+ const BOOLEAN_OPTIONS = [
38
+ ['--allow-degenerate-seed', 'ORIZU_ALLOW_DEGENERATE_SEED', '1'],
39
+ ['--disable-evaluation-cache', 'ORIZU_DISABLE_EVALUATION_CACHE', '1'],
40
+ ['--auto-promote', 'ORIZU_AUTO_PROMOTE', '1'],
41
+ ['--log-row-snapshots', 'ORIZU_LOG_ROW_SNAPSHOTS', '1'],
42
+ ['--no-local-log', 'ORIZU_NO_LOCAL_LOG', '1'],
43
+ ['--skip-perfect-parent-reflection', 'ORIZU_SKIP_PERFECT_PARENT_REFLECTION', '1'],
44
+ ['--no-skip-perfect-parent-reflection', 'ORIZU_SKIP_PERFECT_PARENT_REFLECTION', '0'],
45
+ ];
46
+ function textValue(value) {
47
+ if (value.startsWith('@'))
48
+ return readFileSync(value.slice(1), 'utf8');
49
+ const path = value;
50
+ return existsSync(path) ? readFileSync(path, 'utf8') : value;
51
+ }
52
+ function jsonObjectValue(value, flag) {
53
+ const raw = textValue(value);
54
+ const parsed = JSON.parse(raw);
55
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
56
+ throw new Error(`${flag} must be a JSON object`);
57
+ }
58
+ // Validate in JavaScript, but preserve the raw spelling for Python's JSON
59
+ // parser so large numbers and exponent forms do not lose precision here.
60
+ return raw;
61
+ }
62
+ function removeEngine(args) {
63
+ let engine;
64
+ const forwarded = [];
65
+ for (let index = 0; index < args.length; index += 1) {
66
+ const argument = args[index];
67
+ if (argument === '--engine') {
68
+ const value = args[index + 1];
69
+ if (!value || value.startsWith('--')) {
70
+ throw new Error('--engine must be one of official, legacy');
71
+ }
72
+ if (engine !== undefined && engine !== value) {
73
+ throw new Error('--engine must select exactly one of official, legacy');
74
+ }
75
+ engine = value;
76
+ index += 1;
77
+ continue;
78
+ }
79
+ if (argument.startsWith('--engine=')) {
80
+ const value = argument.slice('--engine='.length);
81
+ if (engine !== undefined && engine !== value) {
82
+ throw new Error('--engine must select exactly one of official, legacy');
83
+ }
84
+ engine = value;
85
+ continue;
86
+ }
87
+ forwarded.push(argument);
88
+ }
89
+ if (engine !== undefined && engine !== 'official' && engine !== 'legacy') {
90
+ throw new Error(`--engine must be one of official, legacy; received ${engine}`);
91
+ }
92
+ return { engine: engine ?? 'official', args: forwarded };
93
+ }
94
+ function validateLegacyMetadata(args) {
95
+ let raw;
96
+ for (let index = 0; index < args.length; index += 1) {
97
+ if (args[index] === '--metadata') {
98
+ const value = args[index + 1];
99
+ if (!value || value.startsWith('--')) {
100
+ throw new Error('--metadata must be a JSON object');
101
+ }
102
+ raw = value;
103
+ index += 1;
104
+ continue;
105
+ }
106
+ if (args[index].startsWith('--metadata=')) {
107
+ raw = args[index].slice('--metadata='.length);
108
+ }
109
+ }
110
+ let metadata = {};
111
+ if (raw !== undefined) {
112
+ try {
113
+ // The frozen legacy CLI hands this value directly to json.loads. Do not
114
+ // add @file/path expansion here: only reflection inputs support it.
115
+ metadata = JSON.parse(raw);
116
+ }
117
+ catch {
118
+ throw new Error('--metadata must be a JSON object containing valid JSON');
119
+ }
120
+ }
121
+ if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
122
+ throw new Error('--metadata must be a JSON object');
123
+ }
124
+ // The frozen CLI consumes the original argv with json.loads. Keep the raw
125
+ // spelling so Python, not JavaScript, owns JSON number precision and adds
126
+ // the selected legacy-engine tag to the run metadata.
127
+ }
128
+ // Parsing starts from an arbitrary command-line string. Keep the literal
129
+ // tuples above checked, then widen only the map boundary used for lookup.
130
+ const VALUE_OPTIONS_BY_FLAG = new Map(VALUE_OPTIONS.map(option => [option.flag, option]));
131
+ const BOOLEAN_OPTIONS_BY_FLAG = new Map(BOOLEAN_OPTIONS.map(([flag, environment, value]) => [flag, { environment, value }]));
132
+ const CONTROLLED_ENVIRONMENT_NAMES = new Set([
133
+ 'ORIZU_PROJECT',
134
+ 'ORIZU_METADATA',
135
+ ...VALUE_OPTIONS.map(option => option.environment),
136
+ ...BOOLEAN_OPTIONS.map(([, environment]) => environment),
137
+ 'ORIZU_USE_MERGE',
138
+ 'ORIZU_MAX_MERGE_INVOCATIONS',
139
+ 'ORIZU_SAMPLING_STRATEGY',
140
+ 'ORIZU_SELECTION_STRATEGY',
141
+ 'ORIZU_MAX_PAYLOAD_CHARS',
142
+ ]);
143
+ function optionValueOrThrow(args, index, flag, allowEmpty = false) {
144
+ const argument = args[index];
145
+ const equalsPrefix = `${flag}=`;
146
+ if (argument.startsWith(equalsPrefix)) {
147
+ const value = argument.slice(equalsPrefix.length);
148
+ if ((!allowEmpty && value === '') || value.startsWith('--')) {
149
+ throw new Error(`${flag} requires a value that does not start with --`);
150
+ }
151
+ return { value, nextIndex: index };
152
+ }
153
+ const value = args[index + 1];
154
+ if (value === undefined || value.startsWith('--')) {
155
+ throw new Error(`${flag} requires a value that does not start with --`);
156
+ }
157
+ return { value, nextIndex: index + 1 };
158
+ }
159
+ function translateOfficialOptions(args, project, environment) {
160
+ const connectorEnvironment = { ...environment };
161
+ for (const name of CONTROLLED_ENVIRONMENT_NAMES) {
162
+ delete connectorEnvironment[name];
163
+ }
164
+ connectorEnvironment.ORIZU_PROJECT = project;
165
+ let metadata;
166
+ const budgetFlags = [];
167
+ for (let index = 0; index < args.length; index += 1) {
168
+ const argument = args[index];
169
+ if (!argument.startsWith('--')) {
170
+ throw new Error(`Unexpected run-gepa argument ${argument}`);
171
+ }
172
+ const flag = argument.includes('=') ? argument.slice(0, argument.indexOf('=')) : argument;
173
+ if (flag === '--project') {
174
+ const parsed = optionValueOrThrow(args, index, flag);
175
+ connectorEnvironment.ORIZU_PROJECT = parsed.value;
176
+ index = parsed.nextIndex;
177
+ continue;
178
+ }
179
+ if (flag === '--metadata') {
180
+ const parsed = optionValueOrThrow(args, index, flag);
181
+ metadata = parsed.value;
182
+ index = parsed.nextIndex;
183
+ continue;
184
+ }
185
+ const option = VALUE_OPTIONS_BY_FLAG.get(flag);
186
+ if (option) {
187
+ const parsed = optionValueOrThrow(args, index, flag, option.allowEmpty);
188
+ if (flag === '--budget') {
189
+ if (!['auto', 'light', 'medium', 'heavy'].includes(parsed.value)) {
190
+ throw new Error('--budget must be one of auto, light, medium, heavy');
191
+ }
192
+ budgetFlags.push(flag);
193
+ }
194
+ if (['--max-metric-calls', '--max-full-evals', '--max-iterations', '--max-candidate-proposals'].includes(flag)) {
195
+ budgetFlags.push(flag);
196
+ }
197
+ if (flag === '--candidate-selection-strategy' && !['pareto', 'current_best', 'epsilon_greedy'].includes(parsed.value)) {
198
+ throw new Error('--candidate-selection-strategy must be one of pareto, current_best, epsilon_greedy');
199
+ }
200
+ if (flag === '--scorer-input-contract' && !['gepa', 'flat_row'].includes(parsed.value)) {
201
+ throw new Error('--scorer-input-contract must be one of gepa, flat_row');
202
+ }
203
+ connectorEnvironment[option.environment] = option.json
204
+ ? jsonObjectValue(parsed.value, option.flag)
205
+ : option.readFile ? textValue(parsed.value) : parsed.value;
206
+ index = parsed.nextIndex;
207
+ continue;
208
+ }
209
+ const boolean = BOOLEAN_OPTIONS_BY_FLAG.get(flag);
210
+ if (boolean) {
211
+ if (argument.includes('=')) {
212
+ throw new Error(`${flag} does not accept a value`);
213
+ }
214
+ if (args[index + 1] !== undefined && !args[index + 1].startsWith('--')) {
215
+ throw new Error(`${flag} does not accept a value`);
216
+ }
217
+ connectorEnvironment[boolean.environment] = boolean.value;
218
+ continue;
219
+ }
220
+ throw new Error(`Unknown run-gepa option ${flag}`);
221
+ }
222
+ if (budgetFlags.length > 1) {
223
+ throw new Error('Budget options are mutually exclusive; choose at most one of --budget, --max-metric-calls, --max-full-evals, --max-iterations, --max-candidate-proposals');
224
+ }
225
+ const reflectionModel = connectorEnvironment.ORIZU_REFLECTION_MODEL ?? 'anthropic/claude-opus-4-7';
226
+ if (!reflectionModel.startsWith('openai/') && connectorEnvironment.ORIZU_REFLECTION_MAX_TOKENS === undefined) {
227
+ throw new Error('--reflection-max-tokens is required for Anthropic reflection models');
228
+ }
229
+ connectorEnvironment.ORIZU_METADATA = validatedMetadata(metadata);
230
+ return connectorEnvironment;
231
+ }
232
+ function validatedMetadata(raw) {
233
+ if (raw === undefined)
234
+ return '{}';
235
+ try {
236
+ const value = JSON.parse(raw);
237
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
238
+ throw new Error();
239
+ }
240
+ catch {
241
+ throw new Error('--metadata must be a JSON object containing valid JSON');
242
+ }
243
+ return raw;
244
+ }
245
+ export function dispatchGepaEngine(args, project, environment) {
246
+ const selected = removeEngine(args);
247
+ if (selected.engine === 'legacy') {
248
+ if (selected.args.some(argument => argument === '--max-candidate-proposals' || argument.startsWith('--max-candidate-proposals='))) {
249
+ throw new Error('--max-candidate-proposals is supported by the official GEPA engine only');
250
+ }
251
+ validateLegacyMetadata(selected.args);
252
+ return {
253
+ engine: 'legacy',
254
+ args: selected.args,
255
+ environment,
256
+ module: 'orizu_gepa.cli',
257
+ };
258
+ }
259
+ const connectorEnvironment = translateOfficialOptions(selected.args, project, environment);
260
+ return { engine: 'official', args: [], environment: connectorEnvironment, module: 'orizu_gepa_connector' };
261
+ }
@@ -0,0 +1,26 @@
1
+ export function getGepaPythonCommand(args, environment) {
2
+ let python = environment.PYTHON || 'python3';
3
+ const forwarded = [];
4
+ for (let index = 0; index < args.length; index += 1) {
5
+ const argument = args[index];
6
+ if (argument === '--python') {
7
+ const value = args[index + 1];
8
+ if (!value || value.startsWith('--')) {
9
+ throw new Error('--python requires a value that does not start with --');
10
+ }
11
+ python = value;
12
+ index += 1;
13
+ continue;
14
+ }
15
+ if (argument.startsWith('--python=')) {
16
+ const value = argument.slice('--python='.length);
17
+ if (!value || value.startsWith('--')) {
18
+ throw new Error('--python requires a value that does not start with --');
19
+ }
20
+ python = value;
21
+ continue;
22
+ }
23
+ forwarded.push(argument);
24
+ }
25
+ return { python, args: forwarded };
26
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { isOrizuSourceCheckout } from '../scripts/orizu-source-checkout.mjs';
4
+ function prefersLiveSource() {
5
+ // The source candidates belong to this monorepo only when the candidate
6
+ // root identifies itself as Orizu. A consumer's .git marker is not proof:
7
+ // npm installs commonly live inside another repository.
8
+ return isOrizuSourceCheckout(fileURLToPath(new URL('../../../package.json', import.meta.url)));
9
+ }
10
+ export function resolveGepaPythonPath(candidates, pathExists = existsSync) {
11
+ return candidates.find(pathExists) ?? null;
12
+ }
13
+ export function bundledOrizuGepaPythonPath() {
14
+ const source = fileURLToPath(new URL('../../orizu-gepa-python/src', import.meta.url));
15
+ const vendor = fileURLToPath(new URL('../vendor/orizu-gepa-python/src', import.meta.url));
16
+ const candidates = prefersLiveSource() ? [source, vendor] : [vendor];
17
+ return resolveGepaPythonPath(candidates);
18
+ }
19
+ export function bundledOrizuGepaConnectorPythonPath() {
20
+ const source = fileURLToPath(new URL('../../orizu-gepa/src', import.meta.url));
21
+ const vendor = fileURLToPath(new URL('../vendor/orizu-gepa/src', import.meta.url));
22
+ const candidates = prefersLiveSource() ? [source, vendor] : [vendor];
23
+ return resolveGepaPythonPath(candidates);
24
+ }
25
+ export function bundledOfficialGepaPythonPath() {
26
+ const candidates = [
27
+ fileURLToPath(new URL('../vendor/gepa-python/src', import.meta.url)),
28
+ ];
29
+ return resolveGepaPythonPath(candidates);
30
+ }
31
+ export function getGepaPythonPathEntries(existingPythonPath) {
32
+ const pythonPathEntries = [
33
+ bundledOrizuGepaConnectorPythonPath(),
34
+ bundledOrizuGepaPythonPath(),
35
+ bundledOfficialGepaPythonPath(),
36
+ existingPythonPath,
37
+ ].filter((entry) => Boolean(entry));
38
+ return pythonPathEntries;
39
+ }
package/dist/help.js CHANGED
@@ -343,13 +343,13 @@ export const COMMAND_DOCS = [
343
343
  },
344
344
  {
345
345
  path: ['prompts', 'report', 'set'],
346
- usage: 'orizu prompts report set --prompt <id-or-name> --project <team/project> (--version <n> | --version-id <id>) (--report <markdown|@file> | --report-file <path>) [--json]',
346
+ usage: 'orizu prompts report set --prompt <id-or-name> [--project <team/project>] (--version <n> | --version-id <id>) (--report <markdown|@file> | --report-file <path>) [--json]',
347
347
  summary: 'Upload or replace the Markdown report for one explicit prompt version.',
348
348
  group: 'Prompts and judges',
349
349
  aliases: [['prompts', 'report', 'upload']],
350
350
  options: [
351
351
  { name: '--prompt <id-or-name>', help: 'Prompt ID or name.', required: true },
352
- { name: '--project <team/project>', help: 'Project slug containing the prompt.', required: true },
352
+ { name: '--project <team/project>', help: 'Project slug containing the prompt.' },
353
353
  { name: '--version <n>', help: 'Positive prompt version number.' },
354
354
  { name: '--version-id <id>', help: 'Explicit prompt version ID.' },
355
355
  { name: '--report <markdown|@file>', help: 'Inline Markdown or an @file reference.' },
@@ -525,8 +525,8 @@ export const COMMAND_DOCS = [
525
525
  },
526
526
  {
527
527
  path: ['optimizations', 'run-gepa'],
528
- usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
529
- summary: 'Run the bundled GEPA-style optimizer locally and stream events. Validates the scorer contract on the seed before iterating; judge runners built for flat-row score runs need --scorer-input-contract flat_row (ALI-1158).',
528
+ usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> [--engine official|legacy] [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n> | --max-candidate-proposals <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
529
+ summary: 'Run official GEPA locally and stream events; pass --engine legacy only for the frozen safety hatch. --max-candidate-proposals is official-engine-only. Validates the scorer contract on the seed before iterating; judge runners built for flat-row score runs need --scorer-input-contract flat_row (ALI-1158).',
530
530
  group: 'Optimizations',
531
531
  },
532
532
  {
package/dist/index.js CHANGED
@@ -43,14 +43,19 @@ import { runGitCredentialInvocation } from './git-credential.js';
43
43
  import { pushPromptDraft } from './prompt-draft-push.js';
44
44
  import { readMarkdownReportInput } from './markdown-report-input.js';
45
45
  import { promptPushErrorMessage, promptReportCommand } from './prompt-report-cli.js';
46
+ import { printPromptSummaryTable } from './prompt-summary-table.js';
46
47
  import { assertSnapshotManifestConfined, verifyGepaRunnerDirsFromArgs, verifyRunnerDirRegistered } from './runner-dir-verify.js';
47
48
  import { runScorersRegister } from './scorer-draft-push.js';
48
49
  import { runZipArtifactPush } from './zip-draft-push.js';
49
50
  import { runGithubLink, runInteractiveHostedSetup } from './github-setup.js';
50
51
  import { parseJsonResponse, sanitizeTerminalText } from './json-response.js';
52
+ import { reportReplacementWarning } from './report-replacement-warning.js';
51
53
  import { reportCommentsCommand, throwDeprecatedPromptCommentsCommand, } from './report-comments-cli.js';
52
54
  import { diffCommentsCommand } from './diff-comments-cli.js';
53
55
  import { exportOptimizationRunCommand } from './optimization-export-cli.js';
56
+ import { dispatchGepaEngine } from './gepa-engine-dispatch.js';
57
+ import { getGepaPythonCommand } from './gepa-python-command.js';
58
+ import { getGepaPythonPathEntries } from './gepa-python-paths.js';
54
59
  export { parseJsonResponse, sanitizeTerminalText } from './json-response.js';
55
60
  function getErrorMessage(error) {
56
61
  return error instanceof Error ? error.message : String(error);
@@ -184,6 +189,14 @@ export function createCodeChallenge(verifier) {
184
189
  function printError(message) {
185
190
  console.error(sanitizeTerminalText(message));
186
191
  }
192
+ function printPushDisclosures(data) {
193
+ const warning = reportReplacementWarning(data);
194
+ if (warning)
195
+ printError(warning);
196
+ if (data.metadata_updated === true) {
197
+ printError('Updated prompt metadata on the identical existing version');
198
+ }
199
+ }
187
200
  export function validateBrowserUrl(url, expectedOrigin) {
188
201
  let parsed;
189
202
  try {
@@ -246,27 +259,6 @@ export function formatTerminalLink(url) {
246
259
  function shellQuote(value) {
247
260
  return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
248
261
  }
249
- function printPromptSummaries(items, emptyMessage) {
250
- if (items.length === 0) {
251
- printLine(emptyMessage);
252
- return;
253
- }
254
- const rows = items.map(item => ({
255
- id: sanitizeTerminalText(item.id),
256
- name: sanitizeTerminalText(item.name),
257
- role: sanitizeTerminalText(item.role),
258
- status: sanitizeTerminalText(item.status || 'active'),
259
- }));
260
- const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length));
261
- const nameWidth = Math.max('NAME'.length, ...rows.map(row => row.name.length));
262
- const roleWidth = Math.max('ROLE'.length, ...rows.map(row => row.role.length));
263
- const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length));
264
- printLine(`${'ID'.padEnd(idWidth)} ${'NAME'.padEnd(nameWidth)} ${'ROLE'.padEnd(roleWidth)} ${'STATUS'.padEnd(statusWidth)}`);
265
- printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(nameWidth)} ${'-'.repeat(roleWidth)} ${'-'.repeat(statusWidth)}`);
266
- rows.forEach(row => {
267
- printLine(`${row.id.padEnd(idWidth)} ${row.name.padEnd(nameWidth)} ${row.role.padEnd(roleWidth)} ${row.status.padEnd(statusWidth)}`);
268
- });
269
- }
270
262
  function readJsonObjectArg(valueArg, label) {
271
263
  if (!valueArg) {
272
264
  return {};
@@ -905,7 +897,7 @@ async function listPrompts() {
905
897
  printJson(data);
906
898
  return;
907
899
  }
908
- printPromptSummaries(data.prompts, 'No prompts found.');
900
+ printPromptSummaryTable(data.prompts, 'No prompts found.', printLine);
909
901
  }
910
902
  async function listJudges() {
911
903
  const project = getArg('--project') || await resolveProjectSlug(null);
@@ -923,7 +915,7 @@ async function listJudges() {
923
915
  printJson(data);
924
916
  return;
925
917
  }
926
- printPromptSummaries(data.judges, 'No judges found.');
918
+ printPromptSummaryTable(data.judges, 'No judges found.', printLine);
927
919
  }
928
920
  async function listScorers() {
929
921
  const project = getArg('--project') || await resolveProjectSlug(null);
@@ -1034,8 +1026,10 @@ async function pushPromptArtifact(kind) {
1034
1026
  });
1035
1027
  if (hasJsonFlag())
1036
1028
  printJson(data);
1037
- else
1029
+ else {
1030
+ printPushDisclosures(data);
1038
1031
  printLine(message);
1032
+ }
1039
1033
  return;
1040
1034
  }
1041
1035
  // LEGACY SESSIONLESS PATH (no branch to commit to; the server dual-writes
@@ -1081,6 +1075,7 @@ async function pushPromptArtifact(kind) {
1081
1075
  printJson(data);
1082
1076
  return;
1083
1077
  }
1078
+ printPushDisclosures(data);
1084
1079
  printLine(`Pushed ${kind} ${sanitizeTerminalText(String(manifest.name || promptDir))} (${sanitizeTerminalText(String(data.prompt_version_id || 'unknown version'))})`);
1085
1080
  }
1086
1081
  async function pullPromptArtifact(kind) {
@@ -1815,30 +1810,12 @@ async function updateOptimizationRunLifecycle(action) {
1815
1810
  }
1816
1811
  // ALI-1073 list moved to optimizations-list-cli.ts (line ratchet, ALI-976);
1817
1812
  // ALI-1175 labels its best scores as agent-reported evidence there.
1818
- function removeFlagWithValue(args, flag) {
1819
- const filtered = [];
1820
- for (let index = 0; index < args.length; index += 1) {
1821
- if (args[index] === flag) {
1822
- index += 1;
1823
- continue;
1824
- }
1825
- filtered.push(args[index]);
1826
- }
1827
- return filtered;
1828
- }
1829
- function bundledOrizuGepaPythonPath() {
1830
- const candidates = [
1831
- fileURLToPath(new URL('../vendor/orizu-gepa-python/src', import.meta.url)),
1832
- fileURLToPath(new URL('../../orizu-gepa-python/src', import.meta.url)),
1833
- ];
1834
- return candidates.find(candidate => existsSync(candidate)) ?? null;
1835
- }
1836
1813
  async function runGepaOptimization() {
1837
1814
  const project = getArg('--project') || await resolveProjectSlug(null);
1838
1815
  const baseUrl = getBaseUrl();
1839
- const python = getArg('--python') || process.env.PYTHON || 'python3';
1840
- const bundledPythonPath = bundledOrizuGepaPythonPath();
1841
- let forwardedArgs = removeFlagWithValue(cliArgs.slice(2), '--python');
1816
+ const runGepa = getGepaPythonCommand(cliArgs.slice(2), process.env);
1817
+ const python = runGepa.python;
1818
+ let forwardedArgs = runGepa.args;
1842
1819
  // ALI-1159 (ADR-007): GEPA executes ad-hoc local runner dirs while
1843
1820
  // attributing records to registered runner version ids — verify the bytes
1844
1821
  // ARE those versions before the optimizer spawns. Scans the forwarded argv
@@ -1848,6 +1825,7 @@ async function runGepaOptimization() {
1848
1825
  const verified = await verifyGepaRunnerDirsFromArgs(forwardedArgs);
1849
1826
  forwardedArgs = verified.args;
1850
1827
  let result;
1828
+ let selectedEngine = 'official';
1851
1829
  try {
1852
1830
  // Uniform resolution (ALI-1090): honors ORIZU_TOKEN / ORIZU_TOKEN_FILE before
1853
1831
  // credentials.json so hosted sandboxes can run optimizations. Read at spawn
@@ -1858,33 +1836,29 @@ async function runGepaOptimization() {
1858
1836
  if (!forwardedArgs.includes('--project')) {
1859
1837
  forwardedArgs = ['--project', project, ...forwardedArgs];
1860
1838
  }
1861
- const pythonPathEntries = [
1862
- bundledPythonPath,
1863
- process.env.PYTHONPATH,
1864
- ].filter((entry) => Boolean(entry));
1865
- result = spawnSync(python, ['-m', 'orizu_gepa.cli', ...forwardedArgs], {
1839
+ const dispatch = dispatchGepaEngine(forwardedArgs, project, {
1840
+ ...process.env,
1841
+ ORIZU_API_URL: baseUrl,
1842
+ ORIZU_TOKEN: token,
1843
+ ORIZU_PROJECT: project,
1844
+ ORIZU_VERIFIED_RUNNER_DIRS: JSON.stringify(verified.verifiedDirs),
1845
+ PYTHONPATH: getGepaPythonPathEntries(process.env.PYTHONPATH).join(delimiter),
1846
+ PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
1847
+ });
1848
+ selectedEngine = dispatch.engine;
1849
+ result = spawnSync(python, ['-m', dispatch.module, ...dispatch.args], {
1866
1850
  stdio: 'inherit',
1867
- env: {
1868
- ...process.env,
1869
- ORIZU_API_URL: baseUrl,
1870
- ORIZU_TOKEN: token,
1871
- ORIZU_PROJECT: project,
1872
- // Integrity handshake (ALI-1159): the Python entrypoint refuses
1873
- // runner dirs that are not in this wrapper-verified list.
1874
- ORIZU_VERIFIED_RUNNER_DIRS: JSON.stringify(verified.verifiedDirs),
1875
- PYTHONPATH: pythonPathEntries.join(delimiter),
1876
- PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
1877
- },
1851
+ env: dispatch.environment,
1878
1852
  });
1879
1853
  }
1880
1854
  finally {
1881
1855
  verified.cleanup();
1882
1856
  }
1883
1857
  if (result.error) {
1884
- throw result.error;
1858
+ throw new Error(`${selectedEngine} GEPA engine failed: ${result.error.message}`, { cause: result.error });
1885
1859
  }
1886
1860
  if (result.status !== 0) {
1887
- throw new Error(`orizu-gepa failed with exit code ${result.status}`);
1861
+ throw new Error(`${selectedEngine} GEPA engine failed with exit code ${result.status}`);
1888
1862
  }
1889
1863
  if (hasJsonFlag()) {
1890
1864
  printJson({ status: 'completed', exitCode: 0 });