orizu 0.5.20 → 0.5.21

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 (260) hide show
  1. package/dist/archive-artifacts-cli.d.ts +50 -0
  2. package/dist/archive-artifacts-cli.d.ts.map +1 -0
  3. package/dist/artifact-archive.d.ts +48 -0
  4. package/dist/artifact-archive.d.ts.map +1 -0
  5. package/dist/artifact-pull.d.ts +22 -0
  6. package/dist/artifact-pull.d.ts.map +1 -0
  7. package/dist/banner.d.ts +3 -0
  8. package/dist/banner.d.ts.map +1 -0
  9. package/dist/connectors-cli.d.ts +31 -0
  10. package/dist/connectors-cli.d.ts.map +1 -0
  11. package/dist/credentials.d.ts +46 -0
  12. package/dist/credentials.d.ts.map +1 -0
  13. package/dist/csv.d.ts +49 -0
  14. package/dist/csv.d.ts.map +1 -0
  15. package/dist/dataset-download.d.ts +6 -0
  16. package/dist/dataset-download.d.ts.map +1 -0
  17. package/dist/dataset-edit-rows.d.ts +10 -0
  18. package/dist/dataset-edit-rows.d.ts.map +1 -0
  19. package/dist/dataset-upload-snapshot.d.ts +13 -0
  20. package/dist/dataset-upload-snapshot.d.ts.map +1 -0
  21. package/dist/daytona-slice-rehearsal.d.ts +89 -0
  22. package/dist/daytona-slice-rehearsal.d.ts.map +1 -0
  23. package/dist/daytona-slice.d.ts +106 -0
  24. package/dist/daytona-slice.d.ts.map +1 -0
  25. package/dist/deploy-key-merge-core.d.ts +121 -0
  26. package/dist/deploy-key-merge-core.d.ts.map +1 -0
  27. package/dist/diff-comments-cli.d.ts +12 -0
  28. package/dist/diff-comments-cli.d.ts.map +1 -0
  29. package/dist/diff-comments-cli.js +3 -0
  30. package/dist/egress-allowlist-cli.d.ts +23 -0
  31. package/dist/egress-allowlist-cli.d.ts.map +1 -0
  32. package/dist/egress-policy.d.ts +108 -0
  33. package/dist/egress-policy.d.ts.map +1 -0
  34. package/dist/error-response.d.ts +9 -0
  35. package/dist/error-response.d.ts.map +1 -0
  36. package/dist/file-parser.d.ts +5 -0
  37. package/dist/file-parser.d.ts.map +1 -0
  38. package/dist/gepa-engine-dispatch.d.ts +9 -0
  39. package/dist/gepa-engine-dispatch.d.ts.map +1 -0
  40. package/dist/gepa-engine-dispatch.js +48 -2
  41. package/dist/gepa-python-command.d.ts +6 -0
  42. package/dist/gepa-python-command.d.ts.map +1 -0
  43. package/dist/gepa-python-paths.d.ts +6 -0
  44. package/dist/gepa-python-paths.d.ts.map +1 -0
  45. package/dist/gepa-python-paths.js +6 -3
  46. package/dist/git-credential.d.ts +69 -0
  47. package/dist/git-credential.d.ts.map +1 -0
  48. package/dist/git-recovery-environment.d.ts +31 -0
  49. package/dist/git-recovery-environment.d.ts.map +1 -0
  50. package/dist/github-setup.d.ts +122 -0
  51. package/dist/github-setup.d.ts.map +1 -0
  52. package/dist/global-flags.d.ts +12 -0
  53. package/dist/global-flags.d.ts.map +1 -0
  54. package/dist/help.d.ts +46 -0
  55. package/dist/help.d.ts.map +1 -0
  56. package/dist/help.js +91 -23
  57. package/dist/hosted-boot.d.ts +273 -0
  58. package/dist/hosted-boot.d.ts.map +1 -0
  59. package/dist/hosted-bootstrap-rehearsal.d.ts +86 -0
  60. package/dist/hosted-bootstrap-rehearsal.d.ts.map +1 -0
  61. package/dist/hosted-bootstrap.d.ts +188 -0
  62. package/dist/hosted-bootstrap.d.ts.map +1 -0
  63. package/dist/hosted-broker-server.d.ts +50 -0
  64. package/dist/hosted-broker-server.d.ts.map +1 -0
  65. package/dist/hosted-git-http-server.d.ts +36 -0
  66. package/dist/hosted-git-http-server.d.ts.map +1 -0
  67. package/dist/hosted-harness-claude.d.ts +195 -0
  68. package/dist/hosted-harness-claude.d.ts.map +1 -0
  69. package/dist/hosted-harness-opencode.d.ts +227 -0
  70. package/dist/hosted-harness-opencode.d.ts.map +1 -0
  71. package/dist/hosted-harness.d.ts +118 -0
  72. package/dist/hosted-harness.d.ts.map +1 -0
  73. package/dist/hosted-harvest.d.ts +68 -0
  74. package/dist/hosted-harvest.d.ts.map +1 -0
  75. package/dist/hosted-headless.d.ts +72 -0
  76. package/dist/hosted-headless.d.ts.map +1 -0
  77. package/dist/hosted-loop-lifecycle.d.ts +206 -0
  78. package/dist/hosted-loop-lifecycle.d.ts.map +1 -0
  79. package/dist/hosted-loop.d.ts +170 -0
  80. package/dist/hosted-loop.d.ts.map +1 -0
  81. package/dist/hosted-run-event-sink.d.ts +222 -0
  82. package/dist/hosted-run-event-sink.d.ts.map +1 -0
  83. package/dist/hosted-runtime-assets.d.ts +186 -0
  84. package/dist/hosted-runtime-assets.d.ts.map +1 -0
  85. package/dist/hosted-session-cli.d.ts +258 -0
  86. package/dist/hosted-session-cli.d.ts.map +1 -0
  87. package/dist/hosted-skill-staging.d.ts +73 -0
  88. package/dist/hosted-skill-staging.d.ts.map +1 -0
  89. package/dist/http.d.ts +8 -0
  90. package/dist/http.d.ts.map +1 -0
  91. package/dist/index.d.ts +22 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +88 -92
  94. package/dist/instruction-set-gepa-launch.d.ts +3 -0
  95. package/dist/instruction-set-gepa-launch.d.ts.map +1 -0
  96. package/dist/instruction-set-gepa-launch.js +15 -0
  97. package/dist/instruction-set-loader.d.ts +6 -0
  98. package/dist/instruction-set-loader.d.ts.map +1 -0
  99. package/dist/instruction-set-loader.js +59 -0
  100. package/dist/instruction-set-manifest.d.ts +14 -0
  101. package/dist/instruction-set-manifest.d.ts.map +1 -0
  102. package/dist/instruction-set-manifest.js +162 -0
  103. package/dist/instruction-sets-cli.d.ts +40 -0
  104. package/dist/instruction-sets-cli.d.ts.map +1 -0
  105. package/dist/instruction-sets-cli.js +361 -0
  106. package/dist/json-response.d.ts +4 -0
  107. package/dist/json-response.d.ts.map +1 -0
  108. package/dist/jsonl-stream.d.ts +8 -0
  109. package/dist/jsonl-stream.d.ts.map +1 -0
  110. package/dist/kill-switch-cli.d.ts +30 -0
  111. package/dist/kill-switch-cli.d.ts.map +1 -0
  112. package/dist/loopback-serve.d.ts +25 -0
  113. package/dist/loopback-serve.d.ts.map +1 -0
  114. package/dist/manifests-cli.d.ts +22 -0
  115. package/dist/manifests-cli.d.ts.map +1 -0
  116. package/dist/markdown-report-input.d.ts +7 -0
  117. package/dist/markdown-report-input.d.ts.map +1 -0
  118. package/dist/merge-job.d.ts +155 -0
  119. package/dist/merge-job.d.ts.map +1 -0
  120. package/dist/model-configs-cli.d.ts +19 -0
  121. package/dist/model-configs-cli.d.ts.map +1 -0
  122. package/dist/model-configs-cli.js +104 -0
  123. package/dist/optimization-export-cli.d.ts +19 -0
  124. package/dist/optimization-export-cli.d.ts.map +1 -0
  125. package/dist/optimizations-list-cli.d.ts +20 -0
  126. package/dist/optimizations-list-cli.d.ts.map +1 -0
  127. package/dist/optimizations-promote-cli.d.ts +8 -0
  128. package/dist/optimizations-promote-cli.d.ts.map +1 -0
  129. package/dist/optimizations-promote-cli.js +34 -0
  130. package/dist/option-validation.d.ts +15 -0
  131. package/dist/option-validation.d.ts.map +1 -0
  132. package/dist/preview-runtime.d.ts +63 -0
  133. package/dist/preview-runtime.d.ts.map +1 -0
  134. package/dist/prompt-draft-push.d.ts +60 -0
  135. package/dist/prompt-draft-push.d.ts.map +1 -0
  136. package/dist/prompt-label-cli.d.ts +11 -0
  137. package/dist/prompt-label-cli.d.ts.map +1 -0
  138. package/dist/prompt-label-cli.js +29 -0
  139. package/dist/prompt-length-wire.d.ts +28 -0
  140. package/dist/prompt-length-wire.d.ts.map +1 -0
  141. package/dist/prompt-report-cli.d.ts +10 -0
  142. package/dist/prompt-report-cli.d.ts.map +1 -0
  143. package/dist/prompt-summary-table.d.ts +15 -0
  144. package/dist/prompt-summary-table.d.ts.map +1 -0
  145. package/dist/prompt-summary-table.js +6 -0
  146. package/dist/registration-tag-warning.d.ts +32 -0
  147. package/dist/registration-tag-warning.d.ts.map +1 -0
  148. package/dist/report-comments-cli.d.ts +16 -0
  149. package/dist/report-comments-cli.d.ts.map +1 -0
  150. package/dist/report-markdown-blankness.d.ts +2 -0
  151. package/dist/report-markdown-blankness.d.ts.map +1 -0
  152. package/dist/report-replacement-warning.d.ts +17 -0
  153. package/dist/report-replacement-warning.d.ts.map +1 -0
  154. package/dist/repository-backup-core.d.ts +225 -0
  155. package/dist/repository-backup-core.d.ts.map +1 -0
  156. package/dist/repository-recovery-manifest.d.ts +83 -0
  157. package/dist/repository-recovery-manifest.d.ts.map +1 -0
  158. package/dist/run-event-digest.d.ts +74 -0
  159. package/dist/run-event-digest.d.ts.map +1 -0
  160. package/dist/runner-dir-verify.d.ts +66 -0
  161. package/dist/runner-dir-verify.d.ts.map +1 -0
  162. package/dist/runner-instruction-set-contract.d.ts +24 -0
  163. package/dist/runner-instruction-set-contract.d.ts.map +1 -0
  164. package/dist/runner-instruction-set-contract.js +50 -0
  165. package/dist/sandbox-hygiene.d.ts +45 -0
  166. package/dist/sandbox-hygiene.d.ts.map +1 -0
  167. package/dist/sandbox-provider.d.ts +193 -0
  168. package/dist/sandbox-provider.d.ts.map +1 -0
  169. package/dist/scorer-draft-push.d.ts +50 -0
  170. package/dist/scorer-draft-push.d.ts.map +1 -0
  171. package/dist/scorers-cli.d.ts +20 -0
  172. package/dist/scorers-cli.d.ts.map +1 -0
  173. package/dist/scorers-cli.js +22 -0
  174. package/dist/scorers-verify-parity-bridge.d.ts +16 -0
  175. package/dist/scorers-verify-parity-bridge.d.ts.map +1 -0
  176. package/dist/scorers-verify-parity-bridge.js +90 -0
  177. package/dist/scorers-verify-parity.d.ts +81 -0
  178. package/dist/scorers-verify-parity.d.ts.map +1 -0
  179. package/dist/scorers-verify-parity.js +502 -0
  180. package/dist/scores-accept-cli.d.ts +21 -0
  181. package/dist/scores-accept-cli.d.ts.map +1 -0
  182. package/dist/secret-redaction.d.ts +47 -0
  183. package/dist/secret-redaction.d.ts.map +1 -0
  184. package/dist/session-timeline.d.ts +54 -0
  185. package/dist/session-timeline.d.ts.map +1 -0
  186. package/dist/setup-prompt.d.ts +5 -0
  187. package/dist/setup-prompt.d.ts.map +1 -0
  188. package/dist/skill-installer.d.ts +68 -0
  189. package/dist/skill-installer.d.ts.map +1 -0
  190. package/dist/skilled-proposer-launch.d.ts +7 -0
  191. package/dist/skilled-proposer-launch.d.ts.map +1 -0
  192. package/dist/skilled-proposer-launch.js +60 -0
  193. package/dist/smoke.d.ts +102 -0
  194. package/dist/smoke.d.ts.map +1 -0
  195. package/dist/start-intent.d.ts +12 -0
  196. package/dist/start-intent.d.ts.map +1 -0
  197. package/dist/task-assignment-manifest.d.ts +15 -0
  198. package/dist/task-assignment-manifest.d.ts.map +1 -0
  199. package/dist/task-create-error.d.ts +18 -0
  200. package/dist/task-create-error.d.ts.map +1 -0
  201. package/dist/team-connectors-cli.d.ts +29 -0
  202. package/dist/team-connectors-cli.d.ts.map +1 -0
  203. package/dist/types.d.ts +39 -0
  204. package/dist/types.d.ts.map +1 -0
  205. package/dist/vercel-sandbox-provider.d.ts +156 -0
  206. package/dist/vercel-sandbox-provider.d.ts.map +1 -0
  207. package/dist/workbench-cli.d.ts +233 -0
  208. package/dist/workbench-cli.d.ts.map +1 -0
  209. package/dist/workspace-sync.d.ts +207 -0
  210. package/dist/workspace-sync.d.ts.map +1 -0
  211. package/dist/workspace.d.ts +92 -0
  212. package/dist/workspace.d.ts.map +1 -0
  213. package/dist/zip-draft-push.d.ts +55 -0
  214. package/dist/zip-draft-push.d.ts.map +1 -0
  215. package/package.json +14 -1
  216. package/requirements/README.md +138 -0
  217. package/requirements/skilled-proposer.in +66 -0
  218. package/requirements/skilled-proposer.lock +265 -0
  219. package/scripts/ensure-skilled-proposer-venv.mjs +5 -0
  220. package/scripts/refresh-skilled-proposer-lock.mjs +256 -0
  221. package/src/diff-comments-cli.ts +4 -0
  222. package/src/gepa-engine-dispatch.ts +42 -2
  223. package/src/gepa-python-paths.ts +6 -3
  224. package/src/help.ts +93 -23
  225. package/src/index.ts +99 -126
  226. package/src/instruction-set-gepa-launch.ts +28 -0
  227. package/src/instruction-set-loader.ts +37 -0
  228. package/src/instruction-set-manifest.ts +120 -0
  229. package/src/instruction-sets-cli.ts +306 -0
  230. package/src/model-configs-cli.ts +119 -0
  231. package/src/optimizations-promote-cli.ts +41 -0
  232. package/src/prompt-label-cli.ts +55 -0
  233. package/src/prompt-summary-table.ts +12 -0
  234. package/src/runner-instruction-set-contract.ts +64 -0
  235. package/src/scorers-cli.ts +40 -0
  236. package/src/scorers-verify-parity-bridge.ts +90 -0
  237. package/src/scorers-verify-parity.ts +564 -0
  238. package/src/skilled-proposer-launch.ts +89 -0
  239. package/src/skilled-proposer-venv-manager.mjs +757 -0
  240. package/tsconfig.json +2 -0
  241. package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +2 -1
  242. package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +8 -2
  243. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +17 -3
  244. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +100 -8
  245. package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +52 -0
  246. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +92 -12
  247. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +100 -22
  248. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +381 -0
  249. package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +17 -0
  250. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +2 -1
  251. package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +52 -7
  252. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +4 -1
  253. package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +10 -5
  254. package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +323 -68
  255. package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +207 -13
  256. package/vendor/skills/orizu-cli/SKILL.md +2 -0
  257. package/vendor/skills/orizu-cli/references/cli-reference.md +85 -1
  258. package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +303 -0
  259. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +25 -0
  260. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +108 -8
@@ -0,0 +1,306 @@
1
+ import { authedFetch } from './http.js'
2
+ import { loadInstructionSetManifest, MAX_INSTRUCTION_SET_COMPONENT_BYTES, type InstructionSetManifest } from './instruction-set-manifest.js'
3
+ import { sanitizeHumanInlineText, sanitizeTerminalText } from './json-response.js'
4
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
5
+ import { join } from 'node:path'
6
+ import { createHash, randomUUID } from 'node:crypto'
7
+
8
+ export interface InstructionSetsCommandIo {
9
+ json: boolean
10
+ print: (line: string) => void
11
+ resolveProjectSlug: (provided: string | null) => Promise<string>
12
+ }
13
+
14
+ function positionalArgs(args: string[]): string[] {
15
+ const values: string[] = []
16
+ for (let index = 0; index < args.length; index += 1) {
17
+ const value = args[index]!
18
+ if (value.startsWith('--')) {
19
+ if (argValue(args, value) !== null) index += 1
20
+ continue
21
+ }
22
+ values.push(value)
23
+ }
24
+ return values
25
+ }
26
+
27
+ function argValue(args: string[], flag: string): string | null {
28
+ const index = args.indexOf(flag)
29
+ return index === -1 || !args[index + 1] || args[index + 1]!.startsWith('--')
30
+ ? null
31
+ : args[index + 1]!
32
+ }
33
+
34
+ async function responsePayload(response: Response, action: string): Promise<Record<string, unknown>> {
35
+ const payload = await response.json().catch(() => ({})) as Record<string, unknown>
36
+ if (!response.ok) {
37
+ throw new Error(`${action} failed (${response.status}): ${typeof payload.error === 'string' ? payload.error : response.statusText}`)
38
+ }
39
+ return payload
40
+ }
41
+
42
+ const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/u
43
+ export interface SyncComponent { body?: string; repoPath?: string; contentSha?: string; commitSha?: string }
44
+ export interface SyncMaterial { profileVersionId: string; versionNumber: number; modelConfigIdentity: string; resolvedFrom: string; components: Record<string, SyncComponent> }
45
+ export interface SyncProfile { modelConfigIdentity: string; resolvedFrom: string; production: SyncMaterial | null }
46
+ export interface SyncSet { name: string; slug?: string; description?: string | null; shape: string[]; default: SyncMaterial; profiles: SyncProfile[]; filteredTo?: string[] }
47
+ function safeSegment(value: string) { if (!SAFE_SEGMENT.test(value) || value === '.' || value === '..' || value.startsWith('.')) throw new Error('instruction_set_path_unsafe') }
48
+ function slug(identity: string) { return identity.replaceAll('/', '__').replace(/[^A-Za-z0-9._-]/gu, '_') }
49
+ function stable(value: unknown): string { return `${JSON.stringify(value, null, 2)}\n` }
50
+ function sha256(value: string): string { return createHash('sha256').update(value, 'utf8').digest('hex') }
51
+
52
+ export interface SyncFileOps { writeFileSync: typeof writeFileSync; renameSync?: typeof renameSync }
53
+
54
+ function writeMaterial(root: string, material: SyncMaterial, shape: string[], manifestRoot: string, fileOps: SyncFileOps = { writeFileSync }) {
55
+ const files: Record<string, string> = {}
56
+ const syncedContentSha256: Record<string, string> = {}
57
+ const pinnedComponents: Record<string, unknown> = {}
58
+ for (const key of [...shape].sort()) {
59
+ safeSegment(key)
60
+ const component = material.components[key]
61
+ if (typeof component?.body === 'string') {
62
+ const relative = `${manifestRoot}/${key}.md`
63
+ files[key] = relative
64
+ const target = join(root, ...relative.split('/'))
65
+ mkdirSync(join(target, '..'), { recursive: true })
66
+ fileOps.writeFileSync(target, component.body)
67
+ syncedContentSha256[key] = sha256(component.body)
68
+ } else if (component?.repoPath && component?.contentSha && component?.commitSha) pinnedComponents[key] = { repoPath: component.repoPath, contentSha: component.contentSha, commitSha: component.commitSha }
69
+ else throw new Error('instruction_set_unresolvable')
70
+ }
71
+ return { profileVersionId: material.profileVersionId, versionNumber: material.versionNumber, modelConfigIdentity: material.modelConfigIdentity, resolvedFrom: material.resolvedFrom, files, syncedContentSha256, ...(Object.keys(pinnedComponents).length ? { pinnedComponents } : {}) }
72
+ }
73
+
74
+ // Only the scratch paths THIS run created are ever removed; a user's own
75
+ // `.<set>.*.bak` in --out is never touched (PR #1668 review).
76
+ function removeOwnScratch(paths: Array<string | null>) {
77
+ for (const path of paths) if (path && existsSync(path)) rmSync(path, { recursive: true, force: true })
78
+ }
79
+
80
+ function existingDestinationIsSynced(destination: string, name: string) {
81
+ try {
82
+ if (!statSync(destination).isDirectory()) return false
83
+ const manifest = JSON.parse(readFileSync(join(destination, 'manifest.json'), 'utf8')) as { manifestVersion?: number; name?: string }
84
+ return manifest.manifestVersion === 1 && manifest.name === name
85
+ } catch { return false }
86
+ }
87
+
88
+ export function syncToDisk(out: string, set: SyncSet, fileOps: SyncFileOps = { writeFileSync }) {
89
+ safeSegment(set.name)
90
+ for (const key of set.shape) safeSegment(key)
91
+ const profileSlugs = new Set<string>()
92
+ for (const profile of set.profiles) {
93
+ const value = slug(profile.modelConfigIdentity)
94
+ safeSegment(value)
95
+ if (profileSlugs.has(value)) throw new Error('instruction_set_slug_collision')
96
+ profileSlugs.add(value)
97
+ }
98
+ mkdirSync(out, { recursive: true })
99
+ const destination = join(out, set.name)
100
+ if (existsSync(destination) && !existingDestinationIsSynced(destination, set.name)) throw new Error('instruction_set_sync_destination_not_synced')
101
+ const temp = join(out, `.${set.name}.${randomUUID()}.tmp`)
102
+ const move = fileOps.renameSync || renameSync
103
+ let backup: string | null = null
104
+ mkdirSync(temp)
105
+ try {
106
+ const defaultMaterial = writeMaterial(temp, set.default, set.shape, 'default', fileOps)
107
+ const profiles = set.profiles.map(profile => ({
108
+ modelConfigIdentity: profile.modelConfigIdentity, slug: slug(profile.modelConfigIdentity), resolvedFrom: profile.resolvedFrom,
109
+ production: profile.production ? writeMaterial(temp, profile.production, set.shape, `profiles/${slug(profile.modelConfigIdentity)}`, fileOps) : null,
110
+ })).sort((a, b) => a.modelConfigIdentity.localeCompare(b.modelConfigIdentity))
111
+ const components = [
112
+ ...Object.entries(defaultMaterial.files).map(([key, path]) => ({ key, path, syncedContentSha256: defaultMaterial.syncedContentSha256[key] })),
113
+ ...profiles.flatMap(profile => profile.production
114
+ ? Object.entries(profile.production.files).map(([key, path]) => ({ key, modelConfig: profile.modelConfigIdentity, path, syncedContentSha256: profile.production!.syncedContentSha256[key] }))
115
+ : []),
116
+ ]
117
+ const manifest = { manifestVersion: 1, name: set.name, ...(Object.hasOwn(set, 'description') ? { description: set.description ?? null } : {}), shape: set.shape, components, default: defaultMaterial, profiles, ...(set.filteredTo ? { filteredTo: [...set.filteredTo].sort() } : {}) }
118
+ fileOps.writeFileSync(join(temp, 'manifest.json'), stable(manifest))
119
+ backup = join(out, `.${set.name}.${randomUUID()}.bak`)
120
+ if (existsSync(destination)) move(destination, backup)
121
+ try { move(temp, destination) } catch (error) { if (existsSync(backup)) move(backup, destination); throw error }
122
+ removeOwnScratch([backup])
123
+ } catch (error) {
124
+ removeOwnScratch([temp])
125
+ if (backup && existsSync(backup) && !existsSync(destination)) move(backup, destination)
126
+ removeOwnScratch([backup])
127
+ throw error
128
+ }
129
+ }
130
+
131
+ export async function instructionSetsCommand(args: string[], io: InstructionSetsCommandIo): Promise<number> {
132
+ const positionals = positionalArgs(args)
133
+ const [subcommand, reference] = positionals
134
+ if (subcommand === 'default' && reference === 'move') {
135
+ const version = argValue(args, '--version')
136
+ if (!version || !/^[0-9]+$/u.test(version) || Number(version) < 1) throw new Error('--version must be a positive integer')
137
+ }
138
+ let writeManifest: InstructionSetManifest | undefined
139
+ if (subcommand === 'create' || subcommand === 'push') {
140
+ if (!reference || reference.startsWith('--')) throw new Error(subcommand === 'create'
141
+ ? 'Usage: orizu instructions create <manifest> --project <team/project> [--runner-version <id>] [--model-config <identity>] [--json]'
142
+ : 'Usage: orizu instructions push <manifest> --project <team/project> [--set <slug-or-exact-name>] [--runner-version <id>] [--json]')
143
+ writeManifest = loadInstructionSetManifest(reference)
144
+ }
145
+ const project = await io.resolveProjectSlug(argValue(args, '--project'))
146
+ if (subcommand === 'default') {
147
+ const operation = reference; const set = positionals[2]; const identity = argValue(args, '--model-config'); const version = argValue(args, '--version')
148
+ if ((operation !== 'show' && operation !== 'move') || !set) throw new Error('Usage: orizu instructions default show|move <set> --project <team/project> [--model-config <identity> --version <n>] [--json]')
149
+ if (operation === 'move' && (!identity || !version || !/^[0-9]+$/u.test(version) || Number(version) < 1)) throw new Error('--version must be a positive integer')
150
+ const path = `/api/cli/instruction-sets/${encodeURIComponent(set)}/default?project=${encodeURIComponent(project)}`
151
+ if (operation === 'show') {
152
+ const payload = await responsePayload(await authedFetch(path, { method: 'GET' }), 'Instruction sets default show')
153
+ if (io.json) io.print(JSON.stringify(payload)); else io.print(`Default: v${(payload.default as { versionNumber?: number } | undefined)?.versionNumber ?? '?'}`)
154
+ return 0
155
+ }
156
+ if (!io.json) {
157
+ const impact = await responsePayload(await authedFetch(path, { method: 'GET' }), 'Instruction sets default show')
158
+ const resolvesToDefault = Array.isArray(impact.resolvesToDefault) ? impact.resolvesToDefault.filter((item): item is string => typeof item === 'string') : []
159
+ io.print(`These model configs resolve to the default: ${resolvesToDefault.join(', ')}`)
160
+ }
161
+ const payload = await responsePayload(await authedFetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ modelConfigIdentity: identity, versionNumber: Number(version) }) }), 'Instruction sets default move')
162
+ if (io.json) io.print(JSON.stringify(payload)); else io.print(`Default moved to ${identity} v${version}`)
163
+ return 0
164
+ }
165
+ if (subcommand === 'shape') {
166
+ const operation = reference; const set = positionals[2]; const key = argValue(args, '--key'); const from = argValue(args, '--from')
167
+ if ((operation !== 'add' && operation !== 'remove') || !set || !key || (operation === 'add' && !from)) throw new Error('Usage: orizu instructions shape add|remove <set> --project <team/project> --key <key> [--from <manifest>] [--json]')
168
+ let component: { body: string } | undefined
169
+ if (operation === 'add') {
170
+ let manifest: unknown
171
+ try { manifest = JSON.parse(readFileSync(from!, 'utf8')) } catch { throw new Error('instruction_set_manifest_invalid_json') }
172
+ const source = manifest && typeof manifest === 'object' && Array.isArray((manifest as { components?: unknown }).components)
173
+ ? (manifest as { components: unknown[] }).components.find((item): item is { key?: unknown; text?: unknown } => Boolean(item && typeof item === 'object' && (item as { key?: unknown }).key === key))
174
+ : undefined
175
+ if (!source || typeof source.text !== 'string' || Buffer.byteLength(source.text, 'utf8') > MAX_INSTRUCTION_SET_COMPONENT_BYTES) throw new Error(`Instruction set manifest has no component for key: ${key}`)
176
+ component = { body: source.text }
177
+ }
178
+ const payload = await responsePayload(await authedFetch(`/api/cli/instruction-sets/${encodeURIComponent(set)}/shape?project=${encodeURIComponent(project)}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ operation, key, ...(component ? { component } : {}) }) }), `Instruction sets shape ${operation}`)
179
+ if (io.json) io.print(JSON.stringify(payload)); else {
180
+ io.print(`Shape ${operation === 'add' ? 'added' : 'removed'}: ${key}`)
181
+ const affected = Array.isArray(payload.affected) ? payload.affected : []
182
+ const identities = [...new Set(affected.flatMap(pointer => pointer && typeof pointer === 'object' && typeof (pointer as { modelConfigIdentity?: unknown }).modelConfigIdentity === 'string' ? [(pointer as { modelConfigIdentity: string }).modelConfigIdentity] : []))]
183
+ if (identities.length) io.print(`${set} does not resolve for these model configs until the pointers move: ${identities.join(', ')}`)
184
+ for (const pointer of affected) if (pointer && typeof pointer === 'object') {
185
+ const item = pointer as { modelConfigIdentity?: unknown; pointer?: unknown; stalePointerVersionNumber?: unknown; headVersionNumber?: unknown; branchedFromVersionNumber?: unknown }
186
+ if (typeof item.modelConfigIdentity === 'string' && (item.pointer === 'production' || item.pointer === 'default') && typeof item.headVersionNumber === 'number') {
187
+ const command = item.pointer === 'production' ? 'profiles promote' : 'default move'
188
+ const operatorAction = item.pointer === 'production' ? 'promote' : 'default move'
189
+ if (typeof item.stalePointerVersionNumber === 'number' && typeof item.branchedFromVersionNumber === 'number' && item.branchedFromVersionNumber !== item.stalePointerVersionNumber) io.print(`${operatorAction} would move ${item.pointer} from v${item.stalePointerVersionNumber}'s text to v${item.headVersionNumber} = v${item.branchedFromVersionNumber}'s text + ${key}`)
190
+ io.print(`Follow up: instructions ${command} ${set} --project ${project} --model-config ${item.modelConfigIdentity} --version ${item.headVersionNumber}`)
191
+ }
192
+ }
193
+ }
194
+ return 0
195
+ }
196
+ if (subcommand === 'profiles') {
197
+ const operation = reference; const set = positionals[2]; const identity = argValue(args, '--model-config')
198
+ if ((operation !== 'new' && operation !== 'promote' && operation !== 'rollback') || !set || !identity) throw new Error('Usage: orizu instructions profiles new|promote|rollback <set> --project <team/project> --model-config <identity> [--version <n>|--to <n>] [--json]')
199
+ const encodedSet = encodeURIComponent(set); const encodedIdentity = encodeURIComponent(identity)
200
+ const version = argValue(args, '--version'); const to = argValue(args, '--to')
201
+ if ((operation === 'promote' && (!version || !/^[0-9]+$/u.test(version) || Number(version) < 1)) || (operation === 'rollback' && (!to || !/^[0-9]+$/u.test(to) || Number(to) < 1))) throw new Error(operation === 'promote' ? '--version must be a positive integer' : '--to must be a positive integer')
202
+ const path = operation === 'new' ? `/api/cli/instruction-sets/${encodedSet}/profiles` : operation === 'promote' ? `/api/cli/instruction-sets/${encodedSet}/profiles/${encodedIdentity}/label` : `/api/cli/instruction-sets/${encodedSet}/profiles/${encodedIdentity}/rollback`
203
+ const body = operation === 'new' ? { modelConfigIdentity: identity } : operation === 'promote' ? { versionNumber: Number(version) } : { to: Number(to) }
204
+ const payload = await responsePayload(await authedFetch(`${path}?project=${encodeURIComponent(project)}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }), `Instruction sets profiles ${operation}`)
205
+ if (io.json) io.print(JSON.stringify(payload))
206
+ else {
207
+ const profile = payload.profile as { modelConfigIdentity?: string; versionNumber?: number } | undefined
208
+ io.print(`${profile?.modelConfigIdentity || identity}: v${profile?.versionNumber ?? '?'} ${operation === 'new' ? 'created' : operation === 'promote' ? 'promoted' : 'rolled back'}`)
209
+ const mirrored = payload.mirroredPromptLabel as { promptName?: unknown; promptId?: unknown } | undefined
210
+ if (mirrored && (typeof mirrored.promptName === 'string' || typeof mirrored.promptId === 'string')) io.print(`also moved production for prompt ${typeof mirrored.promptName === 'string' ? mirrored.promptName : mirrored.promptId} (default profile → prompt label)`)
211
+ }
212
+ return 0
213
+ }
214
+ if (subcommand === 'create' || subcommand === 'push') {
215
+ const manifest = writeManifest!; const modelConfig = argValue(args, '--model-config'); const runnerVersion = argValue(args, '--runner-version')
216
+ const setReference = argValue(args, '--set') || manifest.name
217
+ const path = subcommand === 'create' ? '/api/cli/instruction-sets' : `/api/cli/instruction-sets/${encodeURIComponent(setReference)}/versions`
218
+ const body = subcommand === 'create'
219
+ ? { ...manifest, ...(modelConfig ? { modelConfigIdentity: modelConfig } : {}), ...(runnerVersion ? { runnerVersionId: runnerVersion } : {}) }
220
+ : { shape: manifest.shape, components: manifest.components, ...(Object.hasOwn(manifest, 'description') ? { description: manifest.description } : {}), ...(runnerVersion ? { runnerVersionId: runnerVersion } : {}) }
221
+ const payload = await responsePayload(await authedFetch(`${path}?project=${encodeURIComponent(project)}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }), `Instruction sets ${subcommand}`)
222
+ if (io.json) io.print(JSON.stringify(payload))
223
+ else {
224
+ const set = payload.instructionSet as { name?: string; profiles?: unknown[] } | undefined
225
+ const pushed = Array.isArray(payload.pushed) ? payload.pushed : []
226
+ const unchanged = Array.isArray(payload.unchanged) ? payload.unchanged : []
227
+ const skipped = Array.isArray(payload.skipped) ? payload.skipped : []
228
+ if (subcommand === 'create') {
229
+ const profiles = Array.isArray(set?.profiles) ? set.profiles : []
230
+ io.print(`${set?.name || manifest.name}: ${profiles.length} profile(s) created`)
231
+ } else {
232
+ const describe = (value: unknown) => {
233
+ if (typeof value === 'string') return value
234
+ if (value && typeof value === 'object') {
235
+ const row = value as { modelConfigIdentity?: unknown; versionNumber?: unknown }
236
+ return `${typeof row.modelConfigIdentity === 'string' ? row.modelConfigIdentity : 'unknown'}${typeof row.versionNumber === 'number' ? ` v${row.versionNumber}` : ''}`
237
+ }
238
+ return 'unknown'
239
+ }
240
+ io.print(`${set?.name || manifest.name}: pushed ${pushed.map(describe).join(', ') || 'none'}; unchanged ${unchanged.map(describe).join(', ') || 'none'}; skipped ${skipped.map(describe).join(', ') || 'none'}`)
241
+ }
242
+ }
243
+ return 0
244
+ }
245
+ if (subcommand === 'archive' || subcommand === 'restore') {
246
+ if (!reference || reference.startsWith('--')) throw new Error(`Usage: orizu instructions ${subcommand} <slug-or-exact-name> --project <team/project> [--json]`)
247
+ const archived = subcommand === 'archive'
248
+ const payload = await responsePayload(await authedFetch(`/api/cli/instruction-sets/${encodeURIComponent(reference)}?project=${encodeURIComponent(project)}`, {
249
+ method: 'PATCH',
250
+ headers: { 'content-type': 'application/json' },
251
+ body: JSON.stringify({ archived }),
252
+ }), `Instructions ${subcommand}`)
253
+ if (io.json) io.print(JSON.stringify(payload))
254
+ else {
255
+ const set = payload.instructionSet as { name?: string; slug?: string } | undefined
256
+ const identity = set?.slug && set.slug !== set.name ? `${set.name || reference} (${set.slug})` : set?.name || set?.slug || reference
257
+ io.print(`${archived ? 'Archived' : 'Restored'} instruction set ${identity}`)
258
+ }
259
+ return 0
260
+ }
261
+ const status = argValue(args, '--status') || 'active'
262
+ if (subcommand !== 'list' && subcommand !== 'show' && subcommand !== 'sync') throw new Error('Usage: orizu instructions list|show|sync|archive|restore')
263
+ if (subcommand === 'sync') {
264
+ const output = argValue(args, '--out')
265
+ if (!reference || !output) throw new Error('Usage: orizu instructions sync <set> --out <dir> --project <team/project> [--model-config <identity>] [--json]')
266
+ const modelConfig = argValue(args, '--model-config')
267
+ const path = `/api/cli/instruction-sets/${encodeURIComponent(reference)}/sync?project=${encodeURIComponent(project)}${modelConfig ? `&modelConfig=${encodeURIComponent(modelConfig)}` : ''}`
268
+ const payload = await responsePayload(await authedFetch(path, { method: 'GET' }), 'Instruction sets sync')
269
+ const set = payload.instructionSet as SyncSet
270
+ if (!set || (set.slug !== reference && set.name !== reference) || (modelConfig && (!Array.isArray(set.filteredTo) || set.filteredTo.length !== 1 || set.filteredTo[0] !== modelConfig))) {
271
+ throw new Error('instruction_set_sync_response_mismatch')
272
+ }
273
+ syncToDisk(output, set)
274
+ if (io.json) io.print(JSON.stringify(payload)); else io.print(`Synced ${set.name} to ${join(output, set.name)}`)
275
+ return 0
276
+ }
277
+ if (subcommand === 'show' && (!reference || reference.startsWith('--'))) throw new Error('Instruction set name is required')
278
+ if (status !== 'active' && status !== 'archived' && status !== 'all') throw new Error('--status must be active, archived, or all')
279
+
280
+ const query = `project=${encodeURIComponent(project)}&status=${encodeURIComponent(status)}`
281
+ const path = subcommand === 'list'
282
+ ? `/api/cli/instruction-sets?${query}`
283
+ : `/api/cli/instruction-sets/${encodeURIComponent(reference!)}?${query}`
284
+ const payload = await responsePayload(await authedFetch(path, { method: 'GET' }), `Instruction sets ${subcommand}`)
285
+ if (io.json) {
286
+ io.print(JSON.stringify(payload))
287
+ return 0
288
+ }
289
+ if (subcommand === 'list') {
290
+ const sets = Array.isArray(payload.instructionSets) ? payload.instructionSets : []
291
+ for (const item of sets) {
292
+ const set = item as { name?: string; slug?: string; description?: string | null; shape?: string[]; status?: string }
293
+ io.print(`${set.name || 'unnamed'}${set.slug ? ` [${set.slug}]` : ''}${set.status === 'archived' ? ' [archived]' : ''}${set.shape ? ` (${set.shape.join(', ')})` : ''}`)
294
+ if (set.description) io.print(` ${sanitizeHumanInlineText(sanitizeTerminalText, set.description)}`)
295
+ }
296
+ return 0
297
+ }
298
+ const set = payload.instructionSet as { name?: string; description?: string | null; shape?: string[]; status?: string; default?: { versionNumber?: number } | null; profiles?: Array<{ modelConfigIdentity?: string | null; production?: { versionNumber?: number } | null; latestVersionNumber?: number | null }> } | undefined
299
+ io.print(`${set?.name || reference}${set?.status === 'archived' ? ' [archived]' : ''}: ${set?.shape?.join(', ') || ''}`)
300
+ if (set?.description) io.print(`Description: ${sanitizeHumanInlineText(sanitizeTerminalText, set.description)}`)
301
+ if (set?.default) io.print(`Default: v${set.default.versionNumber ?? '?'}`)
302
+ for (const profile of set?.profiles || []) {
303
+ io.print(`${profile.modelConfigIdentity || 'unspecified'}: production ${profile.production ? `v${profile.production.versionNumber}` : '—'}, latest ${profile.latestVersionNumber ?? '—'}`)
304
+ }
305
+ return 0
306
+ }
@@ -0,0 +1,119 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ import { authedFetch } from './http.js'
4
+
5
+ export type CliFetcher = (path: string, init?: RequestInit) => Promise<Response>
6
+
7
+ export interface ModelConfigSettingsVersion {
8
+ versionNumber: number
9
+ settings: Record<string, unknown>
10
+ }
11
+
12
+ export interface ModelConfig {
13
+ identity: string
14
+ displayName: string
15
+ currentSettingsVersion: ModelConfigSettingsVersion
16
+ createdAt: string
17
+ }
18
+
19
+ export interface ModelConfigsCommandIo {
20
+ json: boolean
21
+ print: (line: string) => void
22
+ fetcher?: CliFetcher
23
+ resolveProjectSlug?: (arg: string | null) => Promise<string>
24
+ }
25
+
26
+ function argValue(args: string[], flag: string): string | null {
27
+ const index = args.lastIndexOf(flag)
28
+ return index === -1 || !args[index + 1] || args[index + 1]!.startsWith('--') ? null : args[index + 1]!
29
+ }
30
+
31
+ function positionalArgs(args: string[]): string[] {
32
+ const values: string[] = []
33
+ for (let index = 0; index < args.length; index += 1) {
34
+ if (args[index]!.startsWith('--')) {
35
+ if (args[index + 1] && !args[index + 1]!.startsWith('--')) index += 1
36
+ } else values.push(args[index]!)
37
+ }
38
+ return values
39
+ }
40
+
41
+ function readSettings(value: string | null, defaultToEmpty = false): Record<string, unknown> {
42
+ if (!value) {
43
+ if (defaultToEmpty) return {}
44
+ throw new Error('--settings is required')
45
+ }
46
+ const source = value.startsWith('@') ? readFileSync(value.slice(1), 'utf8') : value
47
+ const parsed: unknown = JSON.parse(source)
48
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
49
+ throw new Error('--settings must contain a JSON object')
50
+ }
51
+ return parsed as Record<string, unknown>
52
+ }
53
+
54
+ async function responseMessage(response: Response): Promise<string> {
55
+ try {
56
+ const body = await response.clone().json() as Record<string, unknown>
57
+ return typeof body.error === 'string' ? body.error : response.statusText
58
+ } catch {
59
+ return response.statusText
60
+ }
61
+ }
62
+
63
+ async function requestJson(fetcher: CliFetcher, path: string, init?: RequestInit): Promise<unknown> {
64
+ const response = await fetcher(path, init)
65
+ if (!response.ok) throw new Error(`Model config request failed (${response.status}): ${await responseMessage(response)}`)
66
+ return response.json()
67
+ }
68
+
69
+ function renderModelConfig(config: ModelConfig): string {
70
+ return `${config.identity} ${config.displayName} v${config.currentSettingsVersion.versionNumber}`
71
+ }
72
+
73
+ export async function modelConfigsCommand(args: string[], io: ModelConfigsCommandIo): Promise<number> {
74
+ const positional = positionalArgs(args)
75
+ const [command, action, identity] = positional
76
+ const resolveProjectSlug = io.resolveProjectSlug
77
+ if (!resolveProjectSlug) throw new Error('Project resolver unavailable')
78
+ const projectArg = argValue(args, '--project')
79
+ const project = projectArg || await resolveProjectSlug(null)
80
+ const fetcher = io.fetcher ?? authedFetch
81
+ const root = `/api/cli/model-configs?project=${encodeURIComponent(project)}`
82
+ let result: unknown
83
+
84
+ if (command === 'list' && !action) {
85
+ result = await requestJson(fetcher, root, { method: 'GET' })
86
+ if (io.json) io.print(JSON.stringify(result))
87
+ else for (const config of ((result as { modelConfigs?: ModelConfig[] }).modelConfigs || [])) io.print(renderModelConfig(config))
88
+ return 0
89
+ }
90
+ if (command === 'show' && action && !identity) {
91
+ result = await requestJson(fetcher, `/api/cli/model-configs/${encodeURIComponent(action)}?project=${encodeURIComponent(project)}`, { method: 'GET' })
92
+ } else if (command === 'create' && action && !identity) {
93
+ result = await requestJson(fetcher, root, {
94
+ method: 'POST', headers: { 'content-type': 'application/json' },
95
+ body: JSON.stringify({ identity: action, displayName: argValue(args, '--name') || action, settings: readSettings(argValue(args, '--settings'), true) }),
96
+ })
97
+ } else if (command === 'settings' && action === 'set' && identity) {
98
+ result = await requestJson(fetcher, `/api/cli/model-configs/${encodeURIComponent(identity)}/settings?project=${encodeURIComponent(project)}`, {
99
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ settings: readSettings(argValue(args, '--settings')) }),
100
+ })
101
+ } else if (command === 'copy' && action && !identity) {
102
+ const to = argValue(args, '--to')
103
+ if (!to) throw new Error('--to is required')
104
+ result = await requestJson(fetcher, `/api/cli/model-configs/${encodeURIComponent(action)}/copy?project=${encodeURIComponent(project)}`, {
105
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ to }),
106
+ })
107
+ } else {
108
+ io.print('Usage: orizu model-configs <create|list|show|settings set|copy> [--project <team/project>] [--json]')
109
+ return 1
110
+ }
111
+
112
+ if (io.json) {
113
+ io.print(JSON.stringify(result))
114
+ } else {
115
+ const config = (result as { modelConfig: ModelConfig }).modelConfig
116
+ io.print(renderModelConfig(config))
117
+ }
118
+ return 0
119
+ }
@@ -0,0 +1,41 @@
1
+ import { extractErrorMessage } from './error-response.js'
2
+ import { sanitizeTerminalText } from './json-response.js'
3
+
4
+ export interface OptimizationsPromoteIo {
5
+ json: boolean
6
+ print: (line: string) => void
7
+ resolveProjectSlug: (projectArg: string | null) => Promise<string>
8
+ fetcher: (path: string, init?: RequestInit) => Promise<Response>
9
+ }
10
+
11
+ function option(args: readonly string[], name: string): string | null {
12
+ const index = args.indexOf(name)
13
+ return index === -1 || !args[index + 1] || args[index + 1].startsWith('--') ? null : args[index + 1]
14
+ }
15
+
16
+ export async function promoteOptimizationCommand(args: string[], io: OptimizationsPromoteIo): Promise<void> {
17
+ const runId = args.find(arg => !arg.startsWith('--') && arg !== option(args, '--candidate') && arg !== option(args, '--label') && arg !== option(args, '--project'))
18
+ const candidateId = option(args, '--candidate')
19
+ const label = option(args, '--label')
20
+ if (!runId || !candidateId || (label !== null && label !== 'production')) {
21
+ throw new Error('Usage: orizu optimizations promote <run-id> --candidate <id> [--label production] [--project <team/project>] [--json]')
22
+ }
23
+ const project = await io.resolveProjectSlug(option(args, '--project'))
24
+ const response = await io.fetcher(`/api/cli/optimization-runs/${encodeURIComponent(runId)}/promote?project=${encodeURIComponent(project)}`, {
25
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ candidateId, label }),
26
+ })
27
+ if (!response.ok) throw new Error(`Failed to promote optimization candidate: ${await extractErrorMessage(response)}`)
28
+ const data = await response.json() as { promptVersionId?: string; profileVersionId?: string; components?: Array<{ key: string; status: string }> }
29
+ if (io.json) {
30
+ io.print(JSON.stringify(data.profileVersionId
31
+ ? { profileVersionId: data.profileVersionId, components: data.components ?? [] }
32
+ : { promptVersionId: data.promptVersionId }))
33
+ return
34
+ }
35
+ if (data.profileVersionId) {
36
+ io.print(`Promoted profile version ${sanitizeTerminalText(data.profileVersionId)}`)
37
+ for (const component of data.components ?? []) io.print(`${sanitizeTerminalText(component.key)}: ${sanitizeTerminalText(component.status)}`)
38
+ return
39
+ }
40
+ io.print(`Promoted prompt version ${sanitizeTerminalText(data.promptVersionId ?? '')}`)
41
+ }
@@ -0,0 +1,55 @@
1
+ import { authedFetch } from './http.js'
2
+ import { parseJsonResponse, sanitizeTerminalText } from './json-response.js'
3
+
4
+ interface PromptLabelCliContext {
5
+ getArg: (name: string) => string | null
6
+ getPositionalArg: (index: number) => string | null
7
+ json: boolean
8
+ printJson: (value: Record<string, unknown>) => void
9
+ printLine: (value: string) => void
10
+ resolveProjectSlug: (project: string | null) => Promise<string>
11
+ }
12
+
13
+ interface PromptLabelResponse {
14
+ prompt_id: string
15
+ prompt_version_id: string
16
+ label: string
17
+ owner?: {
18
+ instructionSetSlug: string
19
+ componentKey: string
20
+ } | null
21
+ }
22
+
23
+ export async function setPromptLabel(context: PromptLabelCliContext): Promise<void> {
24
+ const promptName = context.getPositionalArg(3)
25
+ const label = context.getPositionalArg(4)
26
+ const project = context.getArg('--project') || await context.resolveProjectSlug(null)
27
+ const promptVersionId = context.getArg('--version')
28
+
29
+ if (!promptName || !label || !promptVersionId) {
30
+ throw new Error('Usage: orizu prompts labels set <prompt-name> <label> --version <prompt-version-id> [--project <team/project>] [--json]')
31
+ }
32
+
33
+ const response = await authedFetch(`/api/cli/prompts/labels?project=${encodeURIComponent(project)}`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ promptName, label, promptVersionId }),
37
+ })
38
+ if (!response.ok) {
39
+ throw new Error(`Failed to set prompt label: ${await response.text()}`)
40
+ }
41
+
42
+ const data = await parseJsonResponse<PromptLabelResponse>(response, 'Prompt label set')
43
+ if (context.json) {
44
+ context.printJson(data as unknown as Record<string, unknown>)
45
+ return
46
+ }
47
+
48
+ context.printLine(`Moved ${sanitizeTerminalText(label)} to ${sanitizeTerminalText(promptVersionId)}`)
49
+ if (data.owner) {
50
+ context.printLine(
51
+ `Owner: ${sanitizeTerminalText(promptName)} (${sanitizeTerminalText(data.prompt_id)}) -> ` +
52
+ `${sanitizeTerminalText(data.owner.instructionSetSlug)} / ${sanitizeTerminalText(data.owner.componentKey)}`
53
+ )
54
+ }
55
+ }
@@ -10,6 +10,10 @@ interface PromptSummaryRow {
10
10
  role: string
11
11
  status?: string
12
12
  lengthStats?: CliLengthStats | null
13
+ owner?: {
14
+ instructionSetSlug: string
15
+ componentKey: string
16
+ } | null
13
17
  }
14
18
 
15
19
  export function printPromptSummaryTable(
@@ -54,4 +58,12 @@ export function printPromptSummaryTable(
54
58
  rows.forEach(row => {
55
59
  printLine(keys.map((key, index) => row[key].padEnd(widths[index])).join(' '))
56
60
  })
61
+ items.forEach(item => {
62
+ if (item.owner) {
63
+ printLine(
64
+ `Owner: ${sanitizeTerminalText(item.name)} (${sanitizeTerminalText(item.id)}) -> ` +
65
+ `${sanitizeTerminalText(item.owner.instructionSetSlug)} / ${sanitizeTerminalText(item.owner.componentKey)}`
66
+ )
67
+ }
68
+ })
57
69
  }
@@ -0,0 +1,64 @@
1
+ import { type SyncComponent, type SyncSet } from './instruction-sets-cli.js'
2
+
3
+ export interface RunnerInstructionSet {
4
+ name: string
5
+ modelConfig: Record<string, unknown>
6
+ shape: string[]
7
+ profileVersionId: string
8
+ modelConfigSettingsVersionId?: string
9
+ versionNumber: number
10
+ promptComponentKey?: string
11
+ components: Record<string, string>
12
+ pinnedComponents?: Record<string, { repoPath: string; contentSha: string; commitSha: string }>
13
+ }
14
+
15
+ export function runnerInstructionSetSyncSet(instructionSet: RunnerInstructionSet): SyncSet {
16
+ const modelConfigIdentity = typeof instructionSet.modelConfig.identity === 'string'
17
+ ? instructionSet.modelConfig.identity
18
+ : ''
19
+ if (!modelConfigIdentity) throw new Error('Runner exec context instruction set has no model config identity')
20
+ const missingKey = instructionSet.shape.find(key => !(key in instructionSet.components) && !(key in (instructionSet.pinnedComponents || {})))
21
+ if (missingKey) throw new Error('instruction_set_tuple_incomplete')
22
+ const components: Record<string, SyncComponent> = Object.fromEntries(Object.entries(instructionSet.components).map(([key, body]) => [key, { body }]))
23
+ for (const [key, pin] of Object.entries(instructionSet.pinnedComponents || {})) components[key] = pin
24
+ const material = {
25
+ profileVersionId: instructionSet.profileVersionId,
26
+ versionNumber: instructionSet.versionNumber,
27
+ modelConfigIdentity,
28
+ // syncToDisk's long-lived manifest requires provenance; this execution
29
+ // path has no resolver decision to expose, only an exact profile version.
30
+ resolvedFrom: 'exact_profile_version',
31
+ components,
32
+ }
33
+ return {
34
+ name: instructionSet.name,
35
+ shape: instructionSet.shape,
36
+ default: material,
37
+ profiles: [{ modelConfigIdentity, resolvedFrom: 'exact_profile_version', production: material }],
38
+ }
39
+ }
40
+
41
+ export function runnerInputPrompt(
42
+ promptContext: { body?: string | null; bodyKind: string; providerSettings: Record<string, unknown> }
43
+ ): Record<string, unknown> {
44
+ const prompt = {
45
+ body_kind: promptContext.bodyKind,
46
+ provider_settings: promptContext.providerSettings,
47
+ } as Record<string, unknown>
48
+ // The route owns this decision. Preserve its response shape exactly so an
49
+ // implicit wrapped prompt remains compatible with existing runners.
50
+ if ('body' in promptContext) prompt.body = promptContext.body
51
+ return prompt
52
+ }
53
+
54
+ export function runnerInputInstructionSet(instructionSet: RunnerInstructionSet): Record<string, unknown> {
55
+ return {
56
+ name: instructionSet.name,
57
+ model_config: instructionSet.modelConfig,
58
+ shape: instructionSet.shape,
59
+ ...(instructionSet.modelConfigSettingsVersionId ? { model_config_settings_version_id: instructionSet.modelConfigSettingsVersionId } : {}),
60
+ ...(instructionSet.promptComponentKey ? { prompt_component_key: instructionSet.promptComponentKey } : {}),
61
+ components: instructionSet.components,
62
+ ...(Object.keys(instructionSet.pinnedComponents || {}).length > 0 ? { pinned_components: instructionSet.pinnedComponents } : {}),
63
+ }
64
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The `orizu scorers` subcommand router (ALI-1554).
3
+ *
4
+ * The five historical branches lived inline in index.ts; folding them here
5
+ * reclaims the headroom the CLI line ratchet (ALI-976) requires before a new
6
+ * subcommand may be added. Behaviour is unchanged: each handler is the same
7
+ * function index.ts already called, and an unrecognised subcommand returns
8
+ * null so main() still falls through to printUsage().
9
+ */
10
+ export interface ScorersCommandIo {
11
+ list: () => Promise<void>
12
+ register: () => Promise<void>
13
+ detail: () => Promise<void>
14
+ labelsSet: () => Promise<void>
15
+ exec: () => Promise<void>
16
+ verifyParity: (args: string[]) => Promise<number>
17
+ }
18
+
19
+ /** Returns the process exit code, or null when the subcommand is not ours. */
20
+ export async function scorersCommand(args: string[], io: ScorersCommandIo): Promise<number | null> {
21
+ const subcommand = args[1]
22
+ // A Map, not an object literal: `scorers constructor` must not resolve to
23
+ // an Object.prototype member and be treated as a known subcommand.
24
+ const handlers = new Map<string, () => Promise<void>>([
25
+ ['list', io.list], ['register', io.register], ['detail', io.detail], ['exec', io.exec],
26
+ ])
27
+ const handler = subcommand === undefined ? undefined : handlers.get(subcommand)
28
+ if (handler) {
29
+ await handler()
30
+ return 0
31
+ }
32
+ if (subcommand === 'labels' && args[2] === 'set') {
33
+ await io.labelsSet()
34
+ return 0
35
+ }
36
+ if (subcommand === 'verify-parity') {
37
+ return io.verifyParity(args.slice(2))
38
+ }
39
+ return null
40
+ }