orizu 0.5.47 → 0.5.49

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 (46) hide show
  1. package/dist/app-publish-workspace.d.ts +6 -0
  2. package/dist/app-publish-workspace.d.ts.map +1 -1
  3. package/dist/app-publish-workspace.js +54 -1
  4. package/dist/help.d.ts.map +1 -1
  5. package/dist/help.js +12 -1
  6. package/dist/hosted-boot.d.ts +7 -0
  7. package/dist/hosted-boot.d.ts.map +1 -1
  8. package/dist/hosted-boot.js +31 -2
  9. package/dist/hosted-optimization.d.ts.map +1 -1
  10. package/dist/hosted-optimization.js +40 -21
  11. package/dist/hosted-runtime-assets.d.ts +9 -0
  12. package/dist/hosted-runtime-assets.d.ts.map +1 -1
  13. package/dist/hosted-runtime-assets.js +8 -7
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +83 -68
  16. package/dist/runner-folders.d.ts +61 -0
  17. package/dist/runner-folders.d.ts.map +1 -0
  18. package/dist/runner-folders.js +222 -0
  19. package/dist/runner-version-materialization.d.ts +1 -1
  20. package/dist/runner-version-materialization.d.ts.map +1 -1
  21. package/dist/runner-version-materialization.js +20 -38
  22. package/dist/session-publish.d.ts +8 -0
  23. package/dist/session-publish.d.ts.map +1 -0
  24. package/dist/session-publish.js +118 -0
  25. package/dist/skilled-proposer-launch.d.ts +11 -1
  26. package/dist/skilled-proposer-launch.d.ts.map +1 -1
  27. package/dist/skilled-proposer-launch.js +35 -4
  28. package/dist/workbench-cli.d.ts.map +1 -1
  29. package/dist/workbench-cli.js +14 -1
  30. package/package.json +1 -1
  31. package/scripts/build-cli-dist.mjs +46 -8
  32. package/scripts/vendor-orizu-skill.mjs +52 -7
  33. package/src/app-publish-workspace.ts +57 -1
  34. package/src/help.ts +13 -1
  35. package/src/hosted-boot.ts +40 -2
  36. package/src/hosted-optimization.ts +49 -31
  37. package/src/hosted-runtime-assets.ts +10 -9
  38. package/src/index.ts +79 -87
  39. package/src/runner-folders.ts +223 -0
  40. package/src/runner-version-materialization.ts +21 -34
  41. package/src/session-publish.ts +127 -0
  42. package/src/skilled-proposer-launch.ts +41 -3
  43. package/src/workbench-cli.ts +14 -1
  44. package/vendor/skills/orizu/references/authority-map.md +1 -1
  45. package/vendor/skills/orizu/references/building-apps.md +1 -1
  46. package/vendor/skills/orizu/references/cli-reference.md +16 -0
@@ -197,6 +197,8 @@ export async function postBootStatus(opts: {
197
197
  status: 'ready' | 'failed' | 'complete' | 'turn_failed'
198
198
  runId: string | null
199
199
  reason?: string | null
200
+ /** ORI-2512: how long each boot step took, sent with `ready`. */
201
+ bootSteps?: readonly BootStep[]
200
202
  fetchImpl: BootFetch
201
203
  log?: (line: string) => void
202
204
  }): Promise<void> {
@@ -208,6 +210,7 @@ export async function postBootStatus(opts: {
208
210
  status: opts.status,
209
211
  ...(opts.runId ? { runId: opts.runId } : {}),
210
212
  ...(opts.reason ? { reason: opts.reason } : {}),
213
+ ...(opts.bootSteps?.length ? { bootSteps: opts.bootSteps } : {}),
211
214
  }),
212
215
  })
213
216
  opts.log?.(`boot-status ${opts.status} reported (${res.status})`)
@@ -216,9 +219,31 @@ export async function postBootStatus(opts: {
216
219
  }
217
220
  }
218
221
 
222
+ /** ORI-2512: one step of the in-sandbox boot and how long it took. */
223
+ interface BootStep {
224
+ step: string
225
+ ms: number
226
+ }
227
+
228
+ /** Times the boot's steps, each from the end of the one before. */
229
+ function bootStepClock(now: () => number): { mark: (step: string) => void; steps: BootStep[] } {
230
+ const steps: BootStep[] = []
231
+ let last = now()
232
+ return {
233
+ steps,
234
+ mark: step => {
235
+ const at = now()
236
+ // Timing only describes the boot: a clock that goes back skips a step.
237
+ if (at >= last) steps.push({ step, ms: Math.round(at - last) })
238
+ last = at
239
+ },
240
+ }
241
+ }
242
+
219
243
  async function postRequiredIdleReady(opts: {
220
244
  bootStatusUrl: string
221
245
  bootSecret: string
246
+ bootSteps: readonly BootStep[]
222
247
  fetchImpl: BootFetch
223
248
  sleep: (ms: number) => Promise<void>
224
249
  attemptTimeoutMs?: number
@@ -229,7 +254,7 @@ async function postRequiredIdleReady(opts: {
229
254
  const response = await opts.fetchImpl(opts.bootStatusUrl, {
230
255
  method: 'POST',
231
256
  headers: { Authorization: `Bearer ${opts.bootSecret}`, 'Content-Type': 'application/json' },
232
- body: JSON.stringify({ status: 'ready' }),
257
+ body: JSON.stringify({ status: 'ready', ...(opts.bootSteps.length ? { bootSteps: opts.bootSteps } : {}) }),
233
258
  signal: AbortSignal.timeout(opts.attemptTimeoutMs ?? TURN_STATUS_ATTEMPT_TIMEOUT_MS),
234
259
  })
235
260
  if (response.ok) return
@@ -1015,6 +1040,7 @@ async function runHostedBootWithLive(
1015
1040
  delete processEnv.ORIZU_BOOT_SECRET
1016
1041
  processEnv.ORIZU_NO_UPDATE_CHECK = '1'
1017
1042
  if (env.runId) assertSafeRunId(env.runId)
1043
+ const bootClock = bootStepClock(now)
1018
1044
 
1019
1045
  // 1 — Pull the agent bearer (retry/backoff — the DO may still be arming).
1020
1046
  let bearer = await pullAgentBearer({
@@ -1027,24 +1053,29 @@ async function runHostedBootWithLive(
1027
1053
  log,
1028
1054
  })
1029
1055
  log('agent bearer pulled')
1056
+ bootClock.mark('boot_agent_token')
1030
1057
 
1031
1058
  // 2 — Pull the connector env bundle and export it (+ register redaction).
1032
1059
  const bundle = await fetchEnvBundle({ envBundleUrl: env.envBundleUrl, bearer: bearer.token, fetchImpl })
1033
1060
  const redacted = applyEnvBundle(bundle, processEnv)
1034
1061
  log(`env bundle applied (${bundle.connectors.length} connectors, ${redacted.length} redacted vars)`)
1062
+ bootClock.mark('boot_env_bundle')
1035
1063
 
1036
1064
  // 3 — Resolve the session (workspace/branch/task) + run + repo from the plane.
1037
1065
  let session = await resolveSession({ baseUrl: env.baseUrl, sessionId: env.sessionId, bearer: bearer.token, fetchImpl })
1038
1066
  let currentNonTerminalRunId = session.runStatus === 'pending' || session.runStatus === 'running'
1039
1067
  ? session.runId
1040
1068
  : null
1069
+ bootClock.mark('boot_session')
1041
1070
  // Production restarts with no live run are between turns. Prove this boot is
1042
1071
  // live before waiting so the coordinator's readiness timeout cannot reap an
1043
1072
  // intentionally idle sandbox before the longer hosted idle policy applies.
1044
- if (!opts.runLoop && !currentNonTerminalRunId) {
1073
+ const bootsIdle = !opts.runLoop && !currentNonTerminalRunId
1074
+ if (bootsIdle) {
1045
1075
  await postRequiredIdleReady({
1046
1076
  bootStatusUrl: env.bootStatusUrl,
1047
1077
  bootSecret: env.bootSecret,
1078
+ bootSteps: bootClock.steps,
1048
1079
  fetchImpl,
1049
1080
  sleep,
1050
1081
  attemptTimeoutMs: turnStatusAttemptTimeoutMs,
@@ -1144,6 +1175,7 @@ async function runHostedBootWithLive(
1144
1175
  const repo = await resolveRepo({ baseUrl: env.baseUrl, workspaceId, sessionId: env.sessionId, bearer: bearer.token, fetchImpl })
1145
1176
  const repositoryHost = new URL(repo.cloneUrl).host.toLowerCase()
1146
1177
  log(`resolved run ${runId} on ${repo.repoFullName}@${session.repoBranch}`)
1178
+ bootClock.mark('boot_repo')
1147
1179
 
1148
1180
  // 4 — Write the run-scoped assets: boot secret + initial bearer (0600), the
1149
1181
  // PULL-MODE credential helper + its boot context. The bearer file feeds the
@@ -1347,6 +1379,7 @@ const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
1347
1379
  exec('git', ['-C', workspaceDir, 'config', 'user.name', AGENT_GIT_IDENTITY.name])
1348
1380
  exec('git', ['-C', workspaceDir, 'config', 'user.email', AGENT_GIT_IDENTITY.email])
1349
1381
  if (!wokenDisk && !sessionCopyFilled) log(`cloned ${session.repoBranch}`)
1382
+ bootClock.mark('boot_clone')
1350
1383
 
1351
1384
  // 5a — ORI-2609: the team copy holds only the projects the team seed knew
1352
1385
  // about. Write this session's project folder if it is missing, before the
@@ -1359,6 +1392,7 @@ const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
1359
1392
  project: { id: session.projectId, slug: session.projectSlug, name: session.projectName },
1360
1393
  exec: (cmd, args) => exec(cmd, args),
1361
1394
  }))
1395
+ bootClock.mark('boot_project_folder')
1362
1396
 
1363
1397
  // 5b — Stage the orizu skill into the cloned repo so the agent discovers the
1364
1398
  // Orizu workflows (ALI-1059). SHARED with the operator path via `stageOrizuSkill`
@@ -1377,6 +1411,7 @@ const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
1377
1411
  } catch (error) {
1378
1412
  log(`orizu skill staging failed: ${error instanceof Error ? error.message : String(error)}`)
1379
1413
  }
1414
+ bootClock.mark('boot_skill_stage')
1380
1415
 
1381
1416
  // ALI-1060: the agent is now genuinely LIVE — bearer minted, connectors
1382
1417
  // applied, session/run resolved, repo cloned, skill staged. Signal the
@@ -1388,6 +1423,9 @@ const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
1388
1423
  bootSecret: env.bootSecret,
1389
1424
  status: 'ready',
1390
1425
  runId,
1426
+ // An idle boot sent its steps before it waited for work; the wait is not
1427
+ // a boot step.
1428
+ ...(bootsIdle ? {} : { bootSteps: bootClock.steps }),
1391
1429
  fetchImpl,
1392
1430
  log,
1393
1431
  })
@@ -3,13 +3,12 @@ import { existsSync, readFileSync, rmSync } from 'node:fs'
3
3
  import { createServer } from 'node:http'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { delimiter, join } from 'node:path'
6
- import { runPipedChild } from './child-output-tee.js'
7
6
  import { getGepaPythonPathEntries } from './gepa-python-paths.js'
8
7
  import { deliveredProviderLookup, HOSTED_PROVIDER_FAILURE, INJECTED_ENV_VARS_ENV, PROVIDER_REGISTRY_EXTRA_ENV } from './hosted-environment.js'
9
8
  import { credentialEnvFor, parseModelIdentity } from './provider-registry.js'
10
9
  import type { ProviderLookup } from './provider-lookup.js'
11
- import { materializeRunnerVersion, cleanupMaterializedRunners } from './runner-version-materialization.js'
12
10
  import { prepareSkilledProposerLaunch, spawnSkilledProposerChild } from './skilled-proposer-launch.js'
11
+ import type { SkilledProposerStopOptions } from './skilled-proposer-launch.js'
13
12
  import { hostedProviderFromModel } from './hosted-provider-settings.js'
14
13
  import { validNormalizedSkilledProposerConfig } from './skilled-proposer-wire.js'
15
14
  import { REDACTION_PLACEHOLDER, redactSecrets } from './secret-redaction.js'
@@ -358,7 +357,8 @@ async function startTokenBroker(
358
357
  }
359
358
  return {
360
359
  url: `http://127.0.0.1:${address.port}/${nonce}`,
361
- stop: () => { server.close() },
360
+ // Also drop open keep-alive connections, so no refresh succeeds after stop.
361
+ stop: () => { server.close(); server.closeAllConnections() },
362
362
  token: () => activeToken,
363
363
  secrets: () => [...tokens],
364
364
  }
@@ -367,30 +367,23 @@ async function spawnOfficial(
367
367
  python: string,
368
368
  environment: NodeJS.ProcessEnv,
369
369
  ready: () => Promise<void>,
370
- output: ProcessOutputCapture
370
+ output: ProcessOutputCapture,
371
+ stopOptions: SkilledProposerStopOptions,
371
372
  ): Promise<{ status: number | null; error?: Error }> {
372
- if (environment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer') {
373
- return spawnSkilledProposerChild(
374
- python,
375
- ['-m', 'orizu_gepa_connector'],
376
- 'official',
377
- environment,
378
- ready,
379
- {
380
- stdout: chunk => output.stdout.append(chunk),
381
- stderr: chunk => output.stderr.append(chunk),
382
- }
383
- )
384
- }
385
- return runPipedChild(
373
+ // ORI-2429: both proposers start through the launcher (ORI-2402), so a stop
374
+ // signal reaches the optimizer, which then ends instead of outliving the
375
+ // CLI, and the CLI cleans up before it ends by that signal.
376
+ return spawnSkilledProposerChild(
386
377
  python,
387
378
  ['-m', 'orizu_gepa_connector'],
379
+ 'official',
388
380
  environment,
389
381
  ready,
390
382
  {
391
383
  stdout: chunk => output.stdout.append(chunk),
392
384
  stderr: chunk => output.stderr.append(chunk),
393
- }
385
+ },
386
+ stopOptions,
394
387
  )
395
388
  }
396
389
  async function ensureFailedRun(
@@ -430,7 +423,14 @@ export async function hostedOptimizationCommand(io: HostedOptimizationIo): Promi
430
423
  }
431
424
  delete process.env.ORIZU_BOOT_SECRET
432
425
  let reason = 'hosted_optimization_mint_refused'
433
- const cleanups: Array<() => void> = []
426
+ // ORI-2429: one Effect scope owns the runner copies, the result file and the
427
+ // token broker, so a stop signal removes them too (see runner-folders.ts).
428
+ const { Effect, Scope } = await import('effect')
429
+ const { downloadedRunner, removeOnClose, RemovalLedger, stopClosesScope } = await import('./runner-folders.js')
430
+ const ledger = new RemovalLedger()
431
+ const { scope, close, stop, listen, removeHandlers } = stopClosesScope()
432
+ const inScope = <A>(effect: import('effect').Effect.Effect<A, unknown, import('effect').Scope.Scope>) =>
433
+ Effect.runPromise(effect.pipe(Effect.provideService(Scope.Scope, scope)))
434
434
  let broker: TokenBroker | undefined
435
435
  const resultFile = join(tmpdir(), `orizu-hosted-optimization-result-${boot.runId}-${randomUUID()}.json`)
436
436
  const diagnosticSecrets = () => injectedSecrets(boot, broker)
@@ -438,6 +438,7 @@ export async function hostedOptimizationCommand(io: HostedOptimizationIo): Promi
438
438
  const bootOutput = new BoundedTextCapture(diagnosticSecrets)
439
439
  let failureDiagnostics: FailureDiagnostics | undefined
440
440
  try {
441
+ listen()
441
442
  const first = await mint(boot)
442
443
  if (first.response.status === 422) reason = 'hosted_optimization_spec_missing'
443
444
  if (!first.response.ok) {
@@ -462,18 +463,28 @@ export async function hostedOptimizationCommand(io: HostedOptimizationIo): Promi
462
463
  process.env.ORIZU_BASE_URL = boot.baseUrl.origin
463
464
  process.env.ORIZU_TOKEN = minted.token
464
465
  reason = 'hosted_optimization_materialization_failed'
465
- const candidate = await materializeRunnerVersion(String(minted.jobSpec.runnerVersionId)); cleanups.push(candidate.cleanup)
466
- const scorer = await materializeRunnerVersion(String(minted.jobSpec.scorerRunnerVersionId)); cleanups.push(scorer.cleanup)
466
+ const candidate = await inScope(downloadedRunner(String(minted.jobSpec.runnerVersionId), ledger))
467
+ const scorer = await inScope(downloadedRunner(String(minted.jobSpec.scorerRunnerVersionId), ledger))
468
+ await inScope(removeOnClose(ledger, 'other', () => rmSync(resultFile, { force: true })))
467
469
  broker = await startTokenBroker(boot, minted)
470
+ const startedBroker = broker
471
+ await inScope(removeOnClose(ledger, 'other', () => startedBroker.stop()))
468
472
  const environment = childEnvironment(boot, minted, candidate.runnerDir, scorer.runnerDir, broker.url, resultFile)
469
473
  const launch = prepareSkilledProposerLaunch(process.env.PYTHON || 'python3', 'official', environment)
470
474
  reason = 'hosted_optimization_process_failed'
471
- const result = await spawnOfficial(
472
- launch.python,
473
- launch.environment,
474
- () => postBoot(boot, 'ready', { status: 'ready' }),
475
- processOutput
476
- )
475
+ stop.launcherOwnsStop()
476
+ let result: Awaited<ReturnType<typeof spawnOfficial>>
477
+ try {
478
+ result = await spawnOfficial(
479
+ launch.python,
480
+ launch.environment,
481
+ () => postBoot(boot, 'ready', { status: 'ready' }),
482
+ processOutput,
483
+ { beforeSignalExit: stop.beforeSignalExit },
484
+ )
485
+ } finally {
486
+ stop.launcherDone()
487
+ }
477
488
  if (result.error || result.status !== 0) {
478
489
  const processFailure = result.error?.message || `optimizer exited with status ${result.status}`
479
490
  const secrets = injectedSecrets(boot, broker)
@@ -506,8 +517,15 @@ export async function hostedOptimizationCommand(io: HostedOptimizationIo): Promi
506
517
  await io.printErr?.(reason)
507
518
  return 1
508
519
  } finally {
509
- broker?.stop()
510
- rmSync(resultFile, { force: true })
511
- cleanupMaterializedRunners(cleanups)
520
+ removeHandlers()
521
+ // Closing stops the broker, deletes the result file and removes the copies.
522
+ close()
523
+ // As before: a broker or result-file failure is thrown first, then runner removals.
524
+ const otherFailure = ledger.failures.find(failure => failure.group === 'other')
525
+ if (otherFailure) throw otherFailure.error
526
+ const runnerFailures = ledger.failures.filter(failure => failure.group === 'downloads').map(failure => failure.error)
527
+ if (runnerFailures.length > 0) {
528
+ throw new AggregateError(runnerFailures, 'Failed to clean materialized runner directories')
529
+ }
512
530
  }
513
531
  }
@@ -51,15 +51,6 @@ export const AGENT_GIT_IDENTITY = { name: 'Orizu Agent', email: 'agent@orizu.ai'
51
51
 
52
52
  // -- Hosted task prompt scaffolding (ALI-1036) -------------------------------
53
53
 
54
- /**
55
- * Origin-specific standing preambles wrap hosted task prompts. Web sessions get
56
- * `HOSTED_TASK_PREAMBLE`, which directs the agent to use the question tool and
57
- * wait for the human. CLI-origin sessions get `HOSTED_TASK_PREAMBLE_CLI`, where
58
- * questions remain auto-answered for unattended operation. Undefined or unknown
59
- * origins fail safely to the CLI preamble. Both variants tell the agent to
60
- * commit as it goes; end-of-run auto-harvest (ALI-1036, `harvestWorkspace`)
61
- * preserves anything left uncommitted.
62
- */
63
54
  /** ORI-2653: where a push lands, shared by both preambles. A web session's
64
55
  * `origin` is its own copy of the team repository; a legacy GitHub session's
65
56
  * current branch is its session branch. Only an Orizu publish puts work on the
@@ -69,6 +60,15 @@ const HOSTED_PUSH_SCOPE =
69
60
  "not on the team's main. Never say work is published or on the team's main unless an Orizu " +
70
61
  'publish command reported that it merged or was already there. '
71
62
 
63
+ /**
64
+ * Origin-specific standing preambles wrap hosted task prompts. Web sessions get
65
+ * `HOSTED_TASK_PREAMBLE`, which directs the agent to use the question tool and
66
+ * wait for the human. CLI-origin sessions get `HOSTED_TASK_PREAMBLE_CLI`, where
67
+ * questions remain auto-answered for unattended operation. Undefined or unknown
68
+ * origins fail safely to the CLI preamble. Both variants tell the agent to
69
+ * commit as it goes; end-of-run auto-harvest (ALI-1036, `harvestWorkspace`)
70
+ * preserves anything left uncommitted.
71
+ */
72
72
  export const HOSTED_TASK_PREAMBLE =
73
73
  'You are in a hosted sandbox. Work autonomously. When blocked on human input, call the built-in ' +
74
74
  'question tool with exactly one clear question and at most eight bounded options, then wait for ' +
@@ -76,6 +76,7 @@ export const HOSTED_TASK_PREAMBLE =
76
76
  'branch in logical units as you go (an automatic ' +
77
77
  'checkpoint will also save anything uncommitted at the end). Never switch branches. ' +
78
78
  HOSTED_PUSH_SCOPE +
79
+ "To put work on the team's main, run `orizu session publish`. " +
79
80
  'Production/default pointers and run metadata live in Orizu, not this repo.'
80
81
 
81
82
  /** Pre-ALI-1758 unattended behavior retained for CLI-originated sessions. */
package/src/index.ts CHANGED
@@ -100,7 +100,6 @@ import {
100
100
  getTaskReport,
101
101
  updateTaskStatus,
102
102
  } from './task-commands.js'
103
- import { cleanupAll, throwWithCleanup } from './cleanup.js'
104
103
  import {
105
104
  extractFlagValue,
106
105
  verifyGepaRunnerDirsFromArgs,
@@ -177,7 +176,6 @@ import { inspectExistingWorkspaceTeam, getWorkspaceRoot } from './workspace.js'
177
176
  import { writeLastErrorRecord, extractLastErrorCode } from './last-error-record.js'
178
177
  import type { CliLengthStats, CliLengthMeasurementUnavailableReason } from './prompt-length-wire.js'
179
178
  import type { RunnerInstructionSet } from './runner-instruction-set-contract.js'
180
- import type { GepaVerifiedArgs } from './runner-dir-verify.js'
181
179
  import type { ReportCommentsCliContext } from './report-comments-cli.js'
182
180
 
183
181
 
@@ -1293,101 +1291,94 @@ const GEPA_RUNNER_MATERIALIZATION_PAIRS = [
1293
1291
  { versionFlag: '--scorer-runner-version-id', dirFlag: '--scorer-runner-dir' },
1294
1292
  ] as const
1295
1293
 
1296
- async function materializeMissingGepaRunnerDirs(args: string[]): Promise<{
1297
- args: string[]
1298
- cleanup: () => void
1299
- }> {
1300
- const rewritten = [...args]
1301
- const cleanups: Array<() => void> = []
1302
- const cleanup = () => cleanupAll(
1303
- cleanups,
1304
- 'Failed to clean materialized runner directories'
1305
- )
1306
-
1307
- try {
1308
- for (const pair of GEPA_RUNNER_MATERIALIZATION_PAIRS) {
1309
- const runnerVersionId = extractFlagValue(rewritten, pair.versionFlag)
1310
- const runnerDir = extractFlagValue(rewritten, pair.dirFlag)
1311
- if (!runnerVersionId || runnerDir) continue
1312
-
1313
- const materialized = await materializeRunnerVersion(runnerVersionId)
1314
- cleanups.push(materialized.cleanup)
1315
- rewritten.push(pair.dirFlag, materialized.runnerDir)
1316
- }
1317
- } catch (error) {
1318
- throwWithCleanup(
1319
- error,
1320
- cleanups,
1321
- 'Runner materialization failed and cleanup also failed'
1322
- )
1323
- }
1324
-
1325
- return { args: rewritten, cleanup }
1326
- }
1327
-
1328
1294
  async function runGepaOptimization() {
1329
1295
  const project = getArg('--project') || await resolveProjectSlug(null)
1330
1296
  const baseUrl = getBaseUrl()
1331
1297
  const runGepa = getGepaPythonCommand(cliArgs.slice(2), process.env)
1332
1298
  const python = runGepa.python
1333
- let forwardedArgs = runGepa.args
1334
- // ALI-1159 (ADR-007): GEPA executes ad-hoc local runner dirs while
1335
- // attributing records to registered runner version ids — verify the bytes
1336
- // ARE those versions before the optimizer spawns. Scans the forwarded argv
1337
- // itself (both argparse flag forms), fail-closed on half-populated pairs,
1338
- // and REWRITES the dir flags to verified snapshots (the exact hashed bytes)
1339
- // so a post-hash mutation of the original dirs cannot change what runs.
1340
- const materialized = await materializeMissingGepaRunnerDirs(forwardedArgs)
1341
- let verified: GepaVerifiedArgs | undefined
1299
+ // ORI-2429: one Effect scope owns every runner copy this run writes to the
1300
+ // temp folder; it removes them on success, failure and stop signals.
1301
+ const { Effect } = await import('effect')
1302
+ const { downloadedRunner, heldUntilClose, RemovalLedger, runStoppable } = await import('./runner-folders.js')
1303
+ const ledger = new RemovalLedger()
1304
+ // Which step failed decides the message when a removal also failed, as before.
1305
+ let phase: 'materialize' | 'launch' = 'materialize'
1342
1306
  let selectedEngine = 'official'
1307
+ let failure: { error: unknown } | undefined
1343
1308
  try {
1344
- verified = await verifyGepaRunnerDirsFromArgs(materialized.args)
1345
- forwardedArgs = verified.args
1346
- // Uniform resolution (ALI-1090): honors ORIZU_TOKEN / ORIZU_TOKEN_FILE before
1347
- // credentials.json so hosted sandboxes can run optimizations. Read at spawn
1348
- // time — AFTER the verification awaits above, so a hosted token-file
1349
- // rotation crossing the hash + lookup round-trips cannot hand the child a
1350
- // stale bearer.
1351
- const token = resolveAuthTokenForBaseUrl(baseUrl)
1352
- if (!forwardedArgs.includes('--project')) {
1353
- forwardedArgs = ['--project', project, ...forwardedArgs]
1354
- }
1355
- const dispatch = applyHostedInstructionSetProfileOverride(dispatchGepaEngine(forwardedArgs, project, {
1356
- ...process.env,
1357
- ORIZU_API_URL: baseUrl,
1358
- ORIZU_TOKEN: token,
1359
- ORIZU_PROJECT: project,
1360
- ORIZU_VERIFIED_RUNNER_DIRS: JSON.stringify(verified.verifiedDirs),
1361
- PYTHONPATH: getGepaPythonPathEntries(process.env.PYTHONPATH).join(delimiter),
1362
- PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
1309
+ await runStoppable(stop => Effect.gen(function* () {
1310
+ // Download registered runner versions passed without a local folder; the
1311
+ // verification below then checks those bytes like any local folder.
1312
+ const materializedArgs = [...runGepa.args]
1313
+ for (const pair of GEPA_RUNNER_MATERIALIZATION_PAIRS) {
1314
+ const runnerVersionId = extractFlagValue(materializedArgs, pair.versionFlag)
1315
+ const runnerDir = extractFlagValue(materializedArgs, pair.dirFlag)
1316
+ if (!runnerVersionId || runnerDir) continue
1317
+ const materialized = yield* downloadedRunner(runnerVersionId, ledger, false)
1318
+ materializedArgs.push(pair.dirFlag, materialized.runnerDir)
1319
+ }
1320
+ phase = 'launch'
1321
+ // ALI-1159 (ADR-007): GEPA executes ad-hoc local runner dirs while
1322
+ // attributing records to registered runner version ids — verify the bytes
1323
+ // ARE those versions before the optimizer spawns. Scans the forwarded argv
1324
+ // itself (both argparse flag forms), fail-closed on half-populated pairs,
1325
+ // and REWRITES the dir flags to verified snapshots (the exact hashed bytes)
1326
+ // so a post-hash mutation of the original dirs cannot change what runs.
1327
+ const verified = yield* heldUntilClose(ledger, 'snapshots', () => verifyGepaRunnerDirsFromArgs(materializedArgs))
1328
+ let forwardedArgs = verified.args
1329
+ // Uniform resolution (ALI-1090): honors ORIZU_TOKEN / ORIZU_TOKEN_FILE before
1330
+ // credentials.json so hosted sandboxes can run optimizations. Read at spawn
1331
+ // time — AFTER the verification awaits above, so a hosted token-file
1332
+ // rotation crossing the hash + lookup round-trips cannot hand the child a
1333
+ // stale bearer.
1334
+ const token = resolveAuthTokenForBaseUrl(baseUrl)
1335
+ if (!forwardedArgs.includes('--project')) {
1336
+ forwardedArgs = ['--project', project, ...forwardedArgs]
1337
+ }
1338
+ const dispatch = applyHostedInstructionSetProfileOverride(dispatchGepaEngine(forwardedArgs, project, {
1339
+ ...process.env,
1340
+ ORIZU_API_URL: baseUrl,
1341
+ ORIZU_TOKEN: token,
1342
+ ORIZU_PROJECT: project,
1343
+ ORIZU_VERIFIED_RUNNER_DIRS: JSON.stringify(verified.verifiedDirs),
1344
+ PYTHONPATH: getGepaPythonPathEntries(process.env.PYTHONPATH).join(delimiter),
1345
+ PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
1346
+ }))
1347
+ const instructionSetProfileVersionId = yield* Effect.tryPromise({
1348
+ try: () => resolveGepaSeedFromEnvironment(dispatch.environment, project, 'production'),
1349
+ catch: error => error,
1350
+ })
1351
+ if (instructionSetProfileVersionId) dispatch.environment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_ID = instructionSetProfileVersionId
1352
+ selectedEngine = dispatch.engine
1353
+ const launch = prepareSkilledProposerLaunch(python, dispatch.engine, dispatch.environment)
1354
+ // While the optimizer runs, the launcher owns stop signals (ORI-2402) and
1355
+ // closes this scope just before the CLI ends by the signal. Taken in the
1356
+ // same step as the launch, so no stop can fall between the two.
1357
+ const result = yield* Effect.tryPromise({
1358
+ try: () => (stop.launcherOwnsStop(), spawnSkilledProposerChild(
1359
+ launch.python, ['-m', dispatch.module, ...dispatch.args], dispatch.engine, launch.environment,
1360
+ undefined, undefined, { beforeSignalExit: stop.beforeSignalExit },
1361
+ )),
1362
+ catch: error => error,
1363
+ }).pipe(Effect.ensuring(Effect.sync(stop.launcherDone)))
1364
+ if (result.error) {
1365
+ return yield* Effect.fail(new Error(`${selectedEngine} GEPA engine failed: ${result.error.message}`, { cause: result.error }))
1366
+ }
1367
+ if (result.status !== 0) {
1368
+ return yield* Effect.fail(new Error(`${selectedEngine} GEPA engine failed with exit code ${result.status}`))
1369
+ }
1363
1370
  }))
1364
- const instructionSetProfileVersionId = await resolveGepaSeedFromEnvironment(dispatch.environment, project, 'production')
1365
- if (instructionSetProfileVersionId) dispatch.environment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_ID = instructionSetProfileVersionId
1366
- selectedEngine = dispatch.engine
1367
- const launch = prepareSkilledProposerLaunch(python, dispatch.engine, dispatch.environment)
1368
- const result = await spawnSkilledProposerChild(
1369
- launch.python, ['-m', dispatch.module, ...dispatch.args], dispatch.engine, launch.environment,
1370
- )
1371
- if (result.error) {
1372
- throw new Error(`${selectedEngine} GEPA engine failed: ${result.error.message}`, { cause: result.error })
1373
- }
1374
- if (result.status !== 0) {
1375
- throw new Error(`${selectedEngine} GEPA engine failed with exit code ${result.status}`)
1376
- }
1377
1371
  } catch (error) {
1378
- throwWithCleanup(
1379
- error,
1380
- [
1381
- ...(verified ? [verified.cleanup] : []),
1382
- materialized.cleanup,
1383
- ],
1384
- 'GEPA launch failed and runner cleanup also failed'
1385
- )
1372
+ failure = { error }
1386
1373
  }
1387
- cleanupAll(
1388
- [verified.cleanup, materialized.cleanup],
1389
- 'Failed to clean GEPA runner directories'
1390
- )
1374
+ // The scope has closed here; a removal failure decides the message, as before.
1375
+ if (ledger.has()) {
1376
+ if (!failure) throw new AggregateError(ledger.errors(), 'Failed to clean GEPA runner directories')
1377
+ throw new AggregateError([failure.error, ...ledger.errors()], phase === 'materialize'
1378
+ ? 'Runner materialization failed and cleanup also failed'
1379
+ : 'GEPA launch failed and runner cleanup also failed')
1380
+ }
1381
+ if (failure) throw failure.error
1391
1382
  if (hasJsonFlag()) {
1392
1383
  printJson({ status: 'completed', exitCode: 0 })
1393
1384
  }
@@ -2256,6 +2247,7 @@ function isCliEntrypoint(): boolean {
2256
2247
  if (isCliEntrypoint()) {
2257
2248
  const rawArgs = process.argv.slice(2)
2258
2249
  withPassiveUpdateNotice(rawArgs, getCliVersion, printError, args =>
2250
+ // ORI-2429: the process-boundary adapter stays a Promise (ADR-038 guide); the record write is Effect-owned and never replaces this error.
2259
2251
  main(args).catch(async error => {
2260
2252
  const message = error instanceof Error ? error.message : 'Unknown error'
2261
2253
  // The reporter email is unknown on the failure path, so the record masks every address.