orizu 0.5.38 → 0.5.39

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 (91) hide show
  1. package/README.md +18 -3
  2. package/dist/app-commands.d.ts.map +1 -1
  3. package/dist/app-commands.js +215 -57
  4. package/dist/app-publish-failure.d.ts +6 -0
  5. package/dist/app-publish-failure.d.ts.map +1 -0
  6. package/dist/app-publish-failure.js +9 -0
  7. package/dist/app-publish-paths.d.ts +4 -0
  8. package/dist/app-publish-paths.d.ts.map +1 -0
  9. package/dist/app-publish-paths.js +32 -0
  10. package/dist/app-publish-workspace.d.ts +16 -0
  11. package/dist/app-publish-workspace.d.ts.map +1 -0
  12. package/dist/app-publish-workspace.js +175 -0
  13. package/dist/artifacts-git-runtime.d.ts +5 -0
  14. package/dist/artifacts-git-runtime.d.ts.map +1 -1
  15. package/dist/artifacts-git-runtime.js +70 -25
  16. package/dist/artifacts-session-merge.d.ts +62 -0
  17. package/dist/artifacts-session-merge.d.ts.map +1 -0
  18. package/dist/artifacts-session-merge.js +123 -0
  19. package/dist/dataset-append-workflow.d.ts +12 -0
  20. package/dist/dataset-append-workflow.d.ts.map +1 -0
  21. package/dist/dataset-append-workflow.js +198 -0
  22. package/dist/dataset-commands.d.ts.map +1 -1
  23. package/dist/dataset-commands.js +11 -130
  24. package/dist/dataset-transfer-download.js +6 -6
  25. package/dist/dataset-transfer-upload.js +8 -8
  26. package/dist/git-credential.d.ts +1 -1
  27. package/dist/git-credential.d.ts.map +1 -1
  28. package/dist/git-credential.js +8 -1
  29. package/dist/help.js +3 -3
  30. package/dist/hosted-boot.d.ts +11 -0
  31. package/dist/hosted-boot.d.ts.map +1 -1
  32. package/dist/hosted-boot.js +46 -1
  33. package/dist/hosted-session-cli.d.ts.map +1 -1
  34. package/dist/hosted-session-cli.js +6 -1
  35. package/dist/hosted-session-seed.d.ts +45 -0
  36. package/dist/hosted-session-seed.d.ts.map +1 -0
  37. package/dist/hosted-session-seed.js +61 -0
  38. package/dist/jsonl-stream.d.ts +1 -0
  39. package/dist/jsonl-stream.d.ts.map +1 -1
  40. package/dist/jsonl-stream.js +7 -0
  41. package/dist/provider-list-workflow.d.ts +3 -0
  42. package/dist/provider-list-workflow.d.ts.map +1 -0
  43. package/dist/provider-list-workflow.js +95 -0
  44. package/dist/provider-wire-record.d.ts +3 -0
  45. package/dist/provider-wire-record.d.ts.map +1 -0
  46. package/dist/provider-wire-record.js +4 -0
  47. package/dist/providers-cli.d.ts.map +1 -1
  48. package/dist/providers-cli.js +5 -15
  49. package/dist/publish-merge-job.d.ts +68 -0
  50. package/dist/publish-merge-job.d.ts.map +1 -0
  51. package/dist/publish-merge-job.js +320 -0
  52. package/dist/publish-merge-timing.d.ts +18 -0
  53. package/dist/publish-merge-timing.d.ts.map +1 -0
  54. package/dist/publish-merge-timing.js +23 -0
  55. package/dist/workbench-cli.d.ts +4 -0
  56. package/dist/workbench-cli.d.ts.map +1 -1
  57. package/dist/workbench-cli.js +11 -1
  58. package/dist/workspace-bootstrap-job.d.ts +1 -0
  59. package/dist/workspace-bootstrap-job.d.ts.map +1 -1
  60. package/dist/workspace-bootstrap-job.js +1 -1
  61. package/package.json +5 -2
  62. package/src/app-commands.ts +234 -66
  63. package/src/app-publish-failure.ts +7 -0
  64. package/src/app-publish-paths.ts +27 -0
  65. package/src/app-publish-workspace.ts +207 -0
  66. package/src/artifacts-git-runtime.ts +73 -31
  67. package/src/artifacts-session-merge.ts +253 -0
  68. package/src/dataset-append-workflow.ts +205 -0
  69. package/src/dataset-commands.ts +10 -170
  70. package/src/dataset-transfer-download.ts +5 -5
  71. package/src/dataset-transfer-upload.ts +7 -7
  72. package/src/git-credential.ts +8 -1
  73. package/src/help.ts +3 -3
  74. package/src/hosted-boot.ts +56 -1
  75. package/src/hosted-session-cli.ts +6 -1
  76. package/src/hosted-session-seed.ts +78 -0
  77. package/src/jsonl-stream.ts +8 -0
  78. package/src/provider-list-workflow.ts +108 -0
  79. package/src/provider-wire-record.ts +4 -0
  80. package/src/providers-cli.ts +5 -13
  81. package/src/publish-merge-job.ts +350 -0
  82. package/src/publish-merge-timing.ts +27 -0
  83. package/src/workbench-cli.ts +15 -1
  84. package/src/workspace-bootstrap-job.ts +1 -1
  85. package/vendor/skills/orizu/flows/onboard.md +1 -1
  86. package/vendor/skills/orizu/references/authority-map.md +2 -2
  87. package/vendor/skills/orizu/references/building-apps.md +24 -0
  88. package/vendor/skills/orizu/references/cli-reference.md +13 -4
  89. package/vendor/skills/orizu/references/optimization-reports.md +1 -1
  90. package/vendor/skills/orizu/references/primer.md +1 -0
  91. package/vendor/skills/orizu/references/prompt-control-plane.md +9 -5
@@ -1,6 +1,6 @@
1
1
  // ORI-2044: moved from index.ts to keep dataset commands together.
2
2
 
3
- import { readFileSync, statSync, writeFileSync } from 'fs'
3
+ import { readFileSync, writeFileSync } from 'fs'
4
4
  import { basename, extname } from 'path'
5
5
  import { createInterface } from 'readline/promises'
6
6
  import { stdin as input, stdout as output } from 'process'
@@ -28,7 +28,6 @@ import { printDatasetSummaries } from './archive-artifacts-cli.js'
28
28
  import { formatTerminalLink } from './auth-commands.js'
29
29
  import { parseDatasetFile } from './file-parser.js'
30
30
  import { parseDatasetReference } from './dataset-download.js'
31
- import { streamJsonlRowChunks } from './jsonl-stream.js'
32
31
  import { editDatasetRows as runEditDatasetRows } from './dataset-edit-rows.js'
33
32
  import { parseCommaSeparated } from './task-commands.js'
34
33
  import type { DatasetSelection } from './account-directory.js'
@@ -432,175 +431,16 @@ async function downloadDataset() {
432
431
  printLine(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
433
432
  }
434
433
 
435
- const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB
436
- const APPEND_CHUNK_SIZE_ROWS = 500
437
- async function appendChunk(
438
- datasetId: string,
439
- rows: Array<Record<string, unknown>>
440
- ): Promise<{ dataset: { id: string; name: string; rowCount: number }; appendedCount: number }> {
441
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
442
- method: 'POST',
443
- headers: { 'Content-Type': 'application/json' },
444
- body: JSON.stringify({ rows }),
445
- })
446
-
447
- if (!response.ok) {
448
- throw new Error(`Append failed: ${await response.text()}`)
449
- }
450
-
451
- return parseJsonResponse<{
452
- dataset: { id: string; name: string; rowCount: number }
453
- appendedCount: number
454
- }>(response, 'Dataset append')
455
- }
456
-
457
- async function appendJsonlDatasetRowsInChunks(datasetId: string, file: string) {
458
- let totalAppended = 0
459
- let lastResult: { dataset: { id: string; name: string; rowCount: number } } | null = null
460
- let chunkIndex = 0
461
- const chunks = streamJsonlRowChunks(file)[Symbol.asyncIterator]()
462
-
463
- while (true) {
464
- let nextChunk: IteratorResult<Array<Record<string, unknown>>>
465
- try {
466
- nextChunk = await chunks.next()
467
- } catch (error) {
468
- const message = error instanceof Error ? error.message : String(error)
469
- throw new Error(
470
- `Append stopped while reading the next JSONL chunk: ${message}\n` +
471
- `${totalAppended} rows from ${chunkIndex} chunk(s) were already appended. ` +
472
- `Fix the file, remove the first ${totalAppended} rows, and re-run the command.`
473
- )
474
- }
475
-
476
- if (nextChunk.done) {
477
- break
478
- }
479
-
480
- const chunk = nextChunk.value
481
- chunkIndex += 1
482
- printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
483
-
484
- try {
485
- const data = await appendChunk(datasetId, chunk)
486
- totalAppended += data.appendedCount
487
- lastResult = data
488
- } catch (error) {
489
- const message = error instanceof Error ? error.message : String(error)
490
- throw new Error(
491
- `Chunk ${chunkIndex} failed: ${message}\n` +
492
- `${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
493
- `To retry, remove the first ${totalAppended} rows from your file and re-run the command.`
494
- )
495
- }
496
- }
497
-
498
- if (!lastResult) {
499
- throw new Error('Dataset append file must contain at least one row')
500
- }
501
-
502
- printLine(
503
- `Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
504
- )
505
- }
506
-
507
434
  async function appendDatasetRows() {
508
- const projectArg = getArg('--project')
509
- const datasetInput = getDatasetReferenceInput()
510
- const fileArg = getArg('--file')
511
-
512
- if (!fileArg) {
513
- throw new Error('Usage: orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>')
514
- }
515
-
516
- let datasetId: string
517
- if (datasetInput) {
518
- datasetId = parseDatasetReference(datasetInput).datasetId
519
- } else {
520
- const selected = await selectDatasetInteractively(projectArg)
521
- datasetId = selected.datasetId
522
- }
523
-
524
- const file = expandHomePath(fileArg)
525
-
526
- if (extname(file).toLowerCase() === '.jsonl') {
527
- await appendJsonlDatasetRowsInChunks(datasetId, file)
528
- return
529
- }
530
-
531
- // Check file size before reading to prevent OOM on large files (ALI-565).
532
- // Wrap statSync in try/catch so missing/inaccessible files get friendly
533
- // errors instead of raw Node.js ENOENT/EPERM (ALI-554).
534
- let fileSizeBytes: number
535
- try {
536
- fileSizeBytes = statSync(file).size
537
- } catch (error) {
538
- const maybeError = error as NodeJS.ErrnoException
539
- if (maybeError.code === 'ENOENT') {
540
- throw new Error(
541
- `File not found: ${file}. Check the path and filename, then retry.`
542
- )
543
- }
544
- if (maybeError.code === 'EPERM' || maybeError.code === 'EACCES') {
545
- throw new Error(
546
- `Cannot read file: ${file}. Grant folder permission to your terminal app and retry.`
547
- )
548
- }
549
- throw new Error(`Failed to access file ${file}: ${maybeError.message}`)
550
- }
551
- if (fileSizeBytes > MAX_INPUT_FILE_SIZE_BYTES) {
552
- const sizeMb = (fileSizeBytes / (1024 * 1024)).toFixed(1)
553
- throw new Error(
554
- `Input file is ${sizeMb} MB, which exceeds the 50 MB limit. Split the file into smaller parts and append each separately.`
555
- )
556
- }
557
-
558
- const { rows } = parseDatasetFile(file)
559
- if (!Array.isArray(rows) || rows.length === 0) {
560
- throw new Error('Dataset append file must contain at least one row')
561
- }
562
-
563
- if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
564
- const data = await appendChunk(datasetId, rows)
565
- printLine(
566
- `Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
567
- )
568
- return
569
- }
570
-
571
- // Chunked upload for large row counts (ALI-555: track partial progress)
572
- let totalAppended = 0
573
- let lastResult: { dataset: { id: string; name: string; rowCount: number } } | null = null
574
- const totalChunks = Math.ceil(rows.length / APPEND_CHUNK_SIZE_ROWS)
575
-
576
- for (let offset = 0; offset < rows.length; offset += APPEND_CHUNK_SIZE_ROWS) {
577
- const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS)
578
- const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1
579
-
580
- printLine(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`)
581
- try {
582
- const data = await appendChunk(datasetId, chunk)
583
- totalAppended += data.appendedCount
584
- lastResult = data
585
- } catch (error) {
586
- const msg = error instanceof Error ? error.message : String(error)
587
- throw new Error(
588
- `Chunk ${chunkIndex}/${totalChunks} failed: ${msg}\n` +
589
- `${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
590
- `To retry, remove the first ${totalAppended} rows from your file and re-run the command.`
591
- )
592
- }
593
- }
594
-
595
- if (lastResult) {
596
- if (hasJsonFlag()) {
597
- printJson({ dataset: lastResult.dataset, appendedCount: totalAppended })
598
- return
599
- }
600
- printLine(
601
- `Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
602
- )
603
- }
435
+ const { appendDatasetWorkflow } = await import('./dataset-append-workflow.js')
436
+ process.exitCode = await appendDatasetWorkflow(async () => {
437
+ const projectArg = getArg('--project')
438
+ const datasetInput = getDatasetReferenceInput()
439
+ const fileArg = getArg('--file')
440
+ if (!fileArg) throw new Error('Usage: orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>')
441
+ const datasetId = datasetInput ? parseDatasetReference(datasetInput).datasetId : (await selectDatasetInteractively(projectArg)).datasetId
442
+ return { datasetId, file: expandHomePath(fileArg) }
443
+ }, { json: hasJsonFlag(), print: printLine })
604
444
  }
605
445
 
606
446
  async function editDatasetRows() {
@@ -1,4 +1,4 @@
1
- import { Effect, Either, Schedule } from 'effect'
1
+ import { Effect, Result, Schedule } from 'effect'
2
2
  import { writeFileSync } from 'node:fs'
3
3
  import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
4
4
  import { authedFetch } from './http.js'
@@ -29,9 +29,9 @@ export async function transferDatasetDownload(datasetId: string, format: string,
29
29
  if (cause instanceof TypeError) return new DownloadTransportError(cause)
30
30
  return cause instanceof Error ? cause : new Error('Dataset download request failed', { cause })
31
31
  },
32
- }).pipe(Effect.timeoutFail({
32
+ }).pipe(Effect.timeoutOrElse({
33
33
  duration: timeoutMs,
34
- onTimeout: () => new Error('DATASET_TRANSFER_TIMEOUT: Dataset download exceeded its deadline'),
34
+ orElse: () => Effect.fail(new Error('DATASET_TRANSFER_TIMEOUT: Dataset download exceeded its deadline')),
35
35
  }), Effect.retry({
36
36
  times: 2,
37
37
  schedule: Schedule.spaced(100),
@@ -49,8 +49,8 @@ export async function transferDatasetDownload(datasetId: string, format: string,
49
49
  process.once('SIGINT', handleInterrupt)
50
50
  process.once('SIGTERM', handleInterrupt)
51
51
  try {
52
- const result = await Effect.runPromise(Effect.either(Effect.flatMap(download, write)), { signal: controller.signal })
53
- if (Either.isLeft(result)) throw result.left
52
+ const result = await Effect.runPromise(Effect.result(Effect.flatMap(download, write)), { signal: controller.signal })
53
+ if (Result.isFailure(result)) throw result.failure
54
54
  } catch (error) {
55
55
  if (controller.signal.aborted) throw new Error('DATASET_TRANSFER_INTERRUPTED: Dataset download cancelled')
56
56
  throw error
@@ -1,4 +1,4 @@
1
- import { Context, Effect, Either, Layer } from 'effect'
1
+ import { Context, Effect, Layer, Result } from 'effect'
2
2
 
3
3
  import { ensureDatasetUploadSnapshot } from './dataset-upload-snapshot.js'
4
4
  import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
@@ -43,7 +43,7 @@ interface DatasetUploadApi {
43
43
  append: (id: string, rows: UploadRows) => Effect.Effect<DatasetAppendResponse, DatasetWriteFailure>
44
44
  snapshot: (id: string) => Effect.Effect<DatasetSnapshot, DatasetWriteFailure>
45
45
  }
46
- const DatasetUploadApi = Context.GenericTag<DatasetUploadApi>('orizu/DatasetUploadApi')
46
+ const DatasetUploadApi = Context.Service<DatasetUploadApi>('orizu/DatasetUploadApi')
47
47
 
48
48
  // Only the existing authentication client's rejected-token refresh may replay a
49
49
  // request. No transient/transport retries belong around these non-idempotent writes.
@@ -64,9 +64,9 @@ function post<T>(path: string, body: unknown, context: string, prefix: string, t
64
64
  catch: cause => cause instanceof DatasetWriteFailure ? cause : new DatasetWriteFailure(
65
65
  cause instanceof Error ? cause.message : String(cause), requestStarted ? 'unknown' : 'rejected', { cause },
66
66
  ),
67
- }).pipe(Effect.timeoutFail({
67
+ }).pipe(Effect.timeoutOrElse({
68
68
  duration: timeoutMs,
69
- onTimeout: () => new DatasetWriteFailure(`DATASET_TRANSFER_TIMEOUT: ${context} exceeded its deadline`, requestStarted ? 'unknown' : 'rejected'),
69
+ orElse: () => Effect.fail(new DatasetWriteFailure(`DATASET_TRANSFER_TIMEOUT: ${context} exceeded its deadline`, requestStarted ? 'unknown' : 'rejected')),
70
70
  }))
71
71
  })
72
72
  }
@@ -84,9 +84,9 @@ function liveUploadApi(options: DatasetUploadOptions) {
84
84
  }
85
85
 
86
86
  async function expectedPromise<A, E>(program: Effect.Effect<A, E>): Promise<A> {
87
- const result = await Effect.runPromise(Effect.either(program))
88
- if (Either.isLeft(result)) throw result.left
89
- return result.right
87
+ const result = await Effect.runPromise(Effect.result(program))
88
+ if (Result.isFailure(result)) throw result.failure
89
+ return result.success
90
90
  }
91
91
 
92
92
  function uploadProgram(options: DatasetUploadOptions) {
@@ -63,6 +63,9 @@ const SAFE_REMOTE_COMPONENT = /^[^\u0000-\u0020\u007f\\?#]+$/
63
63
  export const REPO_TOKEN_PURPOSES = [
64
64
  'read',
65
65
  'write',
66
+ // ORI-2280: a hosted session's read key for its team copy, used by the
67
+ // `orizu-team` remote a publish conflict adds. Artifacts remotes only.
68
+ 'team_read',
66
69
  ] as const
67
70
 
68
71
  export type RepoTokenPurpose =
@@ -375,7 +378,7 @@ async function mintArtifactsCredential(
375
378
  {
376
379
  method: 'POST',
377
380
  headers: { 'Content-Type': 'application/json' },
378
- body: JSON.stringify({ purpose }),
381
+ body: JSON.stringify({ purpose, ...(purpose === 'team_read' ? { sessionId: process.env.ORIZU_SESSION_ID?.trim() } : {}) }),
379
382
  }
380
383
  )
381
384
 
@@ -541,6 +544,10 @@ export async function runGitCredential(op: string, io: GitCredentialIo): Promise
541
544
  )
542
545
  return 1
543
546
  }
547
+ if (io.purpose === 'team_read') {
548
+ io.printErr('orizu git-credential: team_read is only for an Artifacts team copy')
549
+ return 1
550
+ }
544
551
  const primaryPurpose = io.purpose ?? 'write'
545
552
  let minted = await mintToken(
546
553
  fetcher,
package/src/help.ts CHANGED
@@ -84,8 +84,8 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
84
84
  { path: ['instructions', 'profiles', 'rollback'], aliases: [['instruction-sets', 'profiles', 'rollback']], usage: 'orizu instructions profiles rollback <set> --project <team/project> --model-config <identity> --to <n> [--json]', summary: 'Create a rollback version and move profile production to it.', group: 'Instruction sets', options: [{ name: '--model-config <identity>', help: 'Profile model config.', required: true }, { name: '--to <n>', help: 'Historical profile version to copy.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions profiles rollback planner --project core/evals --model-config anthropic/claude-haiku --to 1'] },
85
85
  { path: ['instructions', 'default', 'show'], aliases: [['instruction-sets', 'default', 'show']], usage: 'orizu instructions default show <set> --project <team/project> [--json]', summary: 'Show the Default Profile and its Production state.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions default show planner --project core/evals'] },
86
86
  { path: ['instructions', 'default', 'move'], aliases: [['instruction-sets', 'default', 'move']], usage: 'orizu instructions default move <set> --project <team/project> --model-config <identity> [--json]', summary: 'Move the instruction-set Default to a Profile.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--model-config <identity>', help: 'Target Profile identity.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions default move planner --project core/evals --model-config anthropic/claude-haiku'] },
87
- { path: ['instructions', 'shape', 'add'], aliases: [['instruction-sets', 'shape', 'add']], usage: 'orizu instructions shape add <set> --project <team/project> --key <key> --from <manifest> [--json]', summary: 'Add one component key to every instruction-set profile.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--key <key>', help: 'Component key to add.', required: true }, { name: '--from <manifest>', help: 'Manifest containing the component body.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions shape add planner --project core/evals --key skill --from planner.json'] },
88
- { path: ['instructions', 'shape', 'remove'], aliases: [['instruction-sets', 'shape', 'remove']], usage: 'orizu instructions shape remove <set> --project <team/project> --key <key> [--json]', summary: 'Remove one component key from every instruction-set profile.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--key <key>', help: 'Component key to remove.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions shape remove planner --project core/evals --key skill'] },
87
+ { path: ['instructions', 'shape', 'add'], aliases: [['instruction-sets', 'shape', 'add']], usage: 'orizu instructions shape add <set> --project <team/project> --key <key> --from <manifest> [--json]', summary: 'Add a component before first promotion; a frozen shape requires a new instruction set.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--key <key>', help: 'Component key to add.', required: true }, { name: '--from <manifest>', help: 'Manifest containing the component body.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions shape add planner --project core/evals --key skill --from planner.json'] },
88
+ { path: ['instructions', 'shape', 'remove'], aliases: [['instruction-sets', 'shape', 'remove']], usage: 'orizu instructions shape remove <set> --project <team/project> --key <key> [--json]', summary: 'Remove a component before first promotion; a frozen shape requires a new instruction set.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--key <key>', help: 'Component key to remove.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions shape remove planner --project core/evals --key skill'] },
89
89
  {
90
90
  path: ['instructions', 'sync'],
91
91
  aliases: [['instruction-sets', 'sync']],
@@ -742,7 +742,7 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
742
742
  },
743
743
  {
744
744
  path: ['apps', 'create'],
745
- usage: 'orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]',
745
+ usage: 'orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>] [--id <app-id>]',
746
746
  summary: 'Create a review app from a local component and schema files.',
747
747
  group: 'Apps',
748
748
  examples: [
@@ -34,6 +34,7 @@ import { spawnSync } from 'child_process'
34
34
  import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from 'fs'
35
35
 
36
36
  import { isValidCloudflareArtifactsGitRemote } from './cloudflare-artifacts-git-remote.js'
37
+ import { seedSessionCopy } from './hosted-session-seed.js'
37
38
  import {
38
39
  AGENT_GIT_IDENTITY,
39
40
  BEARER_BASENAME,
@@ -681,6 +682,30 @@ export async function beginRunExecution(opts: {
681
682
  throw new Error(`run_started transition failed: ${detail}`)
682
683
  }
683
684
 
685
+ /** Mint a `team_read` key (ORI-2280 B1) to learn this session's team copy, so
686
+ * the boot can fill an empty session copy from it (ORI-2118). The credential
687
+ * helper mints its own keys for the git commands; this one only names the
688
+ * remote and lapses on its own (300 s). */
689
+ export async function resolveTeamRemote(opts: {
690
+ baseUrl: string
691
+ workspaceId: string
692
+ sessionId: string
693
+ bearer: string
694
+ fetchImpl: BootFetch
695
+ }): Promise<string> {
696
+ const url = `${opts.baseUrl}/api/cli/workspaces/${encodeURIComponent(opts.workspaceId)}/repo-token`
697
+ const minted = await bearerJson(opts.fetchImpl, url, opts.bearer, {
698
+ method: 'POST',
699
+ headers: { 'Content-Type': 'application/json' },
700
+ body: JSON.stringify({ purpose: 'team_read', sessionId: opts.sessionId }),
701
+ })
702
+ const remote = asString(minted.remote)
703
+ if (asString(minted.provider) !== 'cloudflare_artifacts' || !remote || !isValidCloudflareArtifactsGitRemote(remote)) {
704
+ throw new Error('team_read response carried no valid Artifacts remote')
705
+ }
706
+ return remote
707
+ }
708
+
684
709
  /** Mint a session_read repo token to learn the repo full name, then build the
685
710
  * GitHub clone URL (mirrors the operator path's `defaultResolveRepo`). */
686
711
  export async function resolveRepo(opts: {
@@ -740,6 +765,10 @@ export interface BootExecResult {
740
765
  }
741
766
  export type BootExec = (cmd: string, args: string[], opts?: { cwd?: string }) => BootExecResult
742
767
 
768
+ /** The credential-helper context for the team copy, used only to fill an
769
+ * empty session copy (ORI-2118). */
770
+ const TEAM_BOOT_CONTEXT_BASENAME = 'boot-team.json'
771
+
743
772
  const defaultExec: BootExec = (cmd, args, opts) => {
744
773
  const res = spawnSync(cmd, args, { cwd: opts?.cwd, encoding: 'utf8' })
745
774
  return { status: res.status ?? 1, stdout: res.stdout ?? '', stderr: res.stderr ?? '' }
@@ -1037,9 +1066,35 @@ export async function runHostedBoot(opts: RunHostedBootOptions): Promise<HostedB
1037
1066
  }
1038
1067
  writeFile(bootContextAbs, serializeBootContext(bootContext))
1039
1068
 
1069
+ const helperValue = `!node ${helperScriptAbs} ${bootContextAbs}`
1070
+
1071
+ // 4b — ORI-2118: a hosted session's Artifacts copy is created empty (forks
1072
+ // of a busy team copy cannot be cloned), so fill it from the team copy
1073
+ // before the first clone. A copy that already has `main` is left alone.
1074
+ if (isValidCloudflareArtifactsGitRemote(repo.cloneUrl)) {
1075
+ const credentialConfig = (helper: string) => [`credential.helper=${helper}`, 'credential.useHttpPath=true']
1076
+ await seedSessionCopy({
1077
+ exec,
1078
+ sessionRemote: repo.cloneUrl,
1079
+ sessionGitConfig: credentialConfig(helperValue),
1080
+ scratchDir: `${sessionDirAbs}/seed.git`,
1081
+ resolveTeam: async () => {
1082
+ const teamRemote = await resolveTeamRemote({ baseUrl: env.baseUrl, workspaceId, sessionId: env.sessionId, bearer: bearer.token, fetchImpl })
1083
+ assertSafeGitValue('teamRemote', teamRemote)
1084
+ const teamContextAbs = `${sessionDirAbs}/${TEAM_BOOT_CONTEXT_BASENAME}`
1085
+ writeFile(teamContextAbs, serializeBootContext({
1086
+ ...bootContext,
1087
+ repoFullName: teamRemote,
1088
+ host: new URL(teamRemote).host.toLowerCase(),
1089
+ tokenPurposes: { primary: 'team_read', fallback: 'team_read' },
1090
+ }))
1091
+ return { remote: teamRemote, gitConfig: credentialConfig(`!node ${helperScriptAbs} ${teamContextAbs}`) }
1092
+ },
1093
+ })
1094
+ }
1095
+
1040
1096
  // 5 — Clone the session branch VIA the pull-mode credential helper (same
1041
1097
  // invocation the operator path makes; no token in the URL/config).
1042
- const helperValue = `!node ${helperScriptAbs} ${bootContextAbs}`
1043
1098
  const clone = exec('git', [
1044
1099
  'clone',
1045
1100
  '--depth',
@@ -59,6 +59,7 @@ import { DEFAULT_HOSTED_MODEL, hostedLoopCommand, type HostedLoopContext } from
59
59
  import { hostedBootCommand } from './hosted-boot.js'
60
60
  import { mergeJobCommand } from './merge-job.js'
61
61
  import { workspaceBootstrapJobCommand } from './workspace-bootstrap-job.js'
62
+ import { publishMergeJobCommand } from './publish-merge-job.js'
62
63
  import { hostedOptimizationCommand } from './hosted-optimization.js'
63
64
  import { writeChildStderr } from './child-output-tee.js'
64
65
  import { skilledProposerBakeCommand } from './skilled-proposer-launch.js'
@@ -1557,12 +1558,16 @@ export async function hostedCommand(
1557
1558
  if (positional[1] === 'workspace-bootstrap-job') {
1558
1559
  return workspaceBootstrapJobCommand(args, io)
1559
1560
  }
1561
+ // ORI-2280: the one-shot publish merge (coordinator publish-merge job).
1562
+ if (positional[1] === 'publish-merge-job') {
1563
+ return publishMergeJobCommand(args, io)
1564
+ }
1560
1565
  if (positional[1] === 'hosted-optimization') {
1561
1566
  return hostedOptimizationCommand({ ...io, printErr: writeChildStderr })
1562
1567
  }
1563
1568
  if (positional[1] === 'bake-skilled-proposer-venv') return skilledProposerBakeCommand(io)
1564
1569
  if (positional[1] === 'verify-skilled-proposer-bake') return skilledProposerBakeCommand(io, true)
1565
- io.printErr?.('Usage: orizu internal <hosted-loop --context <path> | hosted-boot | hosted-optimization | merge-job | workspace-bootstrap-job | bake-skilled-proposer-venv | verify-skilled-proposer-bake>')
1570
+ io.printErr?.('Usage: orizu internal <hosted-loop --context <path> | hosted-boot | hosted-optimization | merge-job | workspace-bootstrap-job | publish-merge-job | bake-skilled-proposer-venv | verify-skilled-proposer-bake>')
1566
1571
  return 1
1567
1572
  }
1568
1573
 
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Fills an empty hosted session copy from its team copy (ORI-2118).
3
+ *
4
+ * Artifacts forks of a team copy whose history has a few file-adding commits
5
+ * cannot be cloned or pushed to (measured 2026-10-03, binding and REST). The
6
+ * broker therefore creates the session copy EMPTY, and the sandbox boot fills
7
+ * it here before its first clone: a bare clone of the team copy's `main`, then
8
+ * a push of that `main` into the session copy. A Worker cannot run git, so
9
+ * this is the first place in session start that can.
10
+ *
11
+ * It fills a copy only when the copy has no branches at all, which is what a
12
+ * freshly created copy looks like. A retried or resumed boot finds `main` and
13
+ * does nothing, so it never overwrites the agent's work, and a working
14
+ * repository whose content lives on another branch is never touched. The
15
+ * push is never forced; if another boot filled the copy first, that is fine.
16
+ * Credentials come only through the `-c` settings the caller passes (the
17
+ * boot's credential helper), never in a URL or argument.
18
+ */
19
+
20
+ export const SESSION_COPY_SEED_FAILURE = 'session copy could not be filled from the team copy'
21
+
22
+ interface SeedExecResult {
23
+ status: number
24
+ stdout: string
25
+ stderr: string
26
+ }
27
+ type SeedExec = (cmd: string, args: string[], opts?: { cwd?: string }) => SeedExecResult
28
+
29
+ export interface SeedSessionCopyInput {
30
+ exec: SeedExec
31
+ sessionRemote: string
32
+ /** `git -c` settings that authenticate to the session copy. */
33
+ sessionGitConfig: string[]
34
+ /** Called only when the copy is empty: the team copy and its `-c` settings. */
35
+ resolveTeam: () => Promise<{ remote: string; gitConfig: string[] }>
36
+ /** A path owned only by this seed operation; cleared before and after use. */
37
+ scratchDir: string
38
+ }
39
+
40
+ function withConfig(config: string[]): string[] {
41
+ return config.flatMap(setting => ['-c', setting])
42
+ }
43
+
44
+ function failure(step: string, result: SeedExecResult): Error {
45
+ const detail = (result.stderr || result.stdout || `exit ${result.status}`).trim()
46
+ return new Error(`${SESSION_COPY_SEED_FAILURE}: ${step}: ${detail}`)
47
+ }
48
+
49
+ /** Whether the session copy has any branch. Measured on Artifacts: an empty
50
+ * repository answers `ls-remote` with exit 0 and no output. Any non-zero
51
+ * exit is an error, never "empty". */
52
+ function sessionHasBranches(input: SeedSessionCopyInput): boolean {
53
+ const listed = input.exec('git', [...withConfig(input.sessionGitConfig), 'ls-remote', '--heads', input.sessionRemote])
54
+ if (listed.status !== 0) throw failure('reading the session copy', listed)
55
+ return listed.stdout.trim().length > 0
56
+ }
57
+
58
+ export async function seedSessionCopy(input: SeedSessionCopyInput): Promise<{ seeded: boolean }> {
59
+ if (sessionHasBranches(input)) return { seeded: false }
60
+ const team = await input.resolveTeam()
61
+ try {
62
+ // A sandbox shutdown can interrupt clone before finally runs. This path
63
+ // belongs only to seeding; never clear the surrounding session directory.
64
+ const cleared = input.exec('rm', ['-rf', '--', input.scratchDir])
65
+ if (cleared.status !== 0) throw failure('clearing the seed scratch', cleared)
66
+ const fetched = input.exec('git', [...withConfig(team.gitConfig), 'clone', '--quiet', '--bare', '--single-branch', '--branch', 'main', team.remote, input.scratchDir])
67
+ if (fetched.status !== 0) throw failure('reading the team copy', fetched)
68
+ const pushed = input.exec('git', ['-C', input.scratchDir, ...withConfig(input.sessionGitConfig), 'push', '--quiet', input.sessionRemote, 'refs/heads/main:refs/heads/main'])
69
+ if (pushed.status !== 0) {
70
+ // Another boot of the same session may have filled it first.
71
+ if (sessionHasBranches(input)) return { seeded: false }
72
+ throw failure('pushing into the session copy', pushed)
73
+ }
74
+ return { seeded: true }
75
+ } finally {
76
+ input.exec('rm', ['-rf', '--', input.scratchDir])
77
+ }
78
+ }
@@ -5,6 +5,7 @@ export const JSONL_UPLOAD_CHUNK_MAX_BYTES = 2 * 1024 * 1024
5
5
 
6
6
  interface JsonlChunkOptions {
7
7
  maxPayloadBytes?: number
8
+ signal?: AbortSignal
8
9
  }
9
10
 
10
11
  interface PendingRow {
@@ -66,11 +67,17 @@ export async function* streamJsonlRowChunks(
66
67
  let parsedRowCount = 0
67
68
  let lineNumber = 0
68
69
 
70
+ if (options.signal?.aborted) throw new Error('Dataset input interrupted')
71
+
69
72
  const stream = createReadStream(filePath, { encoding: 'utf8' })
70
73
  const lines = createInterface({
71
74
  input: stream,
72
75
  crlfDelay: Infinity,
73
76
  })
77
+ // Closing the owned reader unblocks an outstanding next() before a scoped
78
+ // caller awaits return(); return alone would queue behind that pending read.
79
+ const interrupt = () => { lines.close(); stream.destroy() }
80
+ options.signal?.addEventListener('abort', interrupt, { once: true })
74
81
 
75
82
  try {
76
83
  for await (const line of lines) {
@@ -117,6 +124,7 @@ export async function* streamJsonlRowChunks(
117
124
  } catch (error) {
118
125
  throw mapReadError(filePath, error)
119
126
  } finally {
127
+ options.signal?.removeEventListener('abort', interrupt)
120
128
  lines.close()
121
129
  stream.destroy()
122
130
  }
@@ -0,0 +1,108 @@
1
+ import { Context, Effect, Result, Schema } from 'effect'
2
+
3
+ import { authedFetch } from './http.js'
4
+ import { providerWireRecord as record } from './provider-wire-record.js'
5
+ import { isProviderRefusalCode } from './provider-refusals.js'
6
+
7
+ import type { ModelConfigsCommandIo } from './model-configs-cli.js'
8
+
9
+ type ProviderListFailure =
10
+ | { readonly _tag: 'Http'; readonly message: string }
11
+ | { readonly _tag: 'Transport' }
12
+ | { readonly _tag: 'Resolution'; readonly message: string }
13
+ | { readonly _tag: 'Adapter'; readonly message: string }
14
+
15
+ function providerListFailureMessage(failure: ProviderListFailure): string {
16
+ switch (failure._tag) {
17
+ case 'Http': return failure.message
18
+ case 'Transport': return 'Provider request failed'
19
+ // Project selection remains the existing, separately owned adapter.
20
+ case 'Resolution': case 'Adapter': return failure.message
21
+ default: { const exhaustive: never = failure; return exhaustive }
22
+ }
23
+ }
24
+
25
+ const ProviderListTransport = Context.Service<{
26
+ readonly read: (project: string) => Effect.Effect<Response, ProviderListFailure>
27
+ }>('orizu/cli/ProviderListTransport')
28
+
29
+ const WireRecord = Schema.Record(Schema.String, Schema.Unknown)
30
+ const emptyWireRecord: Record<string, unknown> = {}
31
+
32
+ function providerListProgram(project: string, io: ModelConfigsCommandIo) {
33
+ return Effect.gen(function*() {
34
+ const transport = yield* ProviderListTransport
35
+ const response = yield* transport.read(project)
36
+ // Decode raw wire data; malformed JSON and non-record bodies retain the
37
+ // existing empty record fallback without narrowing accepted record fields.
38
+ const value = yield* Effect.promise(() => response.json().catch(() => null))
39
+ const body = yield* Schema.decodeUnknownEffect(WireRecord)(value).pipe(
40
+ Effect.catch(() => Effect.succeed(emptyWireRecord)),
41
+ )
42
+ if (!response.ok) {
43
+ const message = typeof body.error === 'string' && isProviderRefusalCode(body.error) ? body.error : 'Provider request failed'
44
+ return yield* Effect.fail<ProviderListFailure>({ _tag: 'Http', message })
45
+ }
46
+ yield* Effect.sync(() => {
47
+ if (io.json) io.print(JSON.stringify(body))
48
+ else {
49
+ io.print('ID PROTOCOL HOST CREDENTIAL ENV BUILT-IN')
50
+ for (const value of Array.isArray(body.providers) ? body.providers : []) {
51
+ const row = record(value)
52
+ io.print(`${row.id} ${row.protocol} ${row.host} ${row.credentialEnv} ${row.builtIn ? 'yes' : 'no'}`)
53
+ }
54
+ }
55
+ })
56
+ })
57
+ }
58
+
59
+ export async function listProviders(projectArg: string | null, io: ModelConfigsCommandIo): Promise<number> {
60
+ const controller = new AbortController()
61
+ let interrupted: 130 | 143 | undefined
62
+ const interrupt = () => { interrupted = 130; controller.abort() }
63
+ const terminate = () => { interrupted = 143; controller.abort() }
64
+ const transport = {
65
+ read: (project: string) => Effect.tryPromise({
66
+ try: signal => (io.fetcher ?? authedFetch)(`/api/cli/providers?project=${encodeURIComponent(project)}`, {
67
+ method: 'GET', signal: AbortSignal.any([signal, controller.signal]),
68
+ }),
69
+ // The retained HTTP adapter exposes login/config/refresh diagnostics as
70
+ // untyped Error values. Preserve its public messages until ORI-2420
71
+ // supplies typed adapter failures; never expose nested transport causes.
72
+ catch: (cause): ProviderListFailure => cause instanceof Error
73
+ ? { _tag: 'Adapter', message: cause.message }
74
+ : { _tag: 'Transport' },
75
+ }),
76
+ }
77
+ const program = Effect.gen(function*() {
78
+ const project = yield* Effect.tryPromise({
79
+ try: async () => {
80
+ const project = await io.resolveProjectSlug?.(projectArg)
81
+ if (!project) throw new Error('Project resolver unavailable')
82
+ return project
83
+ },
84
+ catch: (cause): ProviderListFailure => ({ _tag: 'Resolution', message: cause instanceof Error ? cause.message : 'Provider request failed' }),
85
+ })
86
+ // The retained resolver is unsignalled. Do not suppress Node's natural
87
+ // termination until the cancellable catalog transport has been admitted.
88
+ return yield* Effect.acquireUseRelease(
89
+ Effect.sync(() => { process.once('SIGINT', interrupt); process.once('SIGTERM', terminate) }),
90
+ () => providerListProgram(project, io).pipe(Effect.provideService(ProviderListTransport, transport)),
91
+ () => Effect.sync(() => {
92
+ process.removeListener('SIGINT', interrupt)
93
+ process.removeListener('SIGTERM', terminate)
94
+ }),
95
+ )
96
+ })
97
+ try {
98
+ const result = await Effect.runPromise(Effect.result(program), { signal: controller.signal })
99
+ if (Result.isSuccess(result)) return 0
100
+ const message = providerListFailureMessage(result.failure)
101
+ io.print(io.json ? JSON.stringify({ error: message }) : message)
102
+ return 1
103
+ } catch {
104
+ if (interrupted !== undefined) return interrupted
105
+ io.print(io.json ? JSON.stringify({ error: 'Provider request failed' }) : 'Provider request failed')
106
+ return 1
107
+ }
108
+ }