orizu 0.5.21 → 0.5.22

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 (41) hide show
  1. package/README.md +9 -6
  2. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  3. package/dist/gepa-engine-dispatch.js +20 -1
  4. package/dist/gepa-python-paths.d.ts.map +1 -1
  5. package/dist/gepa-python-paths.js +25 -6
  6. package/dist/help.d.ts.map +1 -1
  7. package/dist/help.js +36 -14
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -39
  10. package/dist/instruction-set-loader.d.ts.map +1 -1
  11. package/dist/instruction-set-loader.js +57 -5
  12. package/dist/instruction-sets-cli.d.ts +5 -2
  13. package/dist/instruction-sets-cli.d.ts.map +1 -1
  14. package/dist/instruction-sets-cli.js +134 -24
  15. package/dist/skilled-proposer-config.d.ts +2 -0
  16. package/dist/skilled-proposer-config.d.ts.map +1 -0
  17. package/dist/skilled-proposer-config.js +212 -0
  18. package/dist/skilled-proposer-launch.d.ts +4 -0
  19. package/dist/skilled-proposer-launch.d.ts.map +1 -1
  20. package/dist/skilled-proposer-launch.js +65 -1
  21. package/package.json +1 -1
  22. package/src/gepa-engine-dispatch.ts +21 -1
  23. package/src/gepa-python-paths.ts +24 -6
  24. package/src/help.ts +36 -14
  25. package/src/index.ts +6 -53
  26. package/src/instruction-set-loader.ts +55 -5
  27. package/src/instruction-sets-cli.ts +173 -19
  28. package/src/skilled-proposer-config.ts +191 -0
  29. package/src/skilled-proposer-launch.ts +59 -1
  30. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
  31. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
  32. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
  33. package/vendor/skills/orizu-cli/SKILL.md +50 -16
  34. package/vendor/skills/orizu-cli/references/building-judges.md +2 -2
  35. package/vendor/skills/orizu-cli/references/cli-reference.md +57 -28
  36. package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +120 -0
  37. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +109 -49
  38. package/vendor/skills/orizu-cli/references/optimization-reports.md +16 -11
  39. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +29 -28
  40. package/vendor/skills/orizu-cli/references/primer.md +21 -5
  41. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +132 -85
@@ -0,0 +1,191 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readFileSync, statSync } from 'node:fs'
3
+ import { basename, dirname, extname, relative, resolve, sep } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ interface NormalizedTextSource { source: string, content: string, sha256: string, byteLength: number }
7
+ interface NormalizedSkill extends NormalizedTextSource { name: string, description: string | null }
8
+
9
+ const CONFIG_KEYS = new Set(['schemaVersion', 'skills', 'additionalInstructionsFile', 'baseInstructionsFile',
10
+ 'maxWords', 'maxTokens', 'maxExamples', 'onError',
11
+ ])
12
+ const PATH_SKILL_KEYS = new Set(['path', 'name', 'description'])
13
+ const INLINE_SKILL_KEYS = new Set(['name', 'description', 'inline'])
14
+
15
+ function configError(field: string, detail: string): never { throw new Error(`--candidate-proposer-config ${field} ${detail}`) }
16
+ function isRecord(value: unknown): value is Record<string, unknown> {
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
18
+ }
19
+ function rejectUnknownKeys(value: Record<string, unknown>, allowed: Set<string>, field = ''): void {
20
+ const unknown = Object.keys(value).find(key => !allowed.has(key))
21
+ if (unknown) configError(field ? `${field}.${unknown}` : unknown, 'is not supported')
22
+ }
23
+ function sha256(bytes: Uint8Array): string { return createHash('sha256').update(bytes).digest('hex') }
24
+ function decodeUtf8(bytes: Uint8Array, field: string): string {
25
+ try {
26
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes)
27
+ } catch {
28
+ configError(field, 'must contain valid UTF-8 text')
29
+ }
30
+ }
31
+ function readRegularFile(path: string, field: string): Buffer {
32
+ try {
33
+ if (!statSync(path).isFile()) configError(field, 'must resolve to a file')
34
+ return readFileSync(path)
35
+ } catch (error) {
36
+ if (error instanceof Error && error.message.startsWith('--candidate-proposer-config')) throw error
37
+ configError(field, `is missing or unreadable: ${error instanceof Error ? error.message : String(error)}`)
38
+ }
39
+ }
40
+ function logicalSource(configDirectory: string, path: string): string {
41
+ return relative(configDirectory, path).split(sep).join('/') || basename(path)
42
+ }
43
+ function normalizedText(bytes: Buffer, source: string, field: string): NormalizedTextSource {
44
+ const content = decodeUtf8(bytes, field)
45
+ if (!content.trim()) configError(field, 'must not be empty')
46
+ return { source, content, sha256: sha256(bytes), byteLength: bytes.byteLength }
47
+ }
48
+ function parseFrontmatter(content: string, field: string): { name?: string, description?: string, body: string } {
49
+ const lines = content.split(/\r?\n/)
50
+ if (lines[0]?.replace(/^\uFEFF/, '').trim() !== '---') return { body: content }
51
+ const end = lines.slice(1).findIndex(line => line.trim() === '---')
52
+ if (end < 0) configError(field, 'has malformed frontmatter without a closing ---')
53
+ const metadata: Record<string, string> = {}
54
+ for (const raw of lines.slice(1, end + 1)) {
55
+ if (/^[ \t]/.test(raw) || !raw.includes(':')) continue
56
+ const colon = raw.indexOf(':')
57
+ metadata[raw.slice(0, colon).trim()] = raw.slice(colon + 1).trim().replace(/^(['"])(.*)\1$/, '$2')
58
+ }
59
+ return {
60
+ name: metadata.name || undefined,
61
+ description: metadata.description || undefined,
62
+ body: lines.slice(end + 2).join('\n'),
63
+ }
64
+ }
65
+ function wellFormedString(value: string, field: string): string {
66
+ if (!(value as string & { isWellFormed(): boolean }).isWellFormed()) {
67
+ configError(field, 'must contain well-formed Unicode text')
68
+ }
69
+ return value
70
+ }
71
+ function nonEmptyString(value: unknown, field: string): string {
72
+ if (typeof value !== 'string' || !value.trim()) configError(field, 'must be a non-empty string')
73
+ return wellFormedString(value, field)
74
+ }
75
+ function nullableDescription(value: unknown, field: string): string | null {
76
+ if (value === null) return null
77
+ if (typeof value !== 'string') configError(field, 'must be a string or null')
78
+ return wellFormedString(value, field) || null
79
+ }
80
+ function normalizePathSkill(entry: Record<string, unknown>, index: number, configDirectory: string): NormalizedSkill {
81
+ const field = `skills[${index}]`
82
+ rejectUnknownKeys(entry, PATH_SKILL_KEYS, field)
83
+ const configuredPath = nonEmptyString(entry.path, `${field}.path`)
84
+ let resolvedPath = resolve(configDirectory, configuredPath)
85
+ try {
86
+ if (statSync(resolvedPath).isDirectory()) {
87
+ resolvedPath = resolve(resolvedPath, 'SKILL.md')
88
+ try {
89
+ if (!statSync(resolvedPath).isFile()) configError(`${field}.path`, 'directory must contain SKILL.md')
90
+ } catch (error) {
91
+ if (error instanceof Error && error.message.startsWith('--candidate-proposer-config')) throw error
92
+ configError(`${field}.path`, 'directory must contain SKILL.md')
93
+ }
94
+ }
95
+ } catch (error) {
96
+ if (error instanceof Error && error.message.startsWith('--candidate-proposer-config')) throw error
97
+ configError(`${field}.path`, 'is missing or unreadable')
98
+ }
99
+ const text = normalizedText(readRegularFile(resolvedPath, `${field}.path`),
100
+ logicalSource(configDirectory, resolvedPath), `${field}.path`)
101
+ const frontmatter = parseFrontmatter(text.content, `${field}.path`)
102
+ if (!frontmatter.body.trim()) configError(`${field}.path`, 'must contain non-empty skill content')
103
+ const fallbackName = extname(resolvedPath)
104
+ ? basename(resolvedPath, extname(resolvedPath))
105
+ : basename(resolvedPath)
106
+ const name = entry.name === undefined
107
+ ? frontmatter.name ?? (basename(resolvedPath) === 'SKILL.md' ? basename(dirname(resolvedPath)) : fallbackName)
108
+ : nonEmptyString(entry.name, `${field}.name`)
109
+ const description = entry.description === undefined
110
+ ? frontmatter.description ?? null
111
+ : nullableDescription(entry.description, `${field}.description`)
112
+ return { name, description, ...text }
113
+ }
114
+ function normalizeInlineSkill(entry: Record<string, unknown>, index: number): NormalizedSkill {
115
+ const field = `skills[${index}]`
116
+ rejectUnknownKeys(entry, INLINE_SKILL_KEYS, field)
117
+ const name = nonEmptyString(entry.name, `${field}.name`)
118
+ const description = nonEmptyString(entry.description, `${field}.description`)
119
+ const content = nonEmptyString(entry.inline, `${field}.inline`)
120
+ const bytes = Buffer.from(content, 'utf8')
121
+ return { name, description, source: `inline:${name}`, content,
122
+ sha256: sha256(bytes), byteLength: bytes.byteLength }
123
+ }
124
+ function normalizeSkill(value: unknown, index: number, configDirectory: string): NormalizedSkill {
125
+ const field = `skills[${index}]`
126
+ if (!isRecord(value)) configError(field, 'must be an object')
127
+ const hasPath = Object.hasOwn(value, 'path')
128
+ const hasInline = Object.hasOwn(value, 'inline')
129
+ if (hasPath === hasInline) configError(field, 'must contain exactly one of path or inline')
130
+ return hasPath ? normalizePathSkill(value, index, configDirectory) : normalizeInlineSkill(value, index)
131
+ }
132
+ function optionalPositiveInteger(value: unknown, field: string): number | null {
133
+ if (value === undefined || value === null) return null
134
+ if (!Number.isSafeInteger(value) || (value as number) <= 0) configError(field, 'must be a positive integer or null')
135
+ return value as number
136
+ }
137
+ function normalizeInstructionFile(value: unknown, field: string, configDirectory: string): NormalizedTextSource | null {
138
+ if (value === undefined || value === null) return null
139
+ const configuredPath = nonEmptyString(value, field)
140
+ const resolvedPath = resolve(configDirectory, configuredPath)
141
+ return normalizedText(readRegularFile(resolvedPath, field), logicalSource(configDirectory, resolvedPath), field)
142
+ }
143
+ function lockedPackageVersion(): string {
144
+ const lockPath = fileURLToPath(new URL('../requirements/skilled-proposer.lock', import.meta.url))
145
+ const match = /^skilled-proposer==([^\s\\]+)/m.exec(readFileSync(lockPath, 'utf8'))
146
+ if (!match) throw new Error('ALI_1505_SKILLED_PROPOSER_LOCK_VERSION_MISSING')
147
+ return match[1]
148
+ }
149
+ export function normalizedSkilledProposerConfig(value: string): string {
150
+ const suppliedPath = nonEmptyString(value.startsWith('@') ? value.slice(1) : value, 'path')
151
+ const configPath = resolve(suppliedPath)
152
+ const configBytes = readRegularFile(configPath, 'path')
153
+ const raw = decodeUtf8(configBytes, 'path').replace(/^\uFEFF/, '')
154
+ let parsed: unknown
155
+ try {
156
+ parsed = JSON.parse(raw)
157
+ } catch (error) {
158
+ configError('path', `must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`)
159
+ }
160
+ if (!isRecord(parsed)) configError('path', 'must contain a JSON object')
161
+ rejectUnknownKeys(parsed, CONFIG_KEYS)
162
+ if (parsed.schemaVersion !== 1) configError('schemaVersion', 'must be 1')
163
+ if (!Array.isArray(parsed.skills)) configError('skills', 'must be an array')
164
+
165
+ const configDirectory = dirname(configPath)
166
+ const skills = parsed.skills.map((skill, index) => normalizeSkill(skill, index, configDirectory))
167
+ const names = new Set<string>()
168
+ for (const skill of skills) {
169
+ if (names.has(skill.name)) configError('skills', `contains duplicate resolved name ${skill.name}`)
170
+ names.add(skill.name)
171
+ }
172
+ const onError = parsed.onError ?? 'keep'
173
+ if (onError !== 'keep' && onError !== 'raise') configError('onError', 'must be keep or raise')
174
+
175
+ const normalized = {
176
+ schemaVersion: 1,
177
+ implementation: 'cmpnd-ai/skilled-proposer',
178
+ packageVersion: lockedPackageVersion(),
179
+ skills,
180
+ additionalInstructions: normalizeInstructionFile(parsed.additionalInstructionsFile,
181
+ 'additionalInstructionsFile', configDirectory),
182
+ baseInstructions: normalizeInstructionFile(parsed.baseInstructionsFile, 'baseInstructionsFile', configDirectory),
183
+ maxWords: optionalPositiveInteger(parsed.maxWords, 'maxWords'),
184
+ maxTokens: optionalPositiveInteger(parsed.maxTokens, 'maxTokens'),
185
+ maxExamples: optionalPositiveInteger(parsed.maxExamples, 'maxExamples'),
186
+ onError,
187
+ }
188
+ const configFileSha256 = sha256(configBytes)
189
+ const configSha256 = sha256(Buffer.from(JSON.stringify({ configFileSha256, ...normalized }), 'utf8'))
190
+ return JSON.stringify({ ...normalized, configFileSha256, configSha256 })
191
+ }
@@ -1,4 +1,6 @@
1
- import { spawnSync } from 'child_process'
1
+ import { spawn, spawnSync } from 'child_process'
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'fs'
3
+ import { tmpdir } from 'os'
2
4
  import { join } from 'path'
3
5
  import { fileURLToPath } from 'url'
4
6
 
@@ -20,6 +22,62 @@ export interface SkilledProposerLaunch {
20
22
  environment: NodeJS.ProcessEnv
21
23
  }
22
24
 
25
+ function materializeConfigPayload(engine: GepaEngine, environment: NodeJS.ProcessEnv): (() => void) | undefined {
26
+ const payload = environment.ORIZU_SKILLED_PROPOSER_CONFIG
27
+ if (engine !== 'official' || environment.ORIZU_CANDIDATE_PROPOSER !== 'skilled-proposer' || payload === undefined) return undefined
28
+ const root = mkdtempSync(join(tmpdir(), 'orizu-skilled-proposer-payload-'))
29
+ try {
30
+ const path = join(root, 'config.json')
31
+ writeFileSync(path, payload, { encoding: 'utf8', mode: 0o600 })
32
+ environment.ORIZU_SKILLED_PROPOSER_CONFIG = `@${path}`
33
+ } catch (error) {
34
+ rmSync(root, { recursive: true, force: true })
35
+ throw error
36
+ }
37
+ return () => rmSync(root, { recursive: true, force: true })
38
+ }
39
+
40
+ export async function spawnSkilledProposerChild(
41
+ command: string, args: string[], engine: GepaEngine, environment: NodeJS.ProcessEnv,
42
+ ) {
43
+ const childEnvironment = { ...environment }
44
+ const shouldMaterialize = engine === 'official'
45
+ && childEnvironment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer'
46
+ && childEnvironment.ORIZU_SKILLED_PROPOSER_CONFIG !== undefined
47
+ if (!shouldMaterialize) return spawnSync(command, args, { stdio: 'inherit', env: childEnvironment })
48
+
49
+ let child: ReturnType<typeof spawn> | undefined
50
+ let cleanup = () => {}
51
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'] as const
52
+ const handlers = signals.map(signal => ({
53
+ signal,
54
+ handle: () => {
55
+ try { cleanup() }
56
+ finally {
57
+ try { child?.kill(signal) }
58
+ finally {
59
+ for (const entry of handlers) process.off(entry.signal, entry.handle)
60
+ process.kill(process.pid, signal)
61
+ }
62
+ }
63
+ },
64
+ }))
65
+ for (const entry of handlers) process.once(entry.signal, entry.handle)
66
+ try {
67
+ cleanup = materializeConfigPayload(engine, childEnvironment) ?? cleanup
68
+ await new Promise<void>(resolve => setImmediate(resolve))
69
+ return await new Promise<{ status: number | null, error?: Error }>(resolve => {
70
+ let error: Error | undefined
71
+ child = spawn(command, args, { stdio: 'inherit', env: childEnvironment })
72
+ child.once('error', cause => { error = cause })
73
+ child.once('close', status => resolve(error ? { status, error } : { status }))
74
+ })
75
+ } finally {
76
+ for (const entry of handlers) process.off(entry.signal, entry.handle)
77
+ cleanup()
78
+ }
79
+ }
80
+
23
81
  function ensureSelectedSkilledProposerVenv(
24
82
  python: string,
25
83
  vendoredGepaPath: string,
@@ -219,6 +219,32 @@ def run_official_gepa(
219
219
  owned_stop_callbacks = list(stop_callbacks)
220
220
  if proposal_budget is not None:
221
221
  owned_stop_callbacks.append(ProposalBudgetStopper(proposal_budget))
222
+ proposer_failures: list[Exception] = []
223
+ gepa_candidate_proposer = custom_candidate_proposer
224
+ if custom_candidate_proposer is not None:
225
+ delegate = custom_candidate_proposer
226
+
227
+ def capture_proposer_failure(
228
+ candidate: dict[str, str], reflective_dataset: Any, components_to_update: list[str],
229
+ ) -> dict[str, str]:
230
+ if proposer_failures:
231
+ raise proposer_failures[0]
232
+ try:
233
+ return delegate(candidate, reflective_dataset, components_to_update)
234
+ except Exception as error:
235
+ if not proposer_failures:
236
+ proposer_failures.append(error)
237
+ raise
238
+
239
+ gepa_candidate_proposer = capture_proposer_failure
240
+ def raise_proposer_failure(_state: Any) -> bool:
241
+ if proposer_failures:
242
+ raise proposer_failures[0]
243
+ return False
244
+
245
+ # Failure must win over every normal stopper in CompositeStopper(any),
246
+ # otherwise a simultaneous budget stop emits terminal success first.
247
+ owned_stop_callbacks.insert(0, raise_proposer_failure)
222
248
 
223
249
  reflection_minibatch_size = config.minibatch_size
224
250
  if custom_candidate_proposer is not None and max_metric_calls is not None:
@@ -252,11 +278,18 @@ def run_official_gepa(
252
278
  # The connector implements GEPA's component-aware strategy protocol;
253
279
  # passing it as a bare callable would discard the selected component.
254
280
  kwargs["reflection_strategy"] = reflection_lm
255
- if custom_candidate_proposer is not None:
256
- kwargs["custom_candidate_proposer"] = custom_candidate_proposer
281
+ if gepa_candidate_proposer is not None:
282
+ kwargs["custom_candidate_proposer"] = gepa_candidate_proposer
257
283
  kwargs["trainset"] = _with_row_identities(trainset)
258
284
  kwargs["valset"] = _with_row_identities(valset)
259
- result = optimize(**kwargs)
285
+ try:
286
+ result = optimize(**kwargs)
287
+ except Exception:
288
+ if proposer_failures:
289
+ raise proposer_failures[0]
290
+ raise
291
+ if proposer_failures:
292
+ raise proposer_failures[0]
260
293
  if custom_candidate_proposer is not None and preflight_evaluation is not None:
261
294
  # GEPA's state correctly counts its seed evaluation, but this run
262
295
  # served that evaluation from the already-accounted launch preflight.
@@ -18,10 +18,38 @@ def _inside(root: Path, candidate: Path) -> bool:
18
18
  return False
19
19
 
20
20
 
21
- def load_instruction_set(directory: str, name: str, model_config_identity: str) -> dict[str, str]:
22
- root = (Path(directory) / name).resolve()
23
- if not root.is_dir():
21
+ def _resolve_set_root(directory: str, reference: str) -> Path:
22
+ directory_root = Path(directory).resolve()
23
+ if not directory_root.is_dir():
24
24
  raise InstructionSetLoaderError("instruction_set_not_synced")
25
+ direct = (directory_root / reference).resolve()
26
+ if _inside(directory_root, direct) and direct.is_dir():
27
+ try:
28
+ direct_manifest = json.loads((direct / "manifest.json").read_text())
29
+ except (OSError, json.JSONDecodeError):
30
+ # Preserve the exact-path loader's specific missing/invalid manifest error.
31
+ return direct
32
+ if direct_manifest.get("name") == reference or direct_manifest.get("slug") == reference:
33
+ return direct
34
+ matches: list[Path] = []
35
+ for child in directory_root.iterdir():
36
+ candidate = child.resolve()
37
+ if child.name.startswith(".") or not child.is_dir() or not _inside(directory_root, candidate):
38
+ continue
39
+ try:
40
+ manifest = json.loads((candidate / "manifest.json").read_text())
41
+ except (OSError, json.JSONDecodeError):
42
+ continue
43
+ if manifest.get("name") == reference or manifest.get("slug") == reference:
44
+ matches.append(candidate)
45
+ if len(matches) != 1:
46
+ code = "instruction_set_reference_ambiguous" if len(matches) > 1 else "instruction_set_not_synced"
47
+ raise InstructionSetLoaderError(code)
48
+ return matches[0]
49
+
50
+
51
+ def load_instruction_set(directory: str, name: str, model_config_identity: str) -> dict[str, str]:
52
+ root = _resolve_set_root(directory, name)
25
53
  manifest_path = root / "manifest.json"
26
54
  if not manifest_path.is_file():
27
55
  raise InstructionSetLoaderError("instruction_set_manifest_missing")