orizu 0.5.19 → 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 (141) hide show
  1. package/README.md +23 -0
  2. package/dist/diff-comments-cli.js +18 -2
  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 +34 -40
  8. package/dist/markdown-report-input.js +15 -1
  9. package/dist/prompt-length-wire.js +2 -2
  10. package/dist/prompt-report-cli.js +2 -2
  11. package/dist/report-markdown-blankness.js +6 -0
  12. package/dist/report-replacement-warning.js +31 -0
  13. package/package.json +2 -2
  14. package/scripts/build-cli-dist.mjs +30 -0
  15. package/scripts/orizu-source-checkout.d.mts +1 -0
  16. package/scripts/orizu-source-checkout.mjs +10 -0
  17. package/scripts/vendor-gepa-python.mjs +187 -4
  18. package/src/diff-comments-cli.ts +37 -3
  19. package/src/gepa-engine-dispatch.ts +300 -0
  20. package/src/gepa-python-command.ts +31 -0
  21. package/src/gepa-python-paths.ts +53 -0
  22. package/src/help.ts +4 -4
  23. package/src/index.ts +35 -43
  24. package/src/markdown-report-input.ts +19 -1
  25. package/src/prompt-length-wire.ts +1 -1
  26. package/src/prompt-report-cli.ts +2 -2
  27. package/src/report-markdown-blankness.ts +6 -0
  28. package/src/report-replacement-warning.ts +35 -0
  29. package/vendor/gepa-python/LICENSE +7 -0
  30. package/vendor/gepa-python/manifest.json +107 -0
  31. package/vendor/gepa-python/src/gepa/__init__.py +22 -0
  32. package/vendor/gepa-python/src/gepa/adapters/__init__.py +0 -0
  33. package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/__init__.py +1 -0
  34. package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
  35. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/__init__.py +26 -0
  36. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/confidence_adapter.py +618 -0
  37. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/scoring.py +206 -0
  38. package/vendor/gepa-python/src/gepa/adapters/default_adapter/__init__.py +0 -0
  39. package/vendor/gepa-python/src/gepa/adapters/default_adapter/default_adapter.py +206 -0
  40. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/__init__.py +0 -0
  41. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/dspy_adapter.py +525 -0
  42. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/instruction_proposal.py +500 -0
  43. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/__init__.py +6 -0
  44. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
  45. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/full_program_adapter.py +269 -0
  46. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/__init__.py +56 -0
  47. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
  48. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
  49. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
  50. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
  51. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
  52. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
  53. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
  54. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
  55. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
  56. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
  57. package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/__init__.py +11 -0
  58. package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/langchain_adapter.py +261 -0
  59. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/__init__.py +37 -0
  60. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_adapter.py +693 -0
  61. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_client.py +364 -0
  62. package/vendor/gepa-python/src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py +879 -0
  63. package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/__init__.py +0 -0
  64. package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
  65. package/vendor/gepa-python/src/gepa/api.py +468 -0
  66. package/vendor/gepa-python/src/gepa/core/__init__.py +0 -0
  67. package/vendor/gepa-python/src/gepa/core/adapter.py +227 -0
  68. package/vendor/gepa-python/src/gepa/core/callbacks.py +551 -0
  69. package/vendor/gepa-python/src/gepa/core/data_loader.py +74 -0
  70. package/vendor/gepa-python/src/gepa/core/engine.py +1025 -0
  71. package/vendor/gepa-python/src/gepa/core/result.py +271 -0
  72. package/vendor/gepa-python/src/gepa/core/state.py +706 -0
  73. package/vendor/gepa-python/src/gepa/examples/__init__.py +0 -0
  74. package/vendor/gepa-python/src/gepa/examples/aime.py +24 -0
  75. package/vendor/gepa-python/src/gepa/examples/anymaths-bench/eval_default.py +111 -0
  76. package/vendor/gepa-python/src/gepa/examples/anymaths-bench/train_anymaths.py +177 -0
  77. package/vendor/gepa-python/src/gepa/examples/mcp_adapter/__init__.py +4 -0
  78. package/vendor/gepa-python/src/gepa/examples/mcp_adapter/mcp_optimization_example.py +456 -0
  79. package/vendor/gepa-python/src/gepa/examples/rag_adapter/__init__.py +9 -0
  80. package/vendor/gepa-python/src/gepa/examples/rag_adapter/rag_optimization.py +824 -0
  81. package/vendor/gepa-python/src/gepa/examples/terminal-bench/train_terminus.py +161 -0
  82. package/vendor/gepa-python/src/gepa/gepa_utils.py +117 -0
  83. package/vendor/gepa-python/src/gepa/gskill/__init__.py +1 -0
  84. package/vendor/gepa-python/src/gepa/gskill/gskill/__init__.py +0 -0
  85. package/vendor/gepa-python/src/gepa/gskill/gskill/cost_tracker.py +194 -0
  86. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/__init__.py +0 -0
  87. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code.py +1117 -0
  88. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code_skills.py +613 -0
  89. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/mini_swe_agent.py +440 -0
  90. package/vendor/gepa-python/src/gepa/gskill/gskill/experiment_logger.py +367 -0
  91. package/vendor/gepa-python/src/gepa/gskill/gskill/swe_fitness_fn.py +209 -0
  92. package/vendor/gepa-python/src/gepa/gskill/gskill/swe_harness.py +352 -0
  93. package/vendor/gepa-python/src/gepa/gskill/gskill/train_optimize_anything.py +830 -0
  94. package/vendor/gepa-python/src/gepa/image.py +104 -0
  95. package/vendor/gepa-python/src/gepa/lm.py +261 -0
  96. package/vendor/gepa-python/src/gepa/logging/__init__.py +0 -0
  97. package/vendor/gepa-python/src/gepa/logging/experiment_tracker.py +457 -0
  98. package/vendor/gepa-python/src/gepa/logging/logger.py +75 -0
  99. package/vendor/gepa-python/src/gepa/logging/utils.py +130 -0
  100. package/vendor/gepa-python/src/gepa/optimize_anything.py +1701 -0
  101. package/vendor/gepa-python/src/gepa/proposer/__init__.py +0 -0
  102. package/vendor/gepa-python/src/gepa/proposer/base.py +53 -0
  103. package/vendor/gepa-python/src/gepa/proposer/merge.py +403 -0
  104. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/__init__.py +0 -0
  105. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/base.py +64 -0
  106. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflection_lm.py +178 -0
  107. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflective_mutation.py +641 -0
  108. package/vendor/gepa-python/src/gepa/py.typed +0 -0
  109. package/vendor/gepa-python/src/gepa/strategies/__init__.py +0 -0
  110. package/vendor/gepa-python/src/gepa/strategies/acceptance.py +66 -0
  111. package/vendor/gepa-python/src/gepa/strategies/batch_sampler.py +103 -0
  112. package/vendor/gepa-python/src/gepa/strategies/candidate_selector.py +82 -0
  113. package/vendor/gepa-python/src/gepa/strategies/component_selector.py +36 -0
  114. package/vendor/gepa-python/src/gepa/strategies/eval_policy.py +64 -0
  115. package/vendor/gepa-python/src/gepa/strategies/instruction_proposal.py +153 -0
  116. package/vendor/gepa-python/src/gepa/strategies/proposal_sampling.py +128 -0
  117. package/vendor/gepa-python/src/gepa/strategies/proposal_selection.py +84 -0
  118. package/vendor/gepa-python/src/gepa/utils/__init__.py +63 -0
  119. package/vendor/gepa-python/src/gepa/utils/code_execution.py +626 -0
  120. package/vendor/gepa-python/src/gepa/utils/stdio_capture.py +145 -0
  121. package/vendor/gepa-python/src/gepa/utils/stop_condition.py +230 -0
  122. package/vendor/gepa-python/src/gepa/visualization.py +359 -0
  123. package/vendor/orizu-gepa/pyproject.toml +18 -0
  124. package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +8 -0
  125. package/vendor/orizu-gepa/src/orizu_gepa_connector/__main__.py +5 -0
  126. package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +132 -0
  127. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +652 -0
  128. package/vendor/orizu-gepa/src/orizu_gepa_connector/cli.py +7 -0
  129. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +177 -0
  130. package/vendor/orizu-gepa/src/orizu_gepa_connector/preflight.py +64 -0
  131. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +106 -0
  132. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +643 -0
  133. package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +38 -0
  134. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +202 -0
  135. package/vendor/orizu-gepa-python/pyproject.toml +1 -1
  136. package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
  137. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +49 -0
  138. package/vendor/skills/orizu-cli/SKILL.md +1 -1
  139. package/vendor/skills/orizu-cli/references/optimization-reports.md +1 -1
  140. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +1 -1
  141. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +6 -3
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:
@@ -61,6 +61,16 @@ function formatDeltaValue(value, metric, signed = false) {
61
61
  return '—';
62
62
  return metric === 'tokens' ? `~${formatted}` : formatted;
63
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
+ }
64
74
  function printLengthMeasurements(ctx, pair) {
65
75
  function unavailableSuffix(reason) {
66
76
  if (typeof reason !== 'string' || reason.length === 0)
@@ -83,8 +93,10 @@ function printLengthMeasurements(ctx, pair) {
83
93
  ctx.printLine(` Length delta: unavailable${unavailableSuffix(pair.lengthDeltaUnavailableReason ?? pair.degraded?.reason)}`);
84
94
  return;
85
95
  }
86
- if (pair.lengthDelta.splitUnavailableReason) {
87
- ctx.printLine(` Split unavailable: ${sanitizeHumanInlineText(ctx.sanitizeTerminalText, pair.lengthDelta.splitUnavailableReason)}`);
96
+ const splitUnavailableReason = pair.lengthDelta.splitUnavailableReason;
97
+ if (typeof splitUnavailableReason === 'string' &&
98
+ splitUnavailableReason.length > 0) {
99
+ ctx.printLine(` Split unavailable: ${sanitizeHumanInlineText(ctx.sanitizeTerminalText, splitUnavailableReason)}`);
88
100
  }
89
101
  for (const [metric, label] of [
90
102
  ['tokens', 'Tokens'],
@@ -93,6 +105,10 @@ function printLengthMeasurements(ctx, pair) {
93
105
  ['words', 'Words'],
94
106
  ]) {
95
107
  const delta = pair.lengthDelta[metric];
108
+ if (!isLengthMetricDelta(delta)) {
109
+ ctx.printLine(` ${label}: unavailable`);
110
+ continue;
111
+ }
96
112
  ctx.printLine(` ${label}: removed ${formatDeltaValue(delta.removed, metric)} · ` +
97
113
  `added ${formatDeltaValue(delta.added, metric)} · ` +
98
114
  `net ${formatDeltaValue(delta.net, metric, true)}`);
@@ -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
@@ -49,9 +49,13 @@ import { runScorersRegister } from './scorer-draft-push.js';
49
49
  import { runZipArtifactPush } from './zip-draft-push.js';
50
50
  import { runGithubLink, runInteractiveHostedSetup } from './github-setup.js';
51
51
  import { parseJsonResponse, sanitizeTerminalText } from './json-response.js';
52
+ import { reportReplacementWarning } from './report-replacement-warning.js';
52
53
  import { reportCommentsCommand, throwDeprecatedPromptCommentsCommand, } from './report-comments-cli.js';
53
54
  import { diffCommentsCommand } from './diff-comments-cli.js';
54
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';
55
59
  export { parseJsonResponse, sanitizeTerminalText } from './json-response.js';
56
60
  function getErrorMessage(error) {
57
61
  return error instanceof Error ? error.message : String(error);
@@ -185,6 +189,14 @@ export function createCodeChallenge(verifier) {
185
189
  function printError(message) {
186
190
  console.error(sanitizeTerminalText(message));
187
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
+ }
188
200
  export function validateBrowserUrl(url, expectedOrigin) {
189
201
  let parsed;
190
202
  try {
@@ -1014,8 +1026,10 @@ async function pushPromptArtifact(kind) {
1014
1026
  });
1015
1027
  if (hasJsonFlag())
1016
1028
  printJson(data);
1017
- else
1029
+ else {
1030
+ printPushDisclosures(data);
1018
1031
  printLine(message);
1032
+ }
1019
1033
  return;
1020
1034
  }
1021
1035
  // LEGACY SESSIONLESS PATH (no branch to commit to; the server dual-writes
@@ -1061,6 +1075,7 @@ async function pushPromptArtifact(kind) {
1061
1075
  printJson(data);
1062
1076
  return;
1063
1077
  }
1078
+ printPushDisclosures(data);
1064
1079
  printLine(`Pushed ${kind} ${sanitizeTerminalText(String(manifest.name || promptDir))} (${sanitizeTerminalText(String(data.prompt_version_id || 'unknown version'))})`);
1065
1080
  }
1066
1081
  async function pullPromptArtifact(kind) {
@@ -1795,30 +1810,12 @@ async function updateOptimizationRunLifecycle(action) {
1795
1810
  }
1796
1811
  // ALI-1073 list moved to optimizations-list-cli.ts (line ratchet, ALI-976);
1797
1812
  // ALI-1175 labels its best scores as agent-reported evidence there.
1798
- function removeFlagWithValue(args, flag) {
1799
- const filtered = [];
1800
- for (let index = 0; index < args.length; index += 1) {
1801
- if (args[index] === flag) {
1802
- index += 1;
1803
- continue;
1804
- }
1805
- filtered.push(args[index]);
1806
- }
1807
- return filtered;
1808
- }
1809
- function bundledOrizuGepaPythonPath() {
1810
- const candidates = [
1811
- fileURLToPath(new URL('../vendor/orizu-gepa-python/src', import.meta.url)),
1812
- fileURLToPath(new URL('../../orizu-gepa-python/src', import.meta.url)),
1813
- ];
1814
- return candidates.find(candidate => existsSync(candidate)) ?? null;
1815
- }
1816
1813
  async function runGepaOptimization() {
1817
1814
  const project = getArg('--project') || await resolveProjectSlug(null);
1818
1815
  const baseUrl = getBaseUrl();
1819
- const python = getArg('--python') || process.env.PYTHON || 'python3';
1820
- const bundledPythonPath = bundledOrizuGepaPythonPath();
1821
- 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;
1822
1819
  // ALI-1159 (ADR-007): GEPA executes ad-hoc local runner dirs while
1823
1820
  // attributing records to registered runner version ids — verify the bytes
1824
1821
  // ARE those versions before the optimizer spawns. Scans the forwarded argv
@@ -1828,6 +1825,7 @@ async function runGepaOptimization() {
1828
1825
  const verified = await verifyGepaRunnerDirsFromArgs(forwardedArgs);
1829
1826
  forwardedArgs = verified.args;
1830
1827
  let result;
1828
+ let selectedEngine = 'official';
1831
1829
  try {
1832
1830
  // Uniform resolution (ALI-1090): honors ORIZU_TOKEN / ORIZU_TOKEN_FILE before
1833
1831
  // credentials.json so hosted sandboxes can run optimizations. Read at spawn
@@ -1838,33 +1836,29 @@ async function runGepaOptimization() {
1838
1836
  if (!forwardedArgs.includes('--project')) {
1839
1837
  forwardedArgs = ['--project', project, ...forwardedArgs];
1840
1838
  }
1841
- const pythonPathEntries = [
1842
- bundledPythonPath,
1843
- process.env.PYTHONPATH,
1844
- ].filter((entry) => Boolean(entry));
1845
- 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], {
1846
1850
  stdio: 'inherit',
1847
- env: {
1848
- ...process.env,
1849
- ORIZU_API_URL: baseUrl,
1850
- ORIZU_TOKEN: token,
1851
- ORIZU_PROJECT: project,
1852
- // Integrity handshake (ALI-1159): the Python entrypoint refuses
1853
- // runner dirs that are not in this wrapper-verified list.
1854
- ORIZU_VERIFIED_RUNNER_DIRS: JSON.stringify(verified.verifiedDirs),
1855
- PYTHONPATH: pythonPathEntries.join(delimiter),
1856
- PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
1857
- },
1851
+ env: dispatch.environment,
1858
1852
  });
1859
1853
  }
1860
1854
  finally {
1861
1855
  verified.cleanup();
1862
1856
  }
1863
1857
  if (result.error) {
1864
- throw result.error;
1858
+ throw new Error(`${selectedEngine} GEPA engine failed: ${result.error.message}`, { cause: result.error });
1865
1859
  }
1866
1860
  if (result.status !== 0) {
1867
- throw new Error(`orizu-gepa failed with exit code ${result.status}`);
1861
+ throw new Error(`${selectedEngine} GEPA engine failed with exit code ${result.status}`);
1868
1862
  }
1869
1863
  if (hasJsonFlag()) {
1870
1864
  printJson({ status: 'completed', exitCode: 0 });
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { basename } from 'path';
3
+ import { hasCanonicalReportMarkdownContent } from './report-markdown-blankness.js';
3
4
  export const MARKDOWN_REPORT_MAX_BYTES = 2 * 1024 * 1024;
4
5
  function getArg(args, name) {
5
6
  const index = args.indexOf(name);
@@ -17,6 +18,11 @@ function rejectDashPrefixedOptionValue(name, value) {
17
18
  throw new Error(`Invalid value for ${name}: option values cannot start with a dash`);
18
19
  }
19
20
  }
21
+ function rejectMissingOptionValue(args, name) {
22
+ if (args.some((arg, index) => arg === name && index + 1 >= args.length)) {
23
+ throw new Error(`${name} requires a value`);
24
+ }
25
+ }
20
26
  function readSourceFile(pathArg) {
21
27
  const expandedPath = expandHomePath(pathArg);
22
28
  try {
@@ -31,6 +37,14 @@ function readSourceFile(pathArg) {
31
37
  }
32
38
  }
33
39
  export function readMarkdownReportInput(args, reportLabel) {
40
+ if (args.some(arg => arg.startsWith('--report='))) {
41
+ throw new Error('use --report <markdown|@file> with a space, not =');
42
+ }
43
+ if (args.some(arg => arg.startsWith('--report-file='))) {
44
+ throw new Error('use --report-file <path> with a space, not =');
45
+ }
46
+ rejectMissingOptionValue(args, '--report');
47
+ rejectMissingOptionValue(args, '--report-file');
34
48
  const report = getArg(args, '--report');
35
49
  const reportFile = getArg(args, '--report-file');
36
50
  rejectDashPrefixedOptionValue('--report', report);
@@ -57,7 +71,7 @@ export function readMarkdownReportInput(args, reportLabel) {
57
71
  markdown = report || '';
58
72
  sourceName = 'inline';
59
73
  }
60
- if (!markdown.trim()) {
74
+ if (!hasCanonicalReportMarkdownContent(markdown)) {
61
75
  throw new Error(`${reportLabel} report markdown must not be blank`);
62
76
  }
63
77
  if (Buffer.byteLength(markdown, 'utf8') > MARKDOWN_REPORT_MAX_BYTES) {
@@ -19,9 +19,9 @@ export const CLI_LENGTH_MEASUREMENT_UNAVAILABLE_REASONS = [
19
19
  export function formatFiniteLengthCount(value, signed = false) {
20
20
  if (typeof value !== 'number' || !Number.isFinite(value))
21
21
  return null;
22
- const formatted = Math.abs(value).toLocaleString('en-US');
23
22
  if (!signed)
24
- return formatted;
23
+ return value.toLocaleString('en-US');
24
+ const formatted = Math.abs(value).toLocaleString('en-US');
25
25
  if (value > 0)
26
26
  return `+${formatted}`;
27
27
  if (value < 0)
@@ -27,7 +27,7 @@ export async function promptReportCommand(args, context) {
27
27
  rejectDashPrefixedOptionValue(name, value);
28
28
  }
29
29
  if (!prompt) {
30
- throw new Error('Usage: orizu prompts report set --prompt <id-or-name> --project <team/project> ' +
30
+ throw new Error('Usage: orizu prompts report set --prompt <id-or-name> [--project <team/project>] ' +
31
31
  '(--version <n> | --version-id <id>) ' +
32
32
  '(--report <markdown|@file> | --report-file <path>) [--json]');
33
33
  }
@@ -39,7 +39,7 @@ export async function promptReportCommand(args, context) {
39
39
  }
40
40
  const report = readMarkdownReportInput(args, 'Prompt');
41
41
  if (!report) {
42
- throw new Error('Usage: orizu prompts report set --prompt <id-or-name> --project <team/project> ' +
42
+ throw new Error('Usage: orizu prompts report set --prompt <id-or-name> [--project <team/project>] ' +
43
43
  '(--version <n> | --version-id <id>) ' +
44
44
  '(--report <markdown|@file> | --report-file <path>) [--json]');
45
45
  }
@@ -0,0 +1,6 @@
1
+ // JavaScript's \s is broader than the database locale's POSIX [:space:]
2
+ // predicate. These four code points are content in the measured PostgreSQL
3
+ // boundary, so retain them explicitly when validating CLI report input.
4
+ export function hasCanonicalReportMarkdownContent(markdown) {
5
+ return /(?:[^\s]|[\u00a0\u2007\u202f\ufeff])/u.test(markdown);
6
+ }