orizu 0.5.28 → 0.5.30

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 (232) hide show
  1. package/README.md +41 -2
  2. package/dist/account-directory.d.ts +132 -0
  3. package/dist/account-directory.d.ts.map +1 -0
  4. package/dist/account-directory.js +434 -0
  5. package/dist/app-commands.d.ts +29 -0
  6. package/dist/app-commands.d.ts.map +1 -0
  7. package/dist/app-commands.js +375 -0
  8. package/dist/auth-commands.d.ts +11 -0
  9. package/dist/auth-commands.d.ts.map +1 -0
  10. package/dist/auth-commands.js +321 -0
  11. package/dist/cli-console-output.d.ts +12 -0
  12. package/dist/cli-console-output.d.ts.map +1 -0
  13. package/dist/cli-console-output.js +57 -0
  14. package/dist/cli-project-slug.d.ts +20 -0
  15. package/dist/cli-project-slug.d.ts.map +1 -0
  16. package/dist/cli-project-slug.js +36 -0
  17. package/dist/command-line-arguments.d.ts +15 -0
  18. package/dist/command-line-arguments.d.ts.map +1 -0
  19. package/dist/command-line-arguments.js +77 -0
  20. package/dist/credentials.d.ts +6 -0
  21. package/dist/credentials.d.ts.map +1 -1
  22. package/dist/credentials.js +58 -4
  23. package/dist/dataset-commands.d.ts +16 -0
  24. package/dist/dataset-commands.d.ts.map +1 -0
  25. package/dist/dataset-commands.js +634 -0
  26. package/dist/dataset-download.d.ts.map +1 -1
  27. package/dist/dataset-download.js +4 -3
  28. package/dist/egress-policy.d.ts +8 -5
  29. package/dist/egress-policy.d.ts.map +1 -1
  30. package/dist/egress-policy.js +18 -7
  31. package/dist/feedback-cli.d.ts +14 -0
  32. package/dist/feedback-cli.d.ts.map +1 -0
  33. package/dist/feedback-cli.js +615 -0
  34. package/dist/feedback-contract.d.ts +79 -0
  35. package/dist/feedback-contract.d.ts.map +1 -0
  36. package/dist/feedback-contract.js +428 -0
  37. package/dist/feedback-scrub.d.ts +16 -0
  38. package/dist/feedback-scrub.d.ts.map +1 -0
  39. package/dist/feedback-scrub.js +610 -0
  40. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  41. package/dist/gepa-engine-dispatch.js +12 -2
  42. package/dist/global-flags.d.ts +1 -0
  43. package/dist/global-flags.d.ts.map +1 -1
  44. package/dist/global-flags.js +7 -0
  45. package/dist/headless-login.d.ts +2 -0
  46. package/dist/headless-login.d.ts.map +1 -1
  47. package/dist/headless-login.js +14 -11
  48. package/dist/help.d.ts.map +1 -1
  49. package/dist/help.js +103 -2
  50. package/dist/hosted-boot.d.ts +5 -0
  51. package/dist/hosted-boot.d.ts.map +1 -1
  52. package/dist/hosted-boot.js +12 -3
  53. package/dist/hosted-environment.d.ts +10 -0
  54. package/dist/hosted-environment.d.ts.map +1 -1
  55. package/dist/hosted-environment.js +81 -0
  56. package/dist/hosted-gepa-cli.d.ts.map +1 -1
  57. package/dist/hosted-gepa-cli.js +37 -5
  58. package/dist/hosted-harness-claude.d.ts +1 -2
  59. package/dist/hosted-harness-claude.d.ts.map +1 -1
  60. package/dist/hosted-harness-claude.js +8 -4
  61. package/dist/hosted-harness-opencode.d.ts +15 -10
  62. package/dist/hosted-harness-opencode.d.ts.map +1 -1
  63. package/dist/hosted-harness-opencode.js +27 -14
  64. package/dist/hosted-optimization-refusals.d.ts +6 -1
  65. package/dist/hosted-optimization-refusals.d.ts.map +1 -1
  66. package/dist/hosted-optimization-refusals.js +8 -2
  67. package/dist/hosted-optimization.d.ts.map +1 -1
  68. package/dist/hosted-optimization.js +40 -10
  69. package/dist/hosted-provider-credentials.d.ts +16 -0
  70. package/dist/hosted-provider-credentials.d.ts.map +1 -0
  71. package/dist/hosted-provider-credentials.js +36 -0
  72. package/dist/hosted-provider-settings.d.ts +7 -4
  73. package/dist/hosted-provider-settings.d.ts.map +1 -1
  74. package/dist/hosted-provider-settings.js +16 -13
  75. package/dist/http.d.ts +16 -3
  76. package/dist/http.d.ts.map +1 -1
  77. package/dist/http.js +57 -27
  78. package/dist/index.d.ts +3 -6
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +382 -3686
  81. package/dist/instruction-set-sync/index.d.ts +1 -0
  82. package/dist/instruction-set-sync/index.d.ts.map +1 -1
  83. package/dist/instruction-set-sync/index.js +15 -6
  84. package/dist/last-error-caps.d.ts +7 -0
  85. package/dist/last-error-caps.d.ts.map +1 -0
  86. package/dist/last-error-caps.js +6 -0
  87. package/dist/last-error-record.d.ts +37 -0
  88. package/dist/last-error-record.d.ts.map +1 -0
  89. package/dist/last-error-record.js +316 -0
  90. package/dist/model-config-fields.d.ts +2 -4
  91. package/dist/model-config-fields.d.ts.map +1 -1
  92. package/dist/model-config-fields.js +26 -9
  93. package/dist/model-config-price.d.ts +17 -0
  94. package/dist/model-config-price.d.ts.map +1 -0
  95. package/dist/model-config-price.js +42 -0
  96. package/dist/model-configs-cli.d.ts +2 -0
  97. package/dist/model-configs-cli.d.ts.map +1 -1
  98. package/dist/model-configs-cli.js +47 -4
  99. package/dist/optimization-export-cli.d.ts.map +1 -1
  100. package/dist/optimization-export-cli.js +19 -0
  101. package/dist/preview-runtime.d.ts.map +1 -1
  102. package/dist/preview-runtime.js +11 -4
  103. package/dist/prompt-length-wire.d.ts +1 -1
  104. package/dist/prompt-length-wire.d.ts.map +1 -1
  105. package/dist/prompt-length-wire.js +1 -0
  106. package/dist/provider-lookup.d.ts +24 -0
  107. package/dist/provider-lookup.d.ts.map +1 -0
  108. package/dist/provider-lookup.js +43 -0
  109. package/dist/provider-probe.d.ts +24 -0
  110. package/dist/provider-probe.d.ts.map +1 -0
  111. package/dist/provider-probe.js +88 -0
  112. package/dist/provider-refusals.d.ts +83 -0
  113. package/dist/provider-refusals.d.ts.map +1 -0
  114. package/dist/provider-refusals.js +48 -0
  115. package/dist/provider-registry.d.ts +146 -0
  116. package/dist/provider-registry.d.ts.map +1 -0
  117. package/dist/provider-registry.js +155 -0
  118. package/dist/providers-cli.d.ts +3 -0
  119. package/dist/providers-cli.d.ts.map +1 -0
  120. package/dist/providers-cli.js +178 -0
  121. package/dist/runner-env-allowlist.d.ts +4 -0
  122. package/dist/runner-env-allowlist.d.ts.map +1 -0
  123. package/dist/runner-env-allowlist.js +36 -0
  124. package/dist/runner-env.d.ts +7 -0
  125. package/dist/runner-env.d.ts.map +1 -0
  126. package/dist/runner-env.js +52 -0
  127. package/dist/self-update.d.ts +42 -0
  128. package/dist/self-update.d.ts.map +1 -0
  129. package/dist/self-update.js +506 -0
  130. package/dist/setup-and-skills-commands.d.ts +11 -0
  131. package/dist/setup-and-skills-commands.d.ts.map +1 -0
  132. package/dist/setup-and-skills-commands.js +1055 -0
  133. package/dist/task-commands.d.ts +13 -0
  134. package/dist/task-commands.d.ts.map +1 -0
  135. package/dist/task-commands.js +512 -0
  136. package/dist/workspace.d.ts +1 -0
  137. package/dist/workspace.d.ts.map +1 -1
  138. package/dist/workspace.js +11 -0
  139. package/package.json +1 -1
  140. package/src/account-directory.ts +751 -0
  141. package/src/app-commands.ts +520 -0
  142. package/src/auth-commands.ts +388 -0
  143. package/src/cli-console-output.ts +83 -0
  144. package/src/cli-project-slug.ts +53 -0
  145. package/src/command-line-arguments.ts +111 -0
  146. package/src/credentials.ts +62 -4
  147. package/src/dataset-commands.ts +863 -0
  148. package/src/dataset-download.ts +4 -3
  149. package/src/egress-policy.ts +19 -8
  150. package/src/feedback-cli.ts +778 -0
  151. package/src/feedback-contract.ts +492 -0
  152. package/src/feedback-scrub.ts +799 -0
  153. package/src/gepa-engine-dispatch.ts +16 -2
  154. package/src/global-flags.ts +8 -0
  155. package/src/headless-login.ts +25 -11
  156. package/src/help.ts +103 -2
  157. package/src/hosted-boot.ts +21 -3
  158. package/src/hosted-environment.ts +70 -0
  159. package/src/hosted-gepa-cli.ts +35 -5
  160. package/src/hosted-harness-claude.ts +9 -4
  161. package/src/hosted-harness-opencode.ts +28 -14
  162. package/src/hosted-optimization-refusals.ts +11 -3
  163. package/src/hosted-optimization.ts +42 -12
  164. package/src/hosted-provider-credentials.js +2 -0
  165. package/src/hosted-provider-credentials.ts +42 -0
  166. package/src/hosted-provider-settings.ts +21 -13
  167. package/src/http.ts +75 -26
  168. package/src/index.ts +1134 -5348
  169. package/src/instruction-set-sync/index.ts +25 -6
  170. package/src/last-error-caps.js +4 -0
  171. package/src/last-error-caps.ts +6 -0
  172. package/src/last-error-record.ts +415 -0
  173. package/src/model-config-fields.ts +29 -10
  174. package/src/model-config-price.js +2 -0
  175. package/src/model-config-price.ts +55 -0
  176. package/src/model-configs-cli.ts +41 -4
  177. package/src/optimization-export-cli.ts +23 -0
  178. package/src/preview-runtime.ts +11 -4
  179. package/src/prompt-length-wire.ts +1 -0
  180. package/src/provider-lookup.js +2 -0
  181. package/src/provider-lookup.ts +73 -0
  182. package/src/provider-probe.ts +101 -0
  183. package/src/provider-refusals.ts +53 -0
  184. package/src/provider-registry.js +4 -0
  185. package/src/provider-registry.ts +186 -0
  186. package/src/providers-cli.ts +153 -0
  187. package/src/runner-env-allowlist.js +4 -0
  188. package/src/runner-env-allowlist.ts +38 -0
  189. package/src/runner-env.js +4 -0
  190. package/src/runner-env.ts +58 -0
  191. package/src/secret-redaction.js +4 -0
  192. package/src/self-update.ts +548 -0
  193. package/src/setup-and-skills-commands.ts +1242 -0
  194. package/src/task-commands.ts +703 -0
  195. package/src/workspace.ts +11 -0
  196. package/vendor/orizu-gepa/pyproject.toml +7 -0
  197. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +77 -39
  198. package/vendor/orizu-gepa/src/orizu_gepa_connector/providers.generated.json +32 -0
  199. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +5 -3
  200. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +16 -2
  201. package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +14 -1
  202. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +91 -24
  203. package/vendor/orizu-gepa-python/pyproject.toml +7 -0
  204. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +3 -0
  205. package/vendor/orizu-gepa-python/src/orizu_gepa/providers.generated.json +32 -0
  206. package/vendor/orizu-gepa-python/src/orizu_gepa/providers.py +321 -0
  207. package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +269 -89
  208. package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +40 -6
  209. package/vendor/skills/orizu/SKILL.md +26 -22
  210. package/vendor/skills/orizu/flows/expand.md +2 -0
  211. package/vendor/skills/orizu/flows/first-win.md +2 -0
  212. package/vendor/skills/orizu/flows/onboard.md +2 -0
  213. package/vendor/skills/orizu/flows/promote.md +5 -3
  214. package/vendor/skills/orizu/flows/recurse.md +2 -0
  215. package/vendor/skills/orizu/flows/triage.md +7 -5
  216. package/vendor/skills/orizu/references/assess-and-plan.md +16 -16
  217. package/vendor/skills/orizu/references/authority-map.md +1 -0
  218. package/vendor/skills/orizu/references/building-apps.md +92 -92
  219. package/vendor/skills/orizu/references/building-judges.md +6 -6
  220. package/vendor/skills/orizu/references/cli-reference.md +80 -14
  221. package/vendor/skills/orizu/references/dataset-design.md +3 -3
  222. package/vendor/skills/orizu/references/eval-strategy.md +8 -8
  223. package/vendor/skills/orizu/references/feedback.md +113 -0
  224. package/vendor/skills/orizu/references/instructions-after-prompts.md +5 -5
  225. package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +15 -15
  226. package/vendor/skills/orizu/references/migrate-hand-rolled-instruction-layout.md +4 -4
  227. package/vendor/skills/orizu/references/optimization-reports.md +5 -5
  228. package/vendor/skills/orizu/references/optimization-with-gepa.md +41 -35
  229. package/vendor/skills/orizu/references/orizu-in-your-codebase.md +8 -8
  230. package/vendor/skills/orizu/references/primer.md +42 -42
  231. package/vendor/skills/orizu/references/prompt-control-plane.md +29 -29
  232. package/vendor/skills/orizu/references/vocabulary.md +18 -18
@@ -14,7 +14,7 @@ import {
14
14
  import { assertOutputConfined, createManagedArtifactJournal, emitManagedArtifacts } from './helpers.js'
15
15
 
16
16
  const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/u
17
- const SYNC_LOCK_WAIT_MS = 5_000
17
+ export const SYNC_LOCK_WAIT_MS = 5_000
18
18
  const SYNC_LOCK_RETRY_MS = 50
19
19
  const SYNC_LOCK_STALE_MS = 60_000
20
20
 
@@ -513,7 +513,12 @@ export function withSyncLock<T>(appRoot: string, operation: () => T): T {
513
513
  mkdirSync(appRoot, { recursive: true })
514
514
  const lockPath = join(appRoot, '.orizu.lock.json.lock')
515
515
  const metadata = `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`
516
- const deadline = Date.now() + SYNC_LOCK_WAIT_MS
516
+ const lockWaitMs = processTestMilliseconds(
517
+ 'ORIZU_TEST_SYNC_LOCK_WAIT_MS',
518
+ 100,
519
+ SYNC_LOCK_WAIT_MS
520
+ ) ?? SYNC_LOCK_WAIT_MS
521
+ const deadline = Date.now() + lockWaitMs
517
522
 
518
523
  while (true) {
519
524
  try {
@@ -561,14 +566,28 @@ export function withSyncLock<T>(appRoot: string, operation: () => T): T {
561
566
  }
562
567
 
563
568
  // Test-only process boundary hooks. Production behavior is unchanged unless a
564
- // test child explicitly supplies one of the bounded ORIZU_TEST_SYNC_HOLD_* values.
565
- function holdForProcessTest(name: string): void {
569
+ // test child explicitly supplies one of the bounded ORIZU_TEST_SYNC_* values.
570
+ function processTestMilliseconds(
571
+ name: string,
572
+ minimum: number,
573
+ maximum: number
574
+ ): number | undefined {
566
575
  const raw = process.env[name]
567
- if (raw === undefined) return
576
+ if (raw === undefined) return undefined
568
577
  const milliseconds = Number(raw)
569
- if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || milliseconds > 10_000) {
578
+ if (
579
+ !Number.isSafeInteger(milliseconds) ||
580
+ milliseconds < minimum ||
581
+ milliseconds > maximum
582
+ ) {
570
583
  throw new Error(`instruction_set_sync_test_hold_invalid:${name}`)
571
584
  }
585
+ return milliseconds
586
+ }
587
+
588
+ function holdForProcessTest(name: string): void {
589
+ const milliseconds = processTestMilliseconds(name, 0, 10_000)
590
+ if (milliseconds === undefined) return
572
591
  sleepSync(milliseconds)
573
592
  }
574
593
 
@@ -0,0 +1,4 @@
1
+ // Turbopack does not apply TypeScript's NodeNext .js-to-.ts resolution for
2
+ // source imported by the Next.js app. NodeNext itself resolves the canonical
3
+ // TypeScript module and emits this same .js specifier for the CLI build.
4
+ export * from './last-error-caps.ts'
@@ -0,0 +1,6 @@
1
+ export const LAST_ERROR_MESSAGE_MAX_CHARS = 2_000
2
+ export const LAST_ERROR_ARG_MAX_CHARS = 200
3
+ export const LAST_ERROR_MAX_ARGS = 32
4
+ export const LAST_ERROR_FILE_MAX_BYTES = 16_384
5
+ export const LAST_ERROR_SERVER_BASE_URL_MAX_BYTES = 256
6
+ export const LAST_ERROR_CODE_MAX_CHARS = 64
@@ -0,0 +1,415 @@
1
+ import { randomBytes } from 'crypto'
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ fsyncSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readFileSync,
9
+ renameSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from 'fs'
14
+ import { homedir } from 'os'
15
+ import { join } from 'path'
16
+
17
+ import {
18
+ getConfigDir,
19
+ getSecretsSeenThisProcess,
20
+ resolveAuthTokenForBaseUrl,
21
+ resolveEnvBearerToken,
22
+ } from './credentials.js'
23
+ import { scrubFeedbackText } from './feedback-scrub.js'
24
+ import { getBaseUrl } from './http.js'
25
+ import {
26
+ LAST_ERROR_ARG_MAX_CHARS,
27
+ LAST_ERROR_CODE_MAX_CHARS,
28
+ LAST_ERROR_FILE_MAX_BYTES,
29
+ LAST_ERROR_MAX_ARGS,
30
+ LAST_ERROR_MESSAGE_MAX_CHARS,
31
+ LAST_ERROR_SERVER_BASE_URL_MAX_BYTES,
32
+ } from './last-error-caps.js'
33
+ import { runnerForwardedSecretValues } from './runner-env.js'
34
+
35
+ export interface LastErrorRecord {
36
+ version: 1
37
+ recordedAt: string
38
+ cliVersion: string | null
39
+ code: string | null
40
+ serverBaseUrl: string | null
41
+ teamSlug: string | null
42
+ command: string | null
43
+ argv: string[]
44
+ argvTruncated: boolean
45
+ message: string
46
+ messageTruncated: boolean
47
+ }
48
+
49
+ export const LAST_ERROR_RECORD_VERSION = 1
50
+ export const LAST_ERROR_FILENAME = 'last-error.json'
51
+ export const LAST_ERROR_ATTACH_MAX_AGE_MS = 86_400_000
52
+ export const LAST_ERROR_TRUNCATION_MARKER = '[truncated]'
53
+
54
+ const LAST_ERROR_MESSAGE_PRE_SCRUB_MAX_CHARS = 65_536
55
+ const CREDENTIAL_BEARING_FLAGS = new Set([
56
+ '--model-key',
57
+ '--token',
58
+ '--api-key',
59
+ '--password',
60
+ '--secret',
61
+ '--access-token',
62
+ '--refresh-token',
63
+ '--private-key',
64
+ '--client-secret',
65
+ ])
66
+
67
+ export interface LastErrorRecordInput {
68
+ argv: readonly string[]
69
+ message: string
70
+ cliVersion: string | null
71
+ code?: string | null
72
+ serverBaseUrl?: string | null
73
+ teamSlug?: string | null
74
+ homeDir?: string | null
75
+ secrets?: readonly string[]
76
+ now?: Date
77
+ }
78
+
79
+ export interface WriteLastErrorRecordOptions {
80
+ configDir?: string
81
+ }
82
+
83
+ interface ScrubContext {
84
+ homeDir: string | null
85
+ secrets: readonly string[]
86
+ }
87
+
88
+ function removeTrailingCredentialResidue(input: string): string {
89
+ const hasMarker = input.endsWith(LAST_ERROR_TRUNCATION_MARKER)
90
+ const body = hasMarker
91
+ ? input.slice(0, -LAST_ERROR_TRUNCATION_MARKER.length)
92
+ : input
93
+ const withoutResidue = body.replace(
94
+ /(?:\borizu_(?:agent|pat)_|\bgithub_pat_|\bgh[posur]_?|\bsk-(?:ant-|proj-)?)[^\s]*$/i,
95
+ ''
96
+ )
97
+ return hasMarker
98
+ ? `${withoutResidue}${LAST_ERROR_TRUNCATION_MARKER}`
99
+ : withoutResidue
100
+ }
101
+
102
+ function scrubWithContext(input: string, context: ScrubContext): string {
103
+ return scrubFeedbackText(input, {
104
+ reporterEmail: undefined,
105
+ homeDir: context.homeDir,
106
+ secrets: context.secrets,
107
+ })
108
+ }
109
+
110
+ function scrubAndCap(
111
+ input: string,
112
+ maximumCharacters: number,
113
+ context: ScrubContext
114
+ ): { value: string; wasTruncated: boolean } {
115
+ const scrubbed = scrubWithContext(input, context)
116
+ const wasTruncated = scrubbed.length > maximumCharacters
117
+ const capped = wasTruncated
118
+ ? `${scrubbed.slice(0, maximumCharacters)}${LAST_ERROR_TRUNCATION_MARKER}`
119
+ : scrubbed
120
+ const withoutResidue = removeTrailingCredentialResidue(capped)
121
+ const finalScrubbed = scrubWithContext(withoutResidue, context)
122
+ const finalBody = wasTruncated && finalScrubbed.endsWith(LAST_ERROR_TRUNCATION_MARKER)
123
+ ? finalScrubbed.slice(0, -LAST_ERROR_TRUNCATION_MARKER.length)
124
+ : finalScrubbed
125
+ return {
126
+ value: wasTruncated
127
+ ? `${finalBody.slice(0, maximumCharacters)}${LAST_ERROR_TRUNCATION_MARKER}`
128
+ : finalBody.slice(0, maximumCharacters),
129
+ wasTruncated,
130
+ }
131
+ }
132
+
133
+ function isCredentialBearingFlag(argument: string): boolean {
134
+ const normalizedArgument = argument.toLowerCase()
135
+ return CREDENTIAL_BEARING_FLAGS.has(normalizedArgument)
136
+ || /^--.+[_.-](?:key|token|secret|password)$/.test(normalizedArgument)
137
+ }
138
+
139
+ function credentialBearingArgvSecrets(argv: readonly string[]): string[] {
140
+ const secrets: string[] = []
141
+ for (let index = 0; index < argv.length; index += 1) {
142
+ const argument = argv[index]
143
+ const equalsIndex = argument.indexOf('=')
144
+ if (equalsIndex > 0 && isCredentialBearingFlag(argument.slice(0, equalsIndex))) {
145
+ const value = argument.slice(equalsIndex + 1)
146
+ if (value.length >= 8) secrets.push(value)
147
+ continue
148
+ }
149
+
150
+ if (!isCredentialBearingFlag(argument)) continue
151
+ const value = argv[index + 1]
152
+ if (value && !value.startsWith('--') && value.length >= 8) secrets.push(value)
153
+ index += 1
154
+ }
155
+ return secrets
156
+ }
157
+
158
+ function maskCredentialBearingArgument(
159
+ argv: readonly string[],
160
+ index: number
161
+ ): string {
162
+ const argument = argv[index]
163
+ if (
164
+ index > 0
165
+ && isCredentialBearingFlag(argv[index - 1])
166
+ ) {
167
+ return '[redacted]'
168
+ }
169
+
170
+ const equalsIndex = argument.indexOf('=')
171
+ if (
172
+ equalsIndex > 0
173
+ && isCredentialBearingFlag(argument.slice(0, equalsIndex))
174
+ ) {
175
+ return `${argument.slice(0, equalsIndex + 1)}[redacted]`
176
+ }
177
+ return argument
178
+ }
179
+
180
+ function commandAfterGlobalFlags(argv: readonly string[]): string | null {
181
+ const commandArguments: string[] = []
182
+ for (let index = 0; index < argv.length; index += 1) {
183
+ const argument = argv[index]
184
+ if (argument === '--local') continue
185
+ if (argument === '--server') {
186
+ index += 1
187
+ continue
188
+ }
189
+ commandArguments.push(argument)
190
+ }
191
+ if (commandArguments[0] === '--json') commandArguments.shift()
192
+ return commandArguments[0] ?? null
193
+ }
194
+
195
+ function serializeRecord(record: LastErrorRecord): string {
196
+ return `${JSON.stringify(record, null, 2)}\n`
197
+ }
198
+
199
+ function isSerializedRecordTooLarge(record: LastErrorRecord): boolean {
200
+ return Buffer.byteLength(serializeRecord(record), 'utf8') > LAST_ERROR_FILE_MAX_BYTES
201
+ }
202
+
203
+ function resolveActiveProcessSecrets(): readonly string[] {
204
+ const rememberedSecrets = getSecretsSeenThisProcess()
205
+ try {
206
+ const environmentBearer = resolveEnvBearerToken()
207
+ const currentBearer = environmentBearer ?? resolveAuthTokenForBaseUrl(getBaseUrl())
208
+ return [...new Set([...rememberedSecrets, currentBearer])]
209
+ } catch {
210
+ return rememberedSecrets
211
+ }
212
+ }
213
+
214
+ function propertyValue(value: unknown, key: string, ownOnly = false): unknown {
215
+ if ((typeof value !== 'object' || value === null) && typeof value !== 'function') return undefined
216
+ try {
217
+ return !ownOnly || Object.hasOwn(value, key) ? Reflect.get(value, key) : undefined
218
+ } catch {
219
+ return undefined
220
+ }
221
+ }
222
+
223
+ export function extractLastErrorCode(error: unknown): string | null {
224
+ const ownCode = propertyValue(error, 'code', true)
225
+ if (typeof ownCode === 'string') return ownCode
226
+ const causeCode = propertyValue(propertyValue(error, 'cause'), 'code')
227
+ return typeof causeCode === 'string' ? causeCode : null
228
+ }
229
+
230
+ function resolveHomeDir(): string | null {
231
+ try {
232
+ return homedir()
233
+ } catch {
234
+ return null
235
+ }
236
+ }
237
+
238
+ function isStringWithinCap(
239
+ value: unknown,
240
+ maximumCharacters: number,
241
+ wasTruncated: boolean
242
+ ): value is string {
243
+ if (typeof value !== 'string') return false
244
+ if (value.length <= maximumCharacters) return true
245
+ return wasTruncated
246
+ && value.length <= maximumCharacters + LAST_ERROR_TRUNCATION_MARKER.length
247
+ && value.endsWith(LAST_ERROR_TRUNCATION_MARKER)
248
+ }
249
+
250
+ function boundedServerBaseUrl(value: string | null | undefined): string | null {
251
+ return value && Buffer.byteLength(value, 'utf8') <= LAST_ERROR_SERVER_BASE_URL_MAX_BYTES
252
+ ? value
253
+ : null
254
+ }
255
+
256
+ function isLastErrorRecord(value: unknown): value is LastErrorRecord {
257
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false
258
+ const record = value as Partial<LastErrorRecord>
259
+ if (record.version !== LAST_ERROR_RECORD_VERSION) return false
260
+ if (typeof record.recordedAt !== 'string' || !Number.isFinite(Date.parse(record.recordedAt))) return false
261
+ if (typeof record.cliVersion !== 'string' && record.cliVersion !== null) return false
262
+ if (record.code !== undefined && record.code !== null && (
263
+ typeof record.code !== 'string'
264
+ || Array.from(record.code).length > LAST_ERROR_CODE_MAX_CHARS
265
+ )) return false
266
+ if (record.serverBaseUrl !== undefined && typeof record.serverBaseUrl !== 'string' && record.serverBaseUrl !== null) return false
267
+ if (record.teamSlug !== undefined && typeof record.teamSlug !== 'string' && record.teamSlug !== null) return false
268
+ if (typeof record.argvTruncated !== 'boolean') return false
269
+ const argvTruncated = record.argvTruncated
270
+ if (record.command !== null && !isStringWithinCap(
271
+ record.command,
272
+ LAST_ERROR_ARG_MAX_CHARS,
273
+ argvTruncated
274
+ )) return false
275
+ if (!Array.isArray(record.argv) || record.argv.length > LAST_ERROR_MAX_ARGS) return false
276
+ if (!record.argv.every(argument => isStringWithinCap(
277
+ argument,
278
+ LAST_ERROR_ARG_MAX_CHARS,
279
+ argvTruncated
280
+ ))) return false
281
+ if (typeof record.messageTruncated !== 'boolean') return false
282
+ return isStringWithinCap(
283
+ record.message,
284
+ LAST_ERROR_MESSAGE_MAX_CHARS,
285
+ record.messageTruncated
286
+ )
287
+ }
288
+
289
+ export function buildLastErrorRecord(input: LastErrorRecordInput): LastErrorRecord {
290
+ const context: ScrubContext = {
291
+ homeDir: input.homeDir ?? null,
292
+ secrets: [
293
+ ...(input.secrets ?? []),
294
+ ...credentialBearingArgvSecrets(input.argv),
295
+ ],
296
+ }
297
+ const messageInput = input.message.slice(0, LAST_ERROR_MESSAGE_PRE_SCRUB_MAX_CHARS)
298
+ const cappedMessage = scrubAndCap(messageInput, LAST_ERROR_MESSAGE_MAX_CHARS, context)
299
+ const retainedArguments = input.argv.slice(0, LAST_ERROR_MAX_ARGS)
300
+ let argvTruncated = input.argv.length > LAST_ERROR_MAX_ARGS
301
+ const argv = retainedArguments.map((_argument, index) => {
302
+ const maskedArgument = maskCredentialBearingArgument(input.argv, index)
303
+ const cappedArgument = scrubAndCap(maskedArgument, LAST_ERROR_ARG_MAX_CHARS, context)
304
+ argvTruncated ||= cappedArgument.wasTruncated
305
+ return cappedArgument.value
306
+ })
307
+ const rawCommand = commandAfterGlobalFlags(input.argv)
308
+ const command = rawCommand === null
309
+ ? null
310
+ : scrubAndCap(rawCommand, LAST_ERROR_ARG_MAX_CHARS, context).value
311
+ const code = input.code === undefined || input.code === null
312
+ ? null
313
+ : Array.from(scrubAndCap(
314
+ input.code, LAST_ERROR_CODE_MAX_CHARS * 2, context
315
+ ).value).slice(0, LAST_ERROR_CODE_MAX_CHARS).join('')
316
+
317
+ const record: LastErrorRecord = {
318
+ version: LAST_ERROR_RECORD_VERSION,
319
+ recordedAt: (input.now ?? new Date()).toISOString(),
320
+ cliVersion: input.cliVersion,
321
+ code,
322
+ serverBaseUrl: boundedServerBaseUrl(input.serverBaseUrl),
323
+ teamSlug: input.teamSlug ?? null,
324
+ command,
325
+ argv,
326
+ argvTruncated,
327
+ message: cappedMessage.value,
328
+ messageTruncated: cappedMessage.wasTruncated || input.message.length > LAST_ERROR_MESSAGE_PRE_SCRUB_MAX_CHARS,
329
+ }
330
+
331
+ if (isSerializedRecordTooLarge(record)) {
332
+ record.argv = []
333
+ record.argvTruncated = true
334
+ }
335
+ while (isSerializedRecordTooLarge(record) && record.message.length > LAST_ERROR_TRUNCATION_MARKER.length) {
336
+ const content = record.message.endsWith(LAST_ERROR_TRUNCATION_MARKER)
337
+ ? record.message.slice(0, -LAST_ERROR_TRUNCATION_MARKER.length)
338
+ : record.message
339
+ record.message = scrubAndCap(content, Math.floor(content.length / 2), context).value
340
+ if (!record.message.endsWith(LAST_ERROR_TRUNCATION_MARKER)) {
341
+ record.message += LAST_ERROR_TRUNCATION_MARKER
342
+ }
343
+ record.messageTruncated = true
344
+ }
345
+
346
+ return record
347
+ }
348
+
349
+ export function getLastErrorRecordPath(configDir?: string): string {
350
+ return join(configDir ?? getConfigDir(), LAST_ERROR_FILENAME)
351
+ }
352
+
353
+ export function writeLastErrorRecord(
354
+ input: LastErrorRecordInput,
355
+ options: WriteLastErrorRecordOptions = {}
356
+ ): void {
357
+ try {
358
+ const configDirectory = options.configDir ?? getConfigDir()
359
+ mkdirSync(configDirectory, { recursive: true, mode: 0o700 })
360
+ chmodSync(configDirectory, 0o700)
361
+
362
+ const record = buildLastErrorRecord({
363
+ ...input,
364
+ homeDir: input.homeDir === undefined ? resolveHomeDir() : input.homeDir,
365
+ secrets: [
366
+ ...resolveActiveProcessSecrets(),
367
+ ...runnerForwardedSecretValues(),
368
+ ...(input.secrets ?? []),
369
+ ],
370
+ })
371
+ const serialized = serializeRecord(record)
372
+ if (Buffer.byteLength(serialized, 'utf8') > LAST_ERROR_FILE_MAX_BYTES) return
373
+
374
+ const path = getLastErrorRecordPath(configDirectory)
375
+ const temporaryPath = join(
376
+ configDirectory,
377
+ `.${LAST_ERROR_FILENAME}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`
378
+ )
379
+
380
+ try {
381
+ const fileDescriptor = openSync(temporaryPath, 'wx', 0o600)
382
+ try {
383
+ writeFileSync(fileDescriptor, serialized, 'utf8')
384
+ fsyncSync(fileDescriptor)
385
+ } finally {
386
+ closeSync(fileDescriptor)
387
+ }
388
+ chmodSync(temporaryPath, 0o600)
389
+ renameSync(temporaryPath, path)
390
+ chmodSync(path, 0o600)
391
+ } finally {
392
+ rmSync(temporaryPath, { force: true })
393
+ }
394
+ } catch {
395
+ // A diagnostic record is best effort and must never replace the original CLI failure.
396
+ }
397
+ }
398
+
399
+ export function readLastErrorRecord(configDir?: string): LastErrorRecord | null {
400
+ try {
401
+ const path = getLastErrorRecordPath(configDir)
402
+ if (statSync(path).size > LAST_ERROR_FILE_MAX_BYTES) return null
403
+ const record = JSON.parse(readFileSync(path, 'utf8')) as unknown
404
+ return isLastErrorRecord(record)
405
+ ? {
406
+ ...record,
407
+ code: record.code ?? null,
408
+ serverBaseUrl: boundedServerBaseUrl(record.serverBaseUrl),
409
+ teamSlug: record.teamSlug ?? null,
410
+ }
411
+ : null
412
+ } catch {
413
+ return null
414
+ }
415
+ }
@@ -1,3 +1,5 @@
1
+ import type { WireProtocol } from './provider-registry.js'
2
+
1
3
  export const MODEL_CONFIG_FIELDS = [
2
4
  // The pilot's frozen settings carry the provider model name under `model`.
3
5
  { field: 'MODEL', settingsKeys: ['model'], kind: 'string' },
@@ -7,9 +9,9 @@ export const MODEL_CONFIG_FIELDS = [
7
9
  { field: 'THINKING_LEVEL', settingsKeys: ['reasoning_effort', 'reasoning.effort', 'output_config.effort'], kind: 'string' },
8
10
  // Responses uses `max_output_tokens`; Messages uses `max_tokens` for the output limit.
9
11
  { field: 'MAX_OUTPUT_TOKENS', settingsKeys: ['max_output_tokens', 'max_tokens'], kind: 'number' },
10
- // `temperature` feeds the temperature call option for both supported providers.
12
+ // `temperature` feeds the temperature call option for the applicable protocols.
11
13
  { field: 'TEMPERATURE', settingsKeys: ['temperature'], kind: 'number' },
12
- // `top_p` feeds the top-p call option accepted by both supported providers.
14
+ // `top_p` feeds the top-p call option accepted by the applicable protocols.
13
15
  { field: 'TOP_P', settingsKeys: ['top_p'], kind: 'number' },
14
16
  // `strict_json_schema` controls strict schema enforcement in a structured-output call.
15
17
  { field: 'STRICT_JSON_SCHEMA', settingsKeys: ['strict_json_schema'], kind: 'boolean' },
@@ -18,14 +20,31 @@ export const MODEL_CONFIG_FIELDS = [
18
20
  export type ModelConfigField = typeof MODEL_CONFIG_FIELDS[number]['field']
19
21
 
20
22
  // Dotted settings keys resolve exactly one object level; they are not a general path language.
21
- export const PROVIDER_REQUEST_FIELDS: Record<ModelConfigField, { openai: string | null; anthropic: string | null }> = {
22
- MODEL: { openai: 'model', anthropic: 'model' },
23
+ // Keyed by WIRE PROTOCOL, not by provider name: two providers speaking the same
24
+ // protocol take the same request fields (ORI-2031 row 3).
25
+ //
26
+ // Every cell is MEASURED against a live endpoint, never taken from
27
+ // documentation (ADR-013). T1 shipped this table without an `openai-chat`
28
+ // column because Orizu had never sent a chat-completions request; ORI-2032
29
+ // measured that protocol on 2026-09-12 and filled it in. The measured
30
+ // contract, including which fields each model accepted, is recorded in
31
+ // docs/requirements/any-provider-support/t2-python-transport-plan.md.
32
+ export const PROVIDER_REQUEST_FIELDS: Record<ModelConfigField, Record<WireProtocol, string | null>> = {
33
+ MODEL: { 'openai-responses': 'model', 'openai-chat': 'model', 'anthropic-messages': 'model' },
23
34
  // Orizu consumes protocol selection; it is not a provider request field.
24
- PROTOCOL: { openai: null, anthropic: null },
25
- THINKING_LEVEL: { openai: 'reasoning.effort', anthropic: 'output_config.effort' },
26
- MAX_OUTPUT_TOKENS: { openai: 'max_output_tokens', anthropic: 'max_tokens' },
27
- TEMPERATURE: { openai: 'temperature', anthropic: 'temperature' },
28
- TOP_P: { openai: 'top_p', anthropic: 'top_p' },
35
+ PROTOCOL: { 'openai-responses': null, 'openai-chat': null, 'anthropic-messages': null },
36
+ // null for openai-chat: Orizu's chat builder sends NO reasoning field. The
37
+ // protocol is not enough to decide one — OpenRouter takes `reasoning.effort`
38
+ // and OpenAI chat a top-level `reasoning_effort` — and a customer's own
39
+ // reflection_provider_settings reach the provider verbatim either way.
40
+ THINKING_LEVEL: { 'openai-responses': 'reasoning.effort', 'openai-chat': null, 'anthropic-messages': 'output_config.effort' },
41
+ // 'max_completion_tokens' is measured: gpt-4o-mini accepted either field,
42
+ // gpt-5-mini and o4-mini rejected 'max_tokens' with unsupported_parameter.
43
+ MAX_OUTPUT_TOKENS: { 'openai-responses': 'max_output_tokens', 'openai-chat': 'max_completion_tokens', 'anthropic-messages': 'max_tokens' },
44
+ // openai-chat: accepted by gpt-4o-mini; rejected with 400 by gpt-5-mini and o4-mini, so the builder only sends temperature when set.
45
+ TEMPERATURE: { 'openai-responses': 'temperature', 'openai-chat': 'temperature', 'anthropic-messages': 'temperature' },
46
+ // openai-chat: accepted by gpt-4o-mini; rejected with 400 by gpt-5-mini and o4-mini, so the builder only sends top_p when set.
47
+ TOP_P: { 'openai-responses': 'top_p', 'openai-chat': 'top_p', 'anthropic-messages': 'top_p' },
29
48
  // Orizu consumes strict schema selection; it is not a provider request field.
30
- STRICT_JSON_SCHEMA: { openai: null, anthropic: null },
49
+ STRICT_JSON_SCHEMA: { 'openai-responses': null, 'openai-chat': null, 'anthropic-messages': null },
31
50
  }
@@ -0,0 +1,2 @@
1
+ // Next's bundler follows this shim; the CLI compiles the TypeScript source.
2
+ export * from './model-config-price.ts'
@@ -0,0 +1,55 @@
1
+ /** Current model-config price metadata; never invocation settings. */
2
+ export interface ModelConfigPrice {
3
+ currency?: 'USD'
4
+ input_usd_per_million_tokens: number
5
+ output_usd_per_million_tokens: number
6
+ cache_read_usd_per_million_tokens?: number
7
+ cache_write_usd_per_million_tokens?: number
8
+ }
9
+
10
+ export interface ModelConfigTokenRates {
11
+ inputUsdPerToken: number
12
+ outputUsdPerToken: number
13
+ cachedInputUsdPerToken?: number
14
+ cacheWriteUsdPerToken?: number
15
+ }
16
+
17
+ const rateFields = [
18
+ 'input_usd_per_million_tokens',
19
+ 'output_usd_per_million_tokens',
20
+ 'cache_read_usd_per_million_tokens',
21
+ 'cache_write_usd_per_million_tokens',
22
+ ] as const
23
+ const allowedFields = new Set<string>(['currency', ...rateFields])
24
+
25
+ function invalid(field: string): never {
26
+ throw new Error(`model_config_price_invalid:${field}`)
27
+ }
28
+
29
+ export function parseModelConfigPrice(value: unknown): ModelConfigPrice {
30
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) invalid('price')
31
+ const record = value as Record<string, unknown>
32
+ for (const key of Object.keys(record)) if (!allowedFields.has(key)) invalid(key)
33
+ if ('currency' in record && record.currency !== 'USD') invalid('currency')
34
+ for (const field of rateFields) {
35
+ if (field.startsWith('cache_') && !(field in record)) continue
36
+ const rate = record[field]
37
+ if (typeof rate !== 'number' || !Number.isFinite(rate) || rate < 0 || rate > 1e308) invalid(field)
38
+ }
39
+ return {
40
+ ...(record.currency === 'USD' ? { currency: 'USD' } : {}),
41
+ input_usd_per_million_tokens: record.input_usd_per_million_tokens as number,
42
+ output_usd_per_million_tokens: record.output_usd_per_million_tokens as number,
43
+ ...('cache_read_usd_per_million_tokens' in record ? { cache_read_usd_per_million_tokens: record.cache_read_usd_per_million_tokens as number } : {}),
44
+ ...('cache_write_usd_per_million_tokens' in record ? { cache_write_usd_per_million_tokens: record.cache_write_usd_per_million_tokens as number } : {}),
45
+ }
46
+ }
47
+
48
+ export function ratesFromModelConfigPrice(price: ModelConfigPrice): ModelConfigTokenRates {
49
+ return {
50
+ inputUsdPerToken: price.input_usd_per_million_tokens / 1_000_000,
51
+ outputUsdPerToken: price.output_usd_per_million_tokens / 1_000_000,
52
+ ...(price.cache_read_usd_per_million_tokens === undefined ? {} : { cachedInputUsdPerToken: price.cache_read_usd_per_million_tokens / 1_000_000 }),
53
+ ...(price.cache_write_usd_per_million_tokens === undefined ? {} : { cacheWriteUsdPerToken: price.cache_write_usd_per_million_tokens / 1_000_000 }),
54
+ }
55
+ }