@iislee/opencodex 2.11.0 → 2.31.0

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 (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,967 @@
1
+ /**
2
+ * prompt-layers.ts — the Codex prompt-layer surface in `$CODEX_HOME/config.toml`.
3
+ *
4
+ * Scope boundary: this module owns the five `include_*` prompt toggles and the
5
+ * generated `developer_instructions` projection. It is a SIBLING of
6
+ * `features.ts`, not an extension of it: that module's header explicitly forbids
7
+ * broadening itself past `multi_agent_v2`, so the technique is copied here
8
+ * rather than the file being widened.
9
+ *
10
+ * Two design decisions are load-bearing and were forced by an adversarial audit
11
+ * (devlog/_plan/260802_codex_set_prompt_composer/):
12
+ *
13
+ * 1. NO USER PROSE IS PARSED BACK OUT OF TOML. Custom layers live in
14
+ * `opencodex-prompt.json`, which we own outright; config.toml receives a
15
+ * write-only projection of the enabled subset. Layer identity never has to
16
+ * survive a round trip through a TOML parser.
17
+ *
18
+ * 2. NO TOML LIBRARY IS USED TO VERIFY WHAT WE WROTE. Measured on Bun 1.3.14,
19
+ * `Bun.TOML.parse` transposes `\t` and `\f`, rejects `\u0007`, and does not
20
+ * trim the newline after an opening `'''`. Codex parses with Rust
21
+ * `toml_edit`, so verifying through a JS parser could report success on a
22
+ * file Codex reads differently. Instead the accepted character set is
23
+ * restricted until escaping is total under three rules, and verification is
24
+ * a byte comparison.
25
+ *
26
+ * CODEX_HOME is resolved at CALL time (the `features.ts:58-67` pattern) so tests
27
+ * can point fixtures via env or an explicit path.
28
+ */
29
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
30
+ import { dirname, join, resolve } from "node:path";
31
+ import { createHash, randomBytes } from "node:crypto";
32
+ import { expandUserPath } from "../config";
33
+ import { CODEX_CONFIG_PATH } from "./paths";
34
+ import { OCX_SECTION_MARKER } from "./injected-marker";
35
+ import {
36
+ durableWrite,
37
+ durableWriteExclusive,
38
+ durableDelete,
39
+ encodeJournal,
40
+ ensureDir,
41
+ hashBytes,
42
+ recoverIfNeeded as recoverJournal,
43
+ type JournalRecord,
44
+ } from "./prompt-journal";
45
+ import { release, stillHeld, tryAcquire } from "./prompt-lock";
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Inventory — ONE definition, consumed by the route and the GUI alike.
49
+ // Classes are the five in devlog 001 §4; the partition is total and disjoint.
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export type LayerClass =
53
+ | "base"
54
+ | "config-toggle"
55
+ | "feature-gated"
56
+ | "runtime-conditional"
57
+ | "extension-unknown";
58
+
59
+ export type ToggleId =
60
+ | "permissions"
61
+ | "collaboration"
62
+ | "environment"
63
+ | "apps"
64
+ | "skills";
65
+
66
+ export interface LayerDescriptor {
67
+ id: string;
68
+ class: LayerClass;
69
+ /** config key for config-toggle and feature-gated rows; null otherwise */
70
+ key: string | null;
71
+ /** documented default when the key is absent */
72
+ default: boolean | null;
73
+ /** assembly index from devlog 001 §1; null when registration-order dependent */
74
+ order: number | null;
75
+ }
76
+
77
+ /**
78
+ * Assembly order per `core/src/session/world_state.rs`. `base-instructions` is
79
+ * NOT a world-state section — it travels in the Responses `instructions` field
80
+ * — so it carries order 0 and sits ahead of the rest.
81
+ *
82
+ * `plugins` is `runtime-conditional`, not feature-gated: `core/src/mcp.rs:200`
83
+ * computes `selected_plugin_available || !capability_summaries().is_empty()`,
84
+ * so `[features] plugins` influences the right operand but does not gate
85
+ * emission.
86
+ */
87
+ export const LAYER_INVENTORY: readonly LayerDescriptor[] = Object.freeze([
88
+ { id: "base-instructions", class: "base", key: null, default: null, order: 0 },
89
+ { id: "model-switch", class: "runtime-conditional", key: null, default: null, order: 1 },
90
+ { id: "personality", class: "feature-gated", key: "features.personality", default: true, order: 2 },
91
+ { id: "context-window-guidance", class: "feature-gated", key: "features.token_budget", default: false, order: 3 },
92
+ { id: "realtime", class: "runtime-conditional", key: null, default: null, order: 4 },
93
+ { id: "agents-md", class: "runtime-conditional", key: null, default: null, order: 5 },
94
+ { id: "permissions", class: "config-toggle", key: "include_permissions_instructions", default: true, order: 6 },
95
+ { id: "collaboration", class: "config-toggle", key: "include_collaboration_mode_instructions", default: true, order: 7 },
96
+ { id: "environment", class: "config-toggle", key: "include_environment_context", default: true, order: 8 },
97
+ { id: "environments-instructions", class: "feature-gated", key: "features.deferred_executor", default: false, order: 9 },
98
+ { id: "apps", class: "config-toggle", key: "include_apps_instructions", default: true, order: 10 },
99
+ { id: "plugins", class: "runtime-conditional", key: null, default: null, order: 11 },
100
+ { id: "tools", class: "feature-gated", key: "features.deferred_tool_world_state", default: false, order: 12 },
101
+ { id: "skills", class: "config-toggle", key: "skills.include_instructions", default: true, order: 13 },
102
+ { id: "multi-agent-mode", class: "feature-gated", key: "features.multi_agent_v2.enabled", default: false, order: 14 },
103
+ ] as const);
104
+
105
+ /**
106
+ * The write allowlist. Fixed, never computed: `config_toml.rs` does NOT carry
107
+ * serde's `deny_unknown_fields`, so a typo'd key is silently ignored in normal
108
+ * mode and a hard startup error under `--strict-config`. A fixed table means
109
+ * the GUI can never emit a key it did not intend.
110
+ */
111
+ const TOGGLE_KEYS: Record<ToggleId, { table: string | null; key: string }> = {
112
+ permissions: { table: null, key: "include_permissions_instructions" },
113
+ collaboration: { table: null, key: "include_collaboration_mode_instructions" },
114
+ environment: { table: null, key: "include_environment_context" },
115
+ apps: { table: null, key: "include_apps_instructions" },
116
+ skills: { table: "skills", key: "include_instructions" },
117
+ };
118
+
119
+ export const TOGGLE_IDS = Object.freeze(Object.keys(TOGGLE_KEYS) as ToggleId[]);
120
+
121
+ export function isToggleId(value: string): value is ToggleId {
122
+ return Object.prototype.hasOwnProperty.call(TOGGLE_KEYS, value);
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Paths
127
+ // ---------------------------------------------------------------------------
128
+
129
+ export interface Paths {
130
+ configPath?: string;
131
+ storePath?: string;
132
+ }
133
+
134
+ function activeCodexHome(): string {
135
+ const raw = process.env.CODEX_HOME?.trim();
136
+ if (!raw) return CODEX_CONFIG_PATH.slice(0, -"/config.toml".length);
137
+ const path = resolve(expandUserPath(raw));
138
+ try {
139
+ return realpathSync.native(path);
140
+ } catch {
141
+ return path;
142
+ }
143
+ }
144
+
145
+ export function activeConfigPath(opts?: Paths): string {
146
+ return opts?.configPath ?? join(activeCodexHome(), "config.toml");
147
+ }
148
+
149
+ export function activeStorePath(opts?: Paths): string {
150
+ return opts?.storePath ?? join(activeCodexHome(), "opencodex-prompt.json");
151
+ }
152
+
153
+ function journalPathFor(storePath: string): string {
154
+ return `${storePath.replace(/\.json$/, "")}.journal`;
155
+ }
156
+
157
+ function lockPathFor(storePath: string): string {
158
+ return `${storePath.replace(/\.json$/, "")}.lock`;
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Character policy — see the header. Defined over Unicode SCALAR VALUES, not
163
+ // UTF-16 code units, because a lone surrogate is not a scalar value and UTF-8
164
+ // encoding would silently substitute U+FFFD.
165
+ // ---------------------------------------------------------------------------
166
+
167
+ export interface CharacterFinding {
168
+ /** code-point index, consistent across module, route and editor */
169
+ position: number;
170
+ reason: "control" | "unpaired-surrogate";
171
+ codePoint: number;
172
+ }
173
+
174
+ /** Tab to four spaces, CRLF and lone CR to LF. Applied BEFORE validation. */
175
+ export function normalizeBody(body: string): string {
176
+ return body.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
177
+ }
178
+
179
+ /** First offending scalar, or null. Run AFTER normalizeBody. */
180
+ export function findInvalidCharacter(body: string): CharacterFinding | null {
181
+ let position = 0;
182
+ for (let i = 0; i < body.length; ) {
183
+ const code = body.codePointAt(i)!;
184
+ const unit = body.charCodeAt(i);
185
+ const isHighSurrogate = unit >= 0xd800 && unit <= 0xdbff;
186
+ const isLowSurrogate = unit >= 0xdc00 && unit <= 0xdfff;
187
+ // codePointAt only combines a well-formed pair, so a surviving surrogate
188
+ // code point here is unpaired by construction.
189
+ if ((isHighSurrogate || isLowSurrogate) && code === unit) {
190
+ return { position, reason: "unpaired-surrogate", codePoint: code };
191
+ }
192
+ const isNewline = code === 0x0a;
193
+ const isC0 = code < 0x20 && !isNewline;
194
+ const isDel = code === 0x7f;
195
+ const isC1 = code >= 0x80 && code <= 0x9f;
196
+ if (isC0 || isDel || isC1) {
197
+ return { position, reason: "control", codePoint: code };
198
+ }
199
+ i += code > 0xffff ? 2 : 1;
200
+ position += 1;
201
+ }
202
+ return null;
203
+ }
204
+
205
+ /**
206
+ * TOML basic-string encoding, total over the accepted set: three rules, none of
207
+ * them in the range where `Bun.TOML.parse` misbehaves. `\r` cannot appear
208
+ * because normalizeBody removed it; control characters cannot appear because
209
+ * findInvalidCharacter rejected them.
210
+ */
211
+ export function encodeBasicString(body: string): string {
212
+ return `"${body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
213
+ }
214
+
215
+ /**
216
+ * Inverse of `encodeBasicString`, deliberately narrow: it accepts ONLY the three
217
+ * escapes we emit. `\t`, `\f`, `\b`, `\r` and `\uXXXX` are refused rather than
218
+ * guessed — decoding them correctly is exactly the ambiguity the restricted set
219
+ * exists to avoid.
220
+ */
221
+ export function decodeBasicString(literal: string): string | null {
222
+ if (literal.length < 2 || !literal.startsWith('"') || !literal.endsWith('"')) return null;
223
+ const inner = literal.slice(1, -1);
224
+ let out = "";
225
+ for (let i = 0; i < inner.length; i += 1) {
226
+ const ch = inner[i]!;
227
+ if (ch !== "\\") {
228
+ if (ch === '"') return null; // unescaped quote: not a single literal
229
+ out += ch;
230
+ continue;
231
+ }
232
+ const next = inner[i + 1];
233
+ if (next === "\\") out += "\\";
234
+ else if (next === '"') out += '"';
235
+ else if (next === "n") out += "\n";
236
+ else return null; // any other escape is outside what we will decode
237
+ i += 1;
238
+ }
239
+ return out;
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Byte-level hashing. The revision covers COMPLETE file bytes plus existence,
244
+ // so removing the marker while leaving the value intact still changes it.
245
+ // ---------------------------------------------------------------------------
246
+
247
+ function readFileOrNull(path: string): string | null {
248
+ try {
249
+ if (!existsSync(path)) return null;
250
+ return readFileSync(path, "utf8");
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ export function computeRevision(configBytes: string | null, storeBytes: string | null): string {
257
+ const hash = createHash("sha256");
258
+ hash.update("cfg:");
259
+ hash.update(configBytes ?? "\0absent");
260
+ hash.update("\nstore:");
261
+ hash.update(storeBytes ?? "\0absent");
262
+ return `sha256:${hash.digest("hex")}`;
263
+ }
264
+
265
+ export { readFileOrNull as readFileBytes };
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Scoped TOML scanning. Line-based like `features.ts:80-93`: booleans need no
269
+ // escaping, and line editing preserves the user's comments and formatting
270
+ // exactly where a re-serialize would not.
271
+ // ---------------------------------------------------------------------------
272
+
273
+ const TABLE_HEADER = /^\s*\[/;
274
+
275
+ /** Lines of the root scope: everything before the first `[table]` header. */
276
+ function rootLines(content: string): string[] {
277
+ const lines = content.split("\n");
278
+ const first = lines.findIndex(l => TABLE_HEADER.test(l));
279
+ return first === -1 ? lines : lines.slice(0, first);
280
+ }
281
+
282
+ /** Lines of `[header]`'s body, up to the next table header. */
283
+ function tableLines(content: string, header: string): string[] | null {
284
+ const lines = content.split("\n");
285
+ const escaped = header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
286
+ const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
287
+ if (start === -1) return null;
288
+ const rest = lines.slice(start + 1);
289
+ const end = rest.findIndex(l => TABLE_HEADER.test(l));
290
+ return end === -1 ? rest : rest.slice(0, end);
291
+ }
292
+
293
+ function boolInLines(lines: string[], key: string): boolean | null {
294
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
295
+ const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*(true|false)\\s*(?:#.*)?$`);
296
+ for (const line of lines) {
297
+ const m = pattern.exec(line);
298
+ if (m) return m[1] === "true";
299
+ }
300
+ return null;
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // Ownership of the generated projection.
305
+ //
306
+ // Canonical physical form, always exactly two lines at the top of the document:
307
+ //
308
+ // # Auto-injected by opencodex
309
+ // developer_instructions = "<single-line basic string>"
310
+ //
311
+ // Replacement is "find the marker, replace the next line" — never a span search.
312
+ // Adjacency mirrors `injected-marker.ts:53-60`, tightened by a shape check.
313
+ // ---------------------------------------------------------------------------
314
+
315
+ const DEV_INSTRUCTIONS_KEY = "developer_instructions";
316
+ const CANONICAL_LINE = /^developer_instructions = "(?:[^"\\]|\\.)*"$/;
317
+ const ANY_DEV_INSTRUCTIONS = /^\s*(?:developer_instructions|"developer_instructions"|'developer_instructions')\s*=/;
318
+
319
+ export type Ownership =
320
+ /** no such key anywhere in the root scope */
321
+ | { state: "absent" }
322
+ /** marker-adjacent and canonically shaped: ours to rewrite */
323
+ | { state: "owned"; line: number; literal: string }
324
+ /** marker-adjacent but reshaped: refuse, offer repair */
325
+ | { state: "owned-malformed"; line: number; raw: string }
326
+ /** no marker: externally authored, refuse and offer adoption */
327
+ | { state: "external"; line: number; raw: string };
328
+
329
+ export function inspectOwnership(configBytes: string | null): Ownership {
330
+ if (configBytes === null) return { state: "absent" };
331
+ const lines = rootLines(configBytes);
332
+ for (let i = 0; i < lines.length; i += 1) {
333
+ const raw = lines[i]!;
334
+ if (!ANY_DEV_INSTRUCTIONS.test(raw)) continue;
335
+ const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
336
+ if (!marked) return { state: "external", line: i + 1, raw };
337
+ if (!CANONICAL_LINE.test(raw)) return { state: "owned-malformed", line: i + 1, raw };
338
+ const literal = raw.slice(`${DEV_INSTRUCTIONS_KEY} = `.length);
339
+ return { state: "owned", line: i + 1, literal };
340
+ }
341
+ return { state: "absent" };
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Store — the single source of truth for custom layers.
346
+ // ---------------------------------------------------------------------------
347
+
348
+ export interface CustomLayer {
349
+ /** [a-z0-9]{6}, stable across edits */
350
+ id: string;
351
+ title: string;
352
+ body: string;
353
+ enabled: boolean;
354
+ }
355
+
356
+ const LAYER_ID = /^[a-z0-9]{6}$/;
357
+
358
+ function isCustomLayer(value: unknown): value is CustomLayer {
359
+ if (!value || typeof value !== "object") return false;
360
+ const v = value as Record<string, unknown>;
361
+ return typeof v.id === "string" && LAYER_ID.test(v.id)
362
+ && typeof v.title === "string"
363
+ && typeof v.body === "string"
364
+ && typeof v.enabled === "boolean";
365
+ }
366
+
367
+ /** null means unreadable/malformed, which is NOT the same as an empty store. */
368
+ export function parseStore(storeBytes: string | null): CustomLayer[] | null {
369
+ if (storeBytes === null) return null;
370
+ let parsed: unknown;
371
+ try {
372
+ parsed = JSON.parse(storeBytes);
373
+ } catch {
374
+ return null;
375
+ }
376
+ if (!parsed || typeof parsed !== "object") return null;
377
+ const layers = (parsed as { layers?: unknown }).layers;
378
+ if (!Array.isArray(layers) || !layers.every(isCustomLayer)) return null;
379
+ const ids = new Set(layers.map(l => l.id));
380
+ if (ids.size !== layers.length) return null;
381
+ return layers;
382
+ }
383
+
384
+ /** Enabled layers, joined in order. This is the value written to config.toml. */
385
+ export function composeProjection(layers: readonly CustomLayer[]): string {
386
+ return layers.filter(l => l.enabled).map(l => l.body).join("\n\n");
387
+ }
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Snapshot
391
+ // ---------------------------------------------------------------------------
392
+
393
+ export interface ToggleState {
394
+ id: ToggleId;
395
+ key: string;
396
+ /** null = the key is absent from the user file */
397
+ userFileValue: boolean | null;
398
+ /**
399
+ * userFileValue ?? default. NOT the resolved Codex value: opencodex reads one
400
+ * of the eight config layers, so it reports this file's value under a name
401
+ * that says as much.
402
+ */
403
+ defaultedUserValue: boolean;
404
+ default: boolean;
405
+ }
406
+
407
+ export type Drift =
408
+ | "journal-present"
409
+ | "projection-stale"
410
+ | "store-missing"
411
+ | "owned-malformed"
412
+ | null;
413
+
414
+ export interface PromptLayerSnapshot {
415
+ configPath: string;
416
+ storePath: string;
417
+ configExists: boolean;
418
+ readable: boolean;
419
+ developerInstructionsOwned: boolean;
420
+ /** non-null blocks mutations until resolved */
421
+ drift: Drift;
422
+ toggles: ToggleState[];
423
+ custom: CustomLayer[];
424
+ modelInstructionsFile: string | null;
425
+ revision: string;
426
+ }
427
+
428
+ function readToggle(configBytes: string | null, id: ToggleId): ToggleState {
429
+ const spec = TOGGLE_KEYS[id];
430
+ const descriptor = LAYER_INVENTORY.find(d => d.id === id)!;
431
+ const fallback = descriptor.default ?? true;
432
+ const key = spec.table ? `${spec.table}.${spec.key}` : spec.key;
433
+ let value: boolean | null = null;
434
+ if (configBytes !== null) {
435
+ const scope = spec.table ? tableLines(configBytes, spec.table) : rootLines(configBytes);
436
+ value = scope === null ? null : boolInLines(scope, spec.key);
437
+ }
438
+ return {
439
+ id,
440
+ key,
441
+ userFileValue: value,
442
+ defaultedUserValue: value ?? fallback,
443
+ default: fallback,
444
+ };
445
+ }
446
+
447
+ function readModelInstructionsFile(configBytes: string | null): string | null {
448
+ if (configBytes === null) return null;
449
+ for (const line of rootLines(configBytes)) {
450
+ const m = /^\s*model_instructions_file\s*=\s*"([^"]*)"\s*(?:#.*)?$/.exec(line);
451
+ if (m) return m[1]!;
452
+ }
453
+ return null;
454
+ }
455
+
456
+ /**
457
+ * Pure. Never writes, never locks, never recovers — a GET must not modify a
458
+ * user's configuration, so drift is REPORTED here and resolved elsewhere.
459
+ */
460
+ export function readPromptLayers(opts?: Paths): PromptLayerSnapshot {
461
+ const configPath = activeConfigPath(opts);
462
+ const storePath = activeStorePath(opts);
463
+ const configExists = existsSync(configPath);
464
+ const configBytes = readFileOrNull(configPath);
465
+ const storeExists = existsSync(storePath);
466
+ const storeBytes = readFileOrNull(storePath);
467
+
468
+ // Present but unreadable is a hard stop; absent is an ordinary first run.
469
+ const readable = !configExists || configBytes !== null;
470
+ const ownership = inspectOwnership(configBytes);
471
+ const layers = parseStore(storeBytes);
472
+ const projection = ownership.state === "owned"
473
+ ? decodeBasicString(ownership.literal)
474
+ : null;
475
+
476
+ let drift: Drift = null;
477
+ if (existsSync(`${storePath.replace(/\.json$/, "")}.journal`)) {
478
+ drift = "journal-present";
479
+ } else if (ownership.state === "owned-malformed") {
480
+ drift = "owned-malformed";
481
+ } else if (!storeExists && projection !== null && projection.length > 0) {
482
+ // The store is gone while a live projection remains. Treating this as an
483
+ // empty store would erase the active prompt on the next write.
484
+ drift = "store-missing";
485
+ } else if (layers !== null && projection !== null && composeProjection(layers) !== projection) {
486
+ drift = "projection-stale";
487
+ }
488
+
489
+ return {
490
+ configPath,
491
+ storePath,
492
+ configExists,
493
+ readable,
494
+ developerInstructionsOwned: ownership.state === "owned",
495
+ drift,
496
+ toggles: TOGGLE_IDS.map(id => readToggle(configBytes, id)),
497
+ custom: layers ?? [],
498
+ modelInstructionsFile: readModelInstructionsFile(configBytes),
499
+ revision: computeRevision(configBytes, storeBytes),
500
+ };
501
+ }
502
+
503
+ // ---------------------------------------------------------------------------
504
+ // Writing
505
+ // ---------------------------------------------------------------------------
506
+
507
+ export type WriteError =
508
+ | "config_unreadable"
509
+ | "stale_revision"
510
+ | "developer_instructions_not_owned"
511
+ | "unknown_layer"
512
+ | "store_unreadable"
513
+ | "invalid_characters"
514
+ | "write_superseded"
515
+ | "recovery_required"
516
+ | "locked";
517
+
518
+ export type WriteResult =
519
+ | { ok: true; changed: boolean; snapshot: PromptLayerSnapshot }
520
+ | { ok: false; error: WriteError; detail?: string };
521
+
522
+ /** Line editing, not re-serialization: the user's comments and layout survive. */
523
+ function dominantEol(content: string): "\r\n" | "\n" {
524
+ const crlf = (content.match(/\r\n/g) ?? []).length;
525
+ if (crlf === 0) return "\n";
526
+ const bareLf = (content.match(/\n/g) ?? []).length - crlf;
527
+ return crlf >= bareLf ? "\r\n" : "\n";
528
+ }
529
+
530
+ function splitLines(content: string): string[] {
531
+ return content.replace(/\r\n/g, "\n").split("\n");
532
+ }
533
+
534
+ function joinLines(lines: string[], eol: "\r\n" | "\n"): string {
535
+ const text = lines.join("\n");
536
+ return eol === "\n" ? text : text.replace(/\n/g, "\r\n");
537
+ }
538
+
539
+ function firstTableIndex(lines: string[]): number {
540
+ const idx = lines.findIndex(l => TABLE_HEADER.test(l));
541
+ return idx === -1 ? lines.length : idx;
542
+ }
543
+
544
+ /** Set a root-scope boolean, inserting above the first table when absent. */
545
+ function setRootBool(content: string, key: string, value: boolean): string {
546
+ const eol = dominantEol(content);
547
+ const lines = splitLines(content);
548
+ const limit = firstTableIndex(lines);
549
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
550
+ const pattern = new RegExp(`^(\\s*${escaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
551
+ for (let i = 0; i < limit; i += 1) {
552
+ const m = pattern.exec(lines[i]!);
553
+ if (m) {
554
+ lines[i] = `${m[1]}${value}${m[2]}`;
555
+ return joinLines(lines, eol);
556
+ }
557
+ }
558
+ lines.splice(limit, 0, `${key} = ${value}`);
559
+ return joinLines(lines, eol);
560
+ }
561
+
562
+ /** Set a boolean inside `[table]`, appending the table when absent. */
563
+ function setTableBool(content: string, table: string, key: string, value: boolean): string {
564
+ const eol = dominantEol(content);
565
+ const lines = splitLines(content);
566
+ const escaped = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
567
+ const start = lines.findIndex(l => new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`).test(l));
568
+ if (start === -1) {
569
+ const tail = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
570
+ lines.splice(tail, 0, `[${table}]`, `${key} = ${value}`);
571
+ return joinLines(lines, eol);
572
+ }
573
+ const keyEscaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
574
+ const pattern = new RegExp(`^(\\s*${keyEscaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
575
+ let end = start + 1;
576
+ while (end < lines.length && !TABLE_HEADER.test(lines[end]!)) end += 1;
577
+ for (let i = start + 1; i < end; i += 1) {
578
+ const m = pattern.exec(lines[i]!);
579
+ if (m) {
580
+ lines[i] = `${m[1]}${value}${m[2]}`;
581
+ return joinLines(lines, eol);
582
+ }
583
+ }
584
+ lines.splice(end, 0, `${key} = ${value}`);
585
+ return joinLines(lines, eol);
586
+ }
587
+
588
+ /**
589
+ * Replace, insert, or remove the generated two-line block. Canonical form is
590
+ * marker + assignment at the top of the document; replacement is "find the
591
+ * marker, replace the next line" rather than a span search.
592
+ */
593
+ function setProjection(content: string | null, projection: string | null): string {
594
+ const base = content ?? "";
595
+ const eol = dominantEol(base);
596
+ const lines = splitLines(base);
597
+ const limit = firstTableIndex(lines);
598
+
599
+ let markerAt = -1;
600
+ for (let i = 0; i < limit; i += 1) {
601
+ if (i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER) && ANY_DEV_INSTRUCTIONS.test(lines[i]!)) {
602
+ markerAt = i - 1;
603
+ break;
604
+ }
605
+ }
606
+
607
+ if (markerAt !== -1) {
608
+ if (projection === null) lines.splice(markerAt, 2);
609
+ else lines[markerAt + 1] = `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`;
610
+ return joinLines(lines, eol);
611
+ }
612
+
613
+ if (projection === null) return joinLines(lines, eol);
614
+ lines.splice(0, 0, OCX_SECTION_MARKER, `${DEV_INSTRUCTIONS_KEY} = ${encodeBasicString(projection)}`);
615
+ return joinLines(lines, eol);
616
+ }
617
+
618
+ function serializeStore(layers: readonly CustomLayer[]): string {
619
+ return `${JSON.stringify({ layers }, null, 2)}\n`;
620
+ }
621
+
622
+ interface Mutation {
623
+ nextConfig: string | null;
624
+ nextStore: string | null;
625
+ }
626
+
627
+ /**
628
+ * The seven-step transaction. Every filesystem mutation in this module goes
629
+ * through here so the journal, the lock, and the per-target byte checks cannot
630
+ * be bypassed by a future caller.
631
+ */
632
+ function commit(
633
+ opts: Paths | undefined,
634
+ revision: string,
635
+ build: (snapshot: PromptLayerSnapshot, configBytes: string | null, storeBytes: string | null)
636
+ => Mutation | { error: WriteError; detail?: string },
637
+ ): WriteResult {
638
+ const configPath = activeConfigPath(opts);
639
+ const storePath = activeStorePath(opts);
640
+ const journalPath = journalPathFor(storePath);
641
+ const lockPath = lockPathFor(storePath);
642
+
643
+ ensureDir(configPath);
644
+ ensureDir(storePath);
645
+
646
+ const acquired = tryAcquire(lockPath);
647
+ if (!acquired.ok) return { ok: false, error: "locked" };
648
+ const handle = acquired.handle;
649
+
650
+ try {
651
+ // 1. recovery first: a journal on disk means an earlier attempt never
652
+ // committed, and we must not stack a second transaction on top of it.
653
+ const recovered = recoverJournal(journalPath, { configPath, storePath });
654
+ if (!recovered.ok) return { ok: false, error: "recovery_required", detail: recovered.detail };
655
+
656
+ // 2. re-read and compare against the caller's edit base.
657
+ const configBytes = readFileOrNull(configPath);
658
+ const storeBytes = readFileOrNull(storePath);
659
+ if (existsSync(configPath) && configBytes === null) {
660
+ return { ok: false, error: "config_unreadable" };
661
+ }
662
+ if (computeRevision(configBytes, storeBytes) !== revision) {
663
+ return { ok: false, error: "stale_revision" };
664
+ }
665
+
666
+ const snapshot = readPromptLayers({ configPath, storePath });
667
+ const built = build(snapshot, configBytes, storeBytes);
668
+ if ("error" in built) return { ok: false, error: built.error, detail: built.detail };
669
+
670
+ const { nextConfig, nextStore } = built;
671
+ const configChanged = nextConfig !== configBytes;
672
+ const storeChanged = nextStore !== storeBytes;
673
+ if (!configChanged && !storeChanged) {
674
+ return { ok: true, changed: false, snapshot };
675
+ }
676
+
677
+ // 3. journal the intent. Its existence is NOT a commit.
678
+ const record: JournalRecord = {
679
+ configPath,
680
+ storePath,
681
+ preConfig: hashBytes(configBytes),
682
+ postConfig: hashBytes(nextConfig),
683
+ preStore: hashBytes(storeBytes),
684
+ postStore: hashBytes(nextStore),
685
+ preConfigBytes: configBytes,
686
+ postConfigBytes: nextConfig,
687
+ preStoreBytes: storeBytes,
688
+ postStoreBytes: nextStore,
689
+ };
690
+ durableWrite(journalPath, encodeJournal(record));
691
+
692
+ // 4/5. each target re-verifies ITS OWN bytes immediately before its rename,
693
+ // so a third party writing between step 2 and here is not overwritten.
694
+ if (configChanged) {
695
+ if (hashBytes(readFileOrNull(configPath)) !== record.preConfig) {
696
+ return rollback(record, journalPath, "stale_revision");
697
+ }
698
+ if (nextConfig === null) durableDelete(configPath);
699
+ else durableWrite(configPath, nextConfig);
700
+ }
701
+ if (storeChanged) {
702
+ if (hashBytes(readFileOrNull(storePath)) !== record.preStore) {
703
+ return rollback(record, journalPath, "stale_revision");
704
+ }
705
+ if (nextStore === null) durableDelete(storePath);
706
+ else durableWrite(storePath, nextStore);
707
+ }
708
+
709
+ // 6. verify COMPLETE bytes, not just our two lines: another writer could
710
+ // change an unrelated key and a narrow check would report success.
711
+ const finalConfig = hashBytes(readFileOrNull(configPath));
712
+ const finalStore = hashBytes(readFileOrNull(storePath));
713
+ if (finalConfig !== record.postConfig || finalStore !== record.postStore) {
714
+ return { ok: false, error: "write_superseded" };
715
+ }
716
+ if (!stillHeld(handle)) return { ok: false, error: "write_superseded" };
717
+
718
+ durableDelete(journalPath); // this deletion is the commit
719
+ return { ok: true, changed: true, snapshot: readPromptLayers({ configPath, storePath }) };
720
+ } finally {
721
+ release(handle);
722
+ }
723
+ }
724
+
725
+ /** Undo whatever landed, then drop the journal. Never touches an unknown file. */
726
+ function rollback(record: JournalRecord, journalPath: string, error: WriteError): WriteResult {
727
+ const configNow = hashBytes(readFileOrNull(record.configPath));
728
+ const storeNow = hashBytes(readFileOrNull(record.storePath));
729
+ if (configNow !== record.preConfig && configNow !== record.postConfig) {
730
+ return { ok: false, error: "recovery_required", detail: record.configPath };
731
+ }
732
+ if (storeNow !== record.preStore && storeNow !== record.postStore) {
733
+ return { ok: false, error: "recovery_required", detail: record.storePath };
734
+ }
735
+ if (configNow === record.postConfig) {
736
+ if (record.preConfigBytes === null) durableDelete(record.configPath);
737
+ else durableWrite(record.configPath, record.preConfigBytes);
738
+ }
739
+ if (storeNow === record.postStore) {
740
+ if (record.preStoreBytes === null) durableDelete(record.storePath);
741
+ else durableWrite(record.storePath, record.preStoreBytes);
742
+ }
743
+ durableDelete(journalPath);
744
+ return { ok: false, error };
745
+ }
746
+
747
+ /** Flip one of the five prompt toggles. */
748
+ export function setToggle(id: string, enabled: boolean, revision: string, opts?: Paths): WriteResult {
749
+ if (!isToggleId(id)) return { ok: false, error: "unknown_layer" };
750
+ const spec = TOGGLE_KEYS[id];
751
+ return commit(opts, revision, (_snapshot, configBytes, storeBytes) => ({
752
+ nextConfig: spec.table
753
+ ? setTableBool(configBytes ?? "", spec.table, spec.key, enabled)
754
+ : setRootBool(configBytes ?? "", spec.key, enabled),
755
+ nextStore: storeBytes,
756
+ }));
757
+ }
758
+
759
+ /** Replace the whole custom-layer list; order is composition order. */
760
+ export function writeCustomLayers(layers: readonly CustomLayer[], revision: string, opts?: Paths): WriteResult {
761
+ for (const layer of layers) {
762
+ const normalized = normalizeBody(layer.body);
763
+ const invalid = findInvalidCharacter(normalized);
764
+ if (invalid !== null) {
765
+ return { ok: false, error: "invalid_characters", detail: `layer ${layer.id} at code point ${invalid.position}` };
766
+ }
767
+ }
768
+ const normalizedLayers = layers.map(l => ({ ...l, body: normalizeBody(l.body) }));
769
+ return commit(opts, revision, (snapshot, configBytes, _storeBytes) => {
770
+ // Only a marker-owned key may be rewritten. Absent is fine — we create it.
771
+ const ownership = inspectOwnership(configBytes);
772
+ if (ownership.state === "external" || ownership.state === "owned-malformed") {
773
+ return { error: "developer_instructions_not_owned" };
774
+ }
775
+ const projection = composeProjection(normalizedLayers);
776
+ return {
777
+ nextConfig: setProjection(configBytes, projection.length > 0 ? projection : null),
778
+ nextStore: serializeStore(normalizedLayers),
779
+ };
780
+ });
781
+ }
782
+
783
+ // ---------------------------------------------------------------------------
784
+ // Adoption — taking ownership of an externally authored key.
785
+ //
786
+ // Refusing alone was a dead end: the earlier answer was "delete your existing
787
+ // instructions by hand", which is not a feature. Adoption previews the raw line
788
+ // AND the exact body that will be committed, then imports on confirmation.
789
+ // ---------------------------------------------------------------------------
790
+
791
+ export interface AdoptPreview {
792
+ rawLine: string | null;
793
+ /** post-normalization body — byte-identical to what a confirm would store */
794
+ decodedBody: string | null;
795
+ reason: "ok" | "nothing_to_adopt" | "unsupported_form" | "invalid_characters";
796
+ path: string;
797
+ line: number | null;
798
+ detail?: string;
799
+ }
800
+
801
+ function newLayerId(existing: readonly CustomLayer[]): string {
802
+ const taken = new Set(existing.map(l => l.id));
803
+ for (;;) {
804
+ const id = randomBytes(4).toString("hex").slice(0, 6);
805
+ if (!taken.has(id)) return id;
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Read-only. Runs the same five steps a confirm would, so preview and commit
811
+ * cannot disagree: decode -> normalize -> validate -> cap -> present.
812
+ */
813
+ export function previewAdopt(opts?: Paths): AdoptPreview {
814
+ const configPath = activeConfigPath(opts);
815
+ const ownership = inspectOwnership(readFileOrNull(configPath));
816
+
817
+ if (ownership.state === "absent" || ownership.state === "owned") {
818
+ return { rawLine: null, decodedBody: null, reason: "nothing_to_adopt", path: configPath, line: null };
819
+ }
820
+
821
+ const raw = ownership.raw;
822
+ const line = ownership.line;
823
+ const eq = raw.indexOf("=");
824
+ const literal = eq === -1 ? "" : raw.slice(eq + 1).trim().replace(/\s*#.*$/, "");
825
+ const decoded = decodeBasicString(literal);
826
+ if (decoded === null) {
827
+ return {
828
+ rawLine: raw,
829
+ decodedBody: null,
830
+ reason: "unsupported_form",
831
+ path: configPath,
832
+ line,
833
+ detail: "only a single-line basic string using \\\", \\\\ and \\n can be decoded safely",
834
+ };
835
+ }
836
+
837
+ const normalized = normalizeBody(decoded);
838
+ const invalid = findInvalidCharacter(normalized);
839
+ if (invalid !== null) {
840
+ return {
841
+ rawLine: raw,
842
+ decodedBody: null,
843
+ reason: "invalid_characters",
844
+ path: configPath,
845
+ line,
846
+ detail: `code point ${invalid.position} is a ${invalid.reason}`,
847
+ };
848
+ }
849
+
850
+ return { rawLine: raw, decodedBody: normalized, reason: "ok", path: configPath, line };
851
+ }
852
+
853
+ /** Import the external value as one custom layer and take ownership of the key. */
854
+ export function adoptDeveloperInstructions(revision: string, opts?: Paths): WriteResult {
855
+ const preview = previewAdopt(opts);
856
+ if (preview.reason !== "ok" || preview.decodedBody === null) {
857
+ return {
858
+ ok: false,
859
+ error: preview.reason === "invalid_characters" ? "invalid_characters" : "developer_instructions_not_owned",
860
+ detail: preview.detail,
861
+ };
862
+ }
863
+ const body = preview.decodedBody;
864
+ return commit(opts, revision, (snapshot, configBytes) => {
865
+ const existing = snapshot.custom;
866
+ const adopted: CustomLayer = {
867
+ id: newLayerId(existing),
868
+ title: "Imported from config.toml",
869
+ body,
870
+ enabled: true,
871
+ };
872
+ const layers = [adopted, ...existing];
873
+ // Drop the unowned line first, then write the canonical owned block.
874
+ const stripped = removeUnownedProjection(configBytes ?? "");
875
+ return {
876
+ nextConfig: setProjection(stripped, composeProjection(layers)),
877
+ nextStore: serializeStore(layers),
878
+ };
879
+ });
880
+ }
881
+
882
+ /** Remove an unowned or reshaped `developer_instructions` from the root scope. */
883
+ function removeUnownedProjection(content: string): string {
884
+ const eol = dominantEol(content);
885
+ const lines = splitLines(content);
886
+ const limit = firstTableIndex(lines);
887
+ for (let i = 0; i < limit; i += 1) {
888
+ if (!ANY_DEV_INSTRUCTIONS.test(lines[i]!)) continue;
889
+ const marked = i > 0 && lines[i - 1]!.includes(OCX_SECTION_MARKER);
890
+ lines.splice(marked ? i - 1 : i, marked ? 2 : 1);
891
+ return joinLines(lines, eol);
892
+ }
893
+ return joinLines(lines, eol);
894
+ }
895
+
896
+ // ---------------------------------------------------------------------------
897
+ // Salvage — the store is gone while a live projection remains.
898
+ //
899
+ // This is salvage, NOT reconstruction. The projection is one concatenated
900
+ // string: layer boundaries, ids, titles, order, disabled layers, and whether a
901
+ // blank line separated two layers or was the user's own text are all gone.
902
+ // ---------------------------------------------------------------------------
903
+
904
+ export interface SalvagePreview {
905
+ body: string | null;
906
+ /** the DIRECTORY backups land in. A read-only preview reserves no filename. */
907
+ backupDir: string;
908
+ unrecoverable: readonly string[];
909
+ reason: "ok" | "nothing_to_salvage";
910
+ }
911
+
912
+ const UNRECOVERABLE = Object.freeze([
913
+ "layer boundaries",
914
+ "layer ids",
915
+ "layer titles",
916
+ "row order",
917
+ "disabled layers and their bodies",
918
+ "whether a blank line separated two layers or was your own text",
919
+ ]);
920
+
921
+ export function previewSalvage(opts?: Paths): SalvagePreview {
922
+ const configPath = activeConfigPath(opts);
923
+ const storePath = activeStorePath(opts);
924
+ const ownership = inspectOwnership(readFileOrNull(configPath));
925
+ const body = ownership.state === "owned" ? decodeBasicString(ownership.literal) : null;
926
+ return {
927
+ body,
928
+ // `dirname`, not a hand-rolled `lastIndexOf("/")`: a Windows store path is
929
+ // `D:\...\store.toml`, which contains no forward slash at all, so the
930
+ // slice returned `"."` and the preview named the wrong directory.
931
+ backupDir: dirname(storePath),
932
+ unrecoverable: UNRECOVERABLE,
933
+ reason: body !== null && body.length > 0 ? "ok" : "nothing_to_salvage",
934
+ };
935
+ }
936
+
937
+ /**
938
+ * Adopt the live projection as ONE layer. A durable backup is written first and
939
+ * salvage aborts if it cannot be created — a destructive operation whose safety
940
+ * net failed should not proceed.
941
+ */
942
+ export function salvageProjection(revision: string, opts?: Paths): WriteResult {
943
+ const preview = previewSalvage(opts);
944
+ if (preview.reason !== "ok" || preview.body === null) {
945
+ return { ok: false, error: "developer_instructions_not_owned", detail: "no live projection to salvage" };
946
+ }
947
+ const body = preview.body;
948
+ const storePath = activeStorePath(opts);
949
+ const backupPath = `${storePath.replace(/\.json$/, "")}.salvage-${Date.now()}-${randomBytes(3).toString("hex")}.txt`;
950
+ try {
951
+ durableWriteExclusive(backupPath, body);
952
+ } catch {
953
+ return { ok: false, error: "recovery_required", detail: `could not write a durable backup at ${backupPath}` };
954
+ }
955
+ return commit(opts, revision, (_snapshot, configBytes) => {
956
+ const salvaged: CustomLayer = {
957
+ id: newLayerId([]),
958
+ title: "Salvaged from config.toml",
959
+ body,
960
+ enabled: true,
961
+ };
962
+ return {
963
+ nextConfig: setProjection(configBytes, body),
964
+ nextStore: serializeStore([salvaged]),
965
+ };
966
+ });
967
+ }