orizu 0.5.21 → 0.5.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +46 -15
  2. package/dist/cleanup.d.ts +6 -0
  3. package/dist/cleanup.d.ts.map +1 -0
  4. package/dist/cleanup.js +27 -0
  5. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  6. package/dist/gepa-engine-dispatch.js +20 -1
  7. package/dist/gepa-python-paths.d.ts.map +1 -1
  8. package/dist/gepa-python-paths.js +25 -6
  9. package/dist/help.d.ts.map +1 -1
  10. package/dist/help.js +37 -15
  11. package/dist/hosted-commands.d.ts +13 -0
  12. package/dist/hosted-commands.d.ts.map +1 -0
  13. package/dist/hosted-commands.js +10 -0
  14. package/dist/hosted-gepa-cli.d.ts +11 -0
  15. package/dist/hosted-gepa-cli.d.ts.map +1 -0
  16. package/dist/hosted-gepa-cli.js +110 -0
  17. package/dist/hosted-gepa-numeric-values.d.ts +3 -0
  18. package/dist/hosted-gepa-numeric-values.d.ts.map +1 -0
  19. package/dist/hosted-gepa-numeric-values.js +49 -0
  20. package/dist/hosted-provider-settings.d.ts +3 -0
  21. package/dist/hosted-provider-settings.d.ts.map +1 -0
  22. package/dist/hosted-provider-settings.js +69 -0
  23. package/dist/hosted-session-cli.d.ts.map +1 -1
  24. package/dist/hosted-session-cli.js +6 -1
  25. package/dist/http.d.ts +1 -0
  26. package/dist/http.d.ts.map +1 -1
  27. package/dist/http.js +7 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +72 -74
  30. package/dist/instruction-set-loader.d.ts.map +1 -1
  31. package/dist/instruction-set-loader.js +57 -5
  32. package/dist/instruction-sets-cli.d.ts +5 -2
  33. package/dist/instruction-sets-cli.d.ts.map +1 -1
  34. package/dist/instruction-sets-cli.js +134 -24
  35. package/dist/optimizations-promote-cli.d.ts +3 -0
  36. package/dist/optimizations-promote-cli.d.ts.map +1 -1
  37. package/dist/optimizations-promote-cli.js +75 -7
  38. package/dist/runner-dir-verify.d.ts.map +1 -1
  39. package/dist/runner-dir-verify.js +4 -7
  40. package/dist/skilled-proposer-bake-report.d.ts +2 -0
  41. package/dist/skilled-proposer-bake-report.d.ts.map +1 -0
  42. package/dist/skilled-proposer-bake-report.js +1 -0
  43. package/dist/skilled-proposer-config.d.ts +2 -0
  44. package/dist/skilled-proposer-config.d.ts.map +1 -0
  45. package/dist/skilled-proposer-config.js +212 -0
  46. package/dist/skilled-proposer-launch.d.ts +26 -0
  47. package/dist/skilled-proposer-launch.d.ts.map +1 -1
  48. package/dist/skilled-proposer-launch.js +103 -6
  49. package/package.json +1 -1
  50. package/scripts/vendor-gepa-python.mjs +81 -12
  51. package/src/cleanup.ts +37 -0
  52. package/src/gepa-engine-dispatch.ts +21 -1
  53. package/src/gepa-python-paths.ts +24 -6
  54. package/src/help.ts +37 -15
  55. package/src/hosted-commands.ts +4 -0
  56. package/src/hosted-gepa-cli.ts +107 -0
  57. package/src/hosted-gepa-numeric-values.ts +60 -0
  58. package/src/hosted-provider-settings.ts +69 -0
  59. package/src/hosted-session-cli.ts +4 -1
  60. package/src/http.ts +8 -0
  61. package/src/index.ts +112 -89
  62. package/src/instruction-set-loader.ts +55 -5
  63. package/src/instruction-sets-cli.ts +173 -19
  64. package/src/optimizations-promote-cli.ts +85 -7
  65. package/src/runner-dir-verify.ts +11 -6
  66. package/src/skilled-proposer-bake-report.ts +1 -0
  67. package/src/skilled-proposer-config.ts +191 -0
  68. package/src/skilled-proposer-launch.ts +96 -7
  69. package/src/skilled-proposer-venv-manager.mjs +44 -27
  70. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
  71. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
  72. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +7 -2
  73. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
  74. package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
  75. package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +15 -0
  76. package/vendor/skills/orizu-cli/SKILL.md +94 -168
  77. package/vendor/skills/orizu-cli/references/assess-and-plan.md +107 -0
  78. package/vendor/skills/orizu-cli/references/building-judges.md +442 -180
  79. package/vendor/skills/orizu-cli/references/cli-reference.md +69 -39
  80. package/vendor/skills/orizu-cli/references/dataset-design.md +100 -0
  81. package/vendor/skills/orizu-cli/references/eval-strategy.md +64 -0
  82. package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +115 -0
  83. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +150 -82
  84. package/vendor/skills/orizu-cli/references/optimization-reports.md +196 -104
  85. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +57 -37
  86. package/vendor/skills/orizu-cli/references/primer.md +43 -18
  87. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +199 -131
@@ -1,34 +1,97 @@
1
- import { spawnSync } from 'child_process'
1
+ import { spawn, spawnSync } from 'child_process'
2
+ import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'fs'
3
+ import { tmpdir } from 'os'
2
4
  import { join } from 'path'
3
5
  import { fileURLToPath } from 'url'
4
6
 
5
7
  import type { GepaEngine } from './gepa-engine-dispatch.js'
6
- import { bundledOfficialGepaPythonPath } from './gepa-python-paths.js'
8
+ import { bundledOfficialGepaPythonPath, getGepaPythonPathEntries } from './gepa-python-paths.js'
7
9
  import { sanitizeTerminalText } from './json-response.js'
10
+ import { SKILLED_PROPOSER_BAKE_REPORT_FIELDS } from './skilled-proposer-bake-report.js'
8
11
 
9
12
  interface SkilledProposerVenvReport {
10
13
  venv: string
14
+ publishKey: string
11
15
  sslCertFile: string | null
12
16
  pythonPathEntries: string[]
13
- warnings?: Array<{ code: string, message: string }>
17
+ warnings: Array<{ code: string, message: string }>
18
+ waitedForPublishLock: boolean
14
19
  error?: string
15
20
  message?: string
21
+ publishedVenv?: boolean | null
22
+ executedPipArgv?: string[] | null
16
23
  }
17
-
18
24
  export interface SkilledProposerLaunch {
19
25
  python: string
20
26
  environment: NodeJS.ProcessEnv
27
+ report?: SkilledProposerVenvReport
28
+ }
29
+
30
+ function materializeConfigPayload(engine: GepaEngine, environment: NodeJS.ProcessEnv): (() => void) | undefined {
31
+ const payload = environment.ORIZU_SKILLED_PROPOSER_CONFIG
32
+ if (engine !== 'official' || environment.ORIZU_CANDIDATE_PROPOSER !== 'skilled-proposer' || payload === undefined) return undefined
33
+ const root = mkdtempSync(join(tmpdir(), 'orizu-skilled-proposer-payload-'))
34
+ try {
35
+ const path = join(root, 'config.json')
36
+ writeFileSync(path, payload, { encoding: 'utf8', mode: 0o600 })
37
+ environment.ORIZU_SKILLED_PROPOSER_CONFIG = `@${path}`
38
+ } catch (error) {
39
+ rmSync(root, { recursive: true, force: true })
40
+ throw error
41
+ }
42
+ return () => rmSync(root, { recursive: true, force: true })
43
+ }
44
+
45
+ export async function spawnSkilledProposerChild(
46
+ command: string, args: string[], engine: GepaEngine, environment: NodeJS.ProcessEnv,
47
+ ) {
48
+ const childEnvironment = { ...environment }
49
+ const shouldMaterialize = engine === 'official'
50
+ && childEnvironment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer'
51
+ && childEnvironment.ORIZU_SKILLED_PROPOSER_CONFIG !== undefined
52
+ if (!shouldMaterialize) return spawnSync(command, args, { stdio: 'inherit', env: childEnvironment })
53
+
54
+ let child: ReturnType<typeof spawn> | undefined
55
+ let cleanup = () => {}
56
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'] as const
57
+ const handlers = signals.map(signal => ({
58
+ signal,
59
+ handle: () => {
60
+ try { cleanup() }
61
+ finally {
62
+ try { child?.kill(signal) }
63
+ finally {
64
+ for (const entry of handlers) process.off(entry.signal, entry.handle)
65
+ process.kill(process.pid, signal)
66
+ }
67
+ }
68
+ },
69
+ }))
70
+ for (const entry of handlers) process.once(entry.signal, entry.handle)
71
+ try {
72
+ cleanup = materializeConfigPayload(engine, childEnvironment) ?? cleanup
73
+ await new Promise<void>(resolve => setImmediate(resolve))
74
+ return await new Promise<{ status: number | null, error?: Error }>(resolve => {
75
+ let error: Error | undefined
76
+ child = spawn(command, args, { stdio: 'inherit', env: childEnvironment })
77
+ child.once('error', cause => { error = cause })
78
+ child.once('close', status => resolve(error ? { status, error } : { status }))
79
+ })
80
+ } finally {
81
+ for (const entry of handlers) process.off(entry.signal, entry.handle)
82
+ cleanup()
83
+ }
21
84
  }
22
85
 
23
86
  function ensureSelectedSkilledProposerVenv(
24
87
  python: string,
25
88
  vendoredGepaPath: string,
89
+ cacheRoot = existsSync('/opt/orizu/prebaked.json')
90
+ ? '/opt/orizu/cache/skilled-proposer'
91
+ : join(process.cwd(), '.orizu', 'cache', 'skilled-proposer'),
26
92
  ): SkilledProposerVenvReport {
27
93
  const manager = fileURLToPath(new URL('../scripts/ensure-skilled-proposer-venv.mjs', import.meta.url))
28
94
  const lock = fileURLToPath(new URL('../requirements/skilled-proposer.lock', import.meta.url))
29
- // This is a regenerable workspace cache. The manager's publish key further
30
- // separates interpreters, platforms, and lock revisions inside it.
31
- const cacheRoot = join(process.cwd(), '.orizu', 'cache', 'skilled-proposer')
32
95
  const result = spawnSync(
33
96
  process.execPath,
34
97
  [manager, '--python', python, '--cache-root', cacheRoot, '--lock', lock,
@@ -58,6 +121,31 @@ function ensureSelectedSkilledProposerVenv(
58
121
  return report
59
122
  }
60
123
 
124
+ interface BakeCommandIo { print: (value: string) => void; printErr?: (value: string) => void }
125
+ const packageRoot = join(fileURLToPath(new URL('.', import.meta.url)), '..')
126
+ const managerPath = join(packageRoot, 'scripts/ensure-skilled-proposer-venv.mjs')
127
+ const lockPath = join(packageRoot, 'requirements/skilled-proposer.lock')
128
+ const vendoredGepaPath = join(packageRoot, 'vendor/gepa-python/src')
129
+ const packagedPythonPathEntries = ['vendor/orizu-gepa/src', 'vendor/orizu-gepa-python/src', 'vendor/gepa-python/src'].map(path => join(packageRoot, path))
130
+ const bakedCacheRoot = '/opt/orizu/cache/skilled-proposer'
131
+
132
+ export function skilledProposerBakeCommand(io: BakeCommandIo, verify = false): number {
133
+ try {
134
+ const python = process.env.PYTHON || 'python3'
135
+ if (!verify) { io.print(JSON.stringify(ensureSelectedSkilledProposerVenv(python, vendoredGepaPath, bakedCacheRoot))); return 0 }
136
+ if (!readdirSync(join(bakedCacheRoot, 'venvs'), { withFileTypes: true }).some(entry => entry.isDirectory() && existsSync(join(bakedCacheRoot, 'venvs', entry.name, '.orizu-skilled-proposer-venv.json')))) throw new Error('ALI_1588_VENV_MARKER_MISSING')
137
+ const environment = { ...process.env, ORIZU_CANDIDATE_PROPOSER: 'skilled-proposer' }
138
+ const launch = prepareSkilledProposerLaunch(python, 'official', environment)
139
+ const report = launch.report!
140
+ if (report.publishedVenv !== false || report.executedPipArgv !== null) throw new Error('ALI_1588_DEPENDENCY_SETUP_NOT_REUSED')
141
+ const pythonPathEntries = getGepaPythonPathEntries(undefined)
142
+ if (JSON.stringify(pythonPathEntries) !== JSON.stringify(packagedPythonPathEntries)) throw new Error('ALI_1588_PYTHON_ROOT_MISSING')
143
+ const completeReport = { ...report, pythonPathEntries, marker: 'ALI_1588_DEPENDENCY_SETUP_REUSED', python: launch.python, packageRoot, manager: managerPath, lock: lockPath, vendoredGepaPath, launcherVenv: launch.environment.ORIZU_SKILLED_PROPOSER_VENV ?? null, launcherSslCertFile: launch.environment.SSL_CERT_FILE ?? null }
144
+ io.print(JSON.stringify(Object.fromEntries(SKILLED_PROPOSER_BAKE_REPORT_FIELDS.map(key => [key, completeReport[key]]))))
145
+ return 0
146
+ } catch (error) { io.printErr?.(`${error instanceof Error ? error.message : String(error)}\n`); return 1 }
147
+ }
148
+
61
149
  export function prepareSkilledProposerLaunch(
62
150
  python: string,
63
151
  engine: GepaEngine,
@@ -85,5 +173,6 @@ export function prepareSkilledProposerLaunch(
85
173
  ? join(report.venv, 'Scripts', 'python.exe')
86
174
  : join(report.venv, 'bin', 'python'),
87
175
  environment,
176
+ report,
88
177
  }
89
178
  }
@@ -100,15 +100,14 @@ finally:
100
100
  const MANAGER_SCHEMA_VERSION = 1
101
101
  const MANIFEST_NAME = '.orizu-skilled-proposer-venv.json'
102
102
  const LOCK_WAIT_MILLISECONDS = 25
103
- // This exceeds the longest repair path while holding the lock: existing-cache
104
- // validation (2m) + venv (2m) + install (10m) + staged validation (2m), plus
105
- // cleanup/publication/release margin.
106
- const LOCK_TIMEOUT_MILLISECONDS = 20 * 60 * 1000
107
- const PYTHON_PROBE_TIMEOUT_MILLISECONDS = 15_000
103
+ export const LOCK_TIMEOUT_MILLISECONDS = 20 * 60 * 1000
104
+ export const PYTHON_PROBE_TIMEOUT_MILLISECONDS = 15_000
105
+ export const LOCK_RELEASE_TIMEOUT_MILLISECONDS = 15_000
108
106
  const VENV_CREATE_TIMEOUT_MILLISECONDS = 2 * 60 * 1000
109
- const VALIDATION_TIMEOUT_MILLISECONDS = 60_000
107
+ export const VALIDATION_TIMEOUT_MILLISECONDS = 60_000
110
108
  const PIP_INSTALL_TIMEOUT_MILLISECONDS = 10 * 60 * 1000
111
109
  const TEST_BARRIER_TIMEOUT_MILLISECONDS = 60_000
110
+ export const SKILLED_PROPOSER_MANAGER_MAXIMUM_MILLISECONDS = (2 * PYTHON_PROBE_TIMEOUT_MILLISECONDS) + LOCK_RELEASE_TIMEOUT_MILLISECONDS + LOCK_TIMEOUT_MILLISECONDS + VENV_CREATE_TIMEOUT_MILLISECONDS + PIP_INSTALL_TIMEOUT_MILLISECONDS + (6 * VALIDATION_TIMEOUT_MILLISECONDS) + (2 * 60 * 1000)
112
111
  const ABANDONED_STAGE_MAX_AGE_MILLISECONDS = 24 * 60 * 60 * 1000
113
112
  const SSL_CERT_FILE_UNRESOLVED_WARNING = {
114
113
  code: 'ALI_1505_SSL_CERT_FILE_UNRESOLVED',
@@ -203,11 +202,6 @@ function versionAtLeast(actual, required) {
203
202
  }
204
203
 
205
204
  function assertSupportedWheelTarget(identity) {
206
- // `platform.libc_ver()` is not merely cache identity on Linux: it is also a
207
- // conservative admission check for the locked glibc wheel matrix. A bad
208
- // guess can refuse a compatible host (named, before mutation), but cannot
209
- // admit an sdist or make pip install an incompatible wheel because the
210
- // customer install remains hash-locked and binary-only.
211
205
  const machine = String(identity.machine).toLowerCase()
212
206
  const isSupportedMac = identity.system === 'Darwin'
213
207
  && machine === 'arm64'
@@ -272,7 +266,7 @@ export function characterizePython(python, probeEnv = isolatedPythonProbeEnv())
272
266
  }
273
267
  }
274
268
 
275
- function readAndValidateHashLock(lockPath) {
269
+ export function readAndValidateHashLock(lockPath) {
276
270
  let contents
277
271
  try {
278
272
  contents = readFileSync(lockPath, 'utf8')
@@ -305,7 +299,8 @@ function readAndValidateHashLock(lockPath) {
305
299
  )
306
300
  }
307
301
  for (const requirement of requirements) {
308
- const isExactPin = /^[A-Za-z0-9_.-]+(?:\[[^\]]+\])?==[^\s;]+/.test(requirement)
302
+ const requirementSpecifier = requirement.split(/\s+--hash=/, 1)[0].trim()
303
+ const isExactPin = /^[A-Za-z0-9_.-]+(?:\[[^\]]+\])?==[^\s,;*]+(?:\s*;\s*.+)?$/.test(requirementSpecifier)
309
304
  const hashes = [...requirement.matchAll(/(?:^|\s)--hash=([^\s]+)/g)].map(match => match[1])
310
305
  const hashTokens = [...requirement.matchAll(/(?:^|\s)(--hash(?:=[^\s]*)?)(?=\s|$)/g)]
311
306
  .map(match => match[1])
@@ -343,7 +338,7 @@ function resolveSslCertFile(pythonIdentity, probeEnv) {
343
338
  return null
344
339
  }
345
340
 
346
- function buildPublishKey(pythonIdentity, lockContents) {
341
+ export function buildPublishKey(pythonIdentity, lockContents) {
347
342
  const lockDigest = createHash('sha256').update(lockContents).digest('hex')
348
343
  const identity = {
349
344
  managerSchemaVersion: MANAGER_SCHEMA_VERSION,
@@ -355,7 +350,6 @@ function buildPublishKey(pythonIdentity, lockContents) {
355
350
  soabi: pythonIdentity.soabi,
356
351
  system: pythonIdentity.system,
357
352
  machine: pythonIdentity.machine,
358
- osVersion: pythonIdentity.osVersion,
359
353
  libc: pythonIdentity.libc,
360
354
  lockDigest,
361
355
  }
@@ -523,21 +517,35 @@ function acquirePublishLock(python, lockFile, env) {
523
517
  })
524
518
  }
525
519
 
526
- async function releasePublishLock(lock) {
520
+ export async function releasePublishLock(lock) {
527
521
  if (!lock?.child) return
522
+ const deadline = Date.now() + LOCK_RELEASE_TIMEOUT_MILLISECONDS
523
+ const remaining = () => Math.max(0, deadline - Date.now())
524
+ const waitForExit = timeout => lock.child.exitCode !== null || lock.child.signalCode !== null ? Promise.resolve(true) : new Promise(resolve => {
525
+ const onExit = () => { clearTimeout(timer); resolve(true) }
526
+ const timer = setTimeout(() => { lock.child.off('exit', onExit); resolve(false) }, timeout)
527
+ lock.child.once('exit', onExit)
528
+ })
529
+ const guardianAlive = () => { try { process.kill(lock.guardianPid, 0); return true } catch (error) { if (error?.code === 'ESRCH') return false; throw error } }
530
+ const waitForGuardianExit = async () => { while (guardianAlive() && remaining() > 0) await wait(Math.min(25, remaining())); return !guardianAlive() }
531
+ const signalGuardian = signal => {
532
+ if (lock.guardianPid === lock.child.pid) return
533
+ try { process.kill(lock.guardianPid, signal) } catch (error) { if (error?.code !== 'ESRCH') throw error }
534
+ }
535
+ const phase = LOCK_RELEASE_TIMEOUT_MILLISECONDS / 3
536
+ const finishGuardian = async () => { if (guardianAlive()) signalGuardian('SIGKILL'); return await waitForGuardianExit() }
528
537
  if (lock.child.exitCode !== null || lock.child.signalCode !== null) {
529
- try {
530
- process.kill(lock.guardianPid, 'SIGTERM')
531
- } catch (error) {
532
- if (error?.code !== 'ESRCH') throw error
533
- }
538
+ if (!await finishGuardian()) throw new SkilledProposerVenvError('ALI_1505_PUBLISH_LOCK_RELEASE_TIMEOUT', 'publish-lock guardian did not exit after SIGKILL')
534
539
  return
535
540
  }
536
- const exited = new Promise(resolve => lock.child.once('exit', resolve))
537
541
  lock.child.stdin.end('release\n')
538
- const timeout = setTimeout(() => lock.child.kill(), PYTHON_PROBE_TIMEOUT_MILLISECONDS)
539
- await exited
540
- clearTimeout(timeout)
542
+ signalGuardian('SIGTERM')
543
+ if (await waitForExit(Math.min(phase, remaining()))) { if (await finishGuardian()) return }
544
+ lock.child.kill('SIGTERM')
545
+ if (await waitForExit(Math.min(phase, remaining()))) { if (await finishGuardian()) return }
546
+ signalGuardian('SIGKILL'); lock.child.kill('SIGKILL')
547
+ const [holderExited, guardianExited] = await Promise.all([waitForExit(remaining()), waitForGuardianExit()])
548
+ if (!holderExited || !guardianExited) throw new SkilledProposerVenvError('ALI_1505_PUBLISH_LOCK_RELEASE_TIMEOUT', 'publish-lock holder or guardian did not exit after SIGKILL')
541
549
  }
542
550
 
543
551
  async function killPublishLockLeaderAtTestHook(lock) {
@@ -603,7 +611,7 @@ function isolatedChildEnv(sslCertFile) {
603
611
  return env
604
612
  }
605
613
 
606
- export async function ensureSkilledProposerVenv(options) {
614
+ export async function ensureSkilledProposerVenv(options, dependencies = {}) {
607
615
  // This must stay first: unsupported interpreters may not create cache state or
608
616
  // invoke a mutating `python -m` command.
609
617
  const probeEnv = isolatedPythonProbeEnv()
@@ -643,6 +651,7 @@ export async function ensureSkilledProposerVenv(options) {
643
651
  const lockedReport = { ...baseReport, waitedForPublishLock: lock.waitedForPublishLock }
644
652
 
645
653
  const stage = join(options.cacheRoot, 'venvs', `.${publishKey}-${process.pid}-${randomUUID()}.tmp`)
654
+ let primaryError, hasPrimaryError = false
646
655
  try {
647
656
  // Holding the per-key publish lock proves that no live builder for this key
648
657
  // can own one of these sibling staging directories.
@@ -726,9 +735,17 @@ export async function ensureSkilledProposerVenv(options) {
726
735
  )
727
736
  }
728
737
  return { ...installingReport, publishedVenv: true }
738
+ } catch (error) {
739
+ primaryError = error
740
+ hasPrimaryError = true
741
+ throw error
729
742
  } finally {
730
743
  rmSync(stage, { recursive: true, force: true })
731
- await releasePublishLock(lock)
744
+ try { await (dependencies.releasePublishLock || releasePublishLock)(lock) }
745
+ catch (cleanupError) {
746
+ if (hasPrimaryError) throw new AggregateError([primaryError, cleanupError], 'ALI_1505_PRIMARY_AND_PUBLISH_LOCK_RELEASE_FAILED')
747
+ throw cleanupError
748
+ }
732
749
  }
733
750
  }
734
751
 
@@ -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")
@@ -406,13 +406,17 @@ def run_from_environment() -> dict[str, Any]:
406
406
 
407
407
  ALI-1502 supplies these variables after verifying runner bytes. Scorer
408
408
  contract resolution and seed validation intentionally happen before
409
- ``start_run`` so either refusal leaves no stranded running record.
409
+ ``start_run`` so either refusal leaves no stranded running record. Hosted
410
+ attach identity is resolved before even those runner-backed preflights.
410
411
  """
411
412
  env = _required_environment()
412
413
  train_split = os.environ.get("ORIZU_TRAIN_SPLIT", "train")
413
414
  validation_split = os.environ.get("ORIZU_VALIDATION_SPLIT", "validation")
414
415
  config = build_config_from_environment()
415
416
  client = OrizuClient.from_env()
417
+ hosted_run_id = os.environ.get("ORIZU_HOSTED_OPTIMIZATION_RUN_ID", "").strip() or None
418
+ if hosted_run_id is not None:
419
+ client.require_hosted_optimization_run(run_id=hosted_run_id, project=env["ORIZU_PROJECT"])
416
420
  profile_version_id = os.environ.get("ORIZU_INSTRUCTION_SET_PROFILE_VERSION_ID")
417
421
  prompt_context, trainset = client.fetch_exec_context(
418
422
  prompt_version_id=os.environ.get("ORIZU_PROMPT_VERSION_ID"), runner_version_id=env["ORIZU_RUNNER_VERSION_ID"],
@@ -492,7 +496,7 @@ def run_from_environment() -> dict[str, Any]:
492
496
  is_multi_component_instruction_set = bool(
493
497
  profile_version_id and isinstance(shape, list) and len(shape) > 1
494
498
  )
495
- run_id = client.start_run(
499
+ run_id = hosted_run_id or client.start_run(
496
500
  project=env["ORIZU_PROJECT"], optimizer_version_id=env["ORIZU_OPTIMIZER_VERSION_ID"],
497
501
  # A set of one is the legacy prompt subject. Only a genuine component
498
502
  # map is attributed to the profile version instead of a prompt version.
@@ -506,6 +510,7 @@ def run_from_environment() -> dict[str, Any]:
506
510
  dataset_version_id=env["ORIZU_DATASET_VERSION_ID"], split_set_id=env["ORIZU_SPLIT_SET_ID"],
507
511
  train_split=train_split, validation_split=validation_split, metadata=metadata,
508
512
  model_config_settings_version_id=instruction_set.get("model_config_settings_version_id") if isinstance(instruction_set, dict) else None,
513
+ runner_version_id=prompt_context.runner_version_id,
509
514
  )
510
515
  print(json.dumps({"optimization_run_id": run_id}), flush=True)
511
516
  # The frozen sink treats ``None`` as no local artifact writer. Keep that