okengine 0.21.1 → 0.23.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 (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -0,0 +1,174 @@
1
+ /**
2
+ * `oke decide promote` — fetch the operator candidate and write the lockfile.
3
+ * The command does not recompute a certificate.
4
+ */
5
+
6
+ import { join, resolve } from "node:path";
7
+ import {
8
+ DECISION_LOCK_FILENAME,
9
+ lockFromCandidate,
10
+ parseDecisionLockfile,
11
+ type DecisionLockfile,
12
+ } from "../elements/ai/decisions/certificate.ts";
13
+ import { DECISION_EXPORT_WARNING } from "../elements/ai/decisions/export.ts";
14
+
15
+ /** Options for {@link promoteDecision}. */
16
+ export interface PromoteDecisionOptions {
17
+ readonly name: string;
18
+ /** Origin of the running app, including scheme and port. */
19
+ readonly origin: string;
20
+ /** Lockfile path. Defaults to `oke-decisions.lock.json` in the app root. */
21
+ readonly lockPath?: string;
22
+ /** Operator credential header value (`Authorization`). */
23
+ readonly authorization?: string;
24
+ /** Injected fetch. Defaults to the global fetch. */
25
+ readonly fetcher?: (input: string, init?: RequestInit) => Promise<Response>;
26
+ /** Existing lockfile body. When omitted, the file is read if it exists. */
27
+ readonly current?: DecisionLockfile;
28
+ }
29
+
30
+ /**
31
+ * Fetch one candidate and write it into the app lockfile.
32
+ *
33
+ * @param options - Decision name, origin, and credentials
34
+ */
35
+ export async function promoteDecision(options: PromoteDecisionOptions): Promise<DecisionLockfile> {
36
+ const fetcher = options.fetcher ?? fetch;
37
+ const url = `${options.origin.replace(/\/$/, "")}/_oke/decisions/${encodeURIComponent(options.name)}/candidate`;
38
+ const headers = new Headers();
39
+ if (options.authorization) headers.set("authorization", options.authorization);
40
+ const res = await fetcher(url, { headers });
41
+ if (!res.ok) {
42
+ throw new Error(`oke decide promote: ${res.status} from ${url}`);
43
+ }
44
+ const candidate: unknown = await res.json();
45
+ const root = resolve(process.env["OKE_ROOT_DIR"] ?? ".");
46
+ const path = resolve(options.lockPath ?? join(root, DECISION_LOCK_FILENAME));
47
+ let current = options.current;
48
+ if (!current) {
49
+ const file = Bun.file(path);
50
+ if (await file.exists()) current = parseDecisionLockfile(await file.json());
51
+ }
52
+ const next = lockFromCandidate(options.name, candidate, current);
53
+ const { persistDecisionDrift } = await import("../elements/ai/decisions/labels.ts");
54
+ persistDecisionDrift(options.name, false);
55
+ await Bun.write(path, `${JSON.stringify(next, null, 2)}\n`);
56
+ return next;
57
+ }
58
+
59
+ /** Options for {@link exportDecisionLabelsFile}. */
60
+ export interface ExportDecisionLabelsOptions {
61
+ readonly name: string;
62
+ /** Origin of the running app, including scheme and port. */
63
+ readonly origin: string;
64
+ /** File the JSONL is written to. */
65
+ readonly out: string;
66
+ /** Operator credential header value (`Authorization`). */
67
+ readonly authorization?: string;
68
+ /** Tenant the operator is exporting. A mismatch is rejected by the app. */
69
+ readonly tenant?: string;
70
+ /** Injected fetch. Defaults to the global fetch. */
71
+ readonly fetcher?: (input: string, init?: RequestInit) => Promise<Response>;
72
+ /** Where the production-data warning is printed. Defaults to stdout. */
73
+ readonly write?: (text: string) => void;
74
+ }
75
+
76
+ /**
77
+ * Fetch reviewed labels and write seed JSONL. Prints that the file holds production data.
78
+ *
79
+ * @param options - Decision name, origin, and output path
80
+ */
81
+ export async function exportDecisionLabelsFile(
82
+ options: ExportDecisionLabelsOptions,
83
+ ): Promise<void> {
84
+ const fetcher = options.fetcher ?? fetch;
85
+ const url = new URL(
86
+ `/_oke/decisions/${encodeURIComponent(options.name)}/labels`,
87
+ options.origin.endsWith("/") ? options.origin : `${options.origin}/`,
88
+ );
89
+ if (options.tenant) url.searchParams.set("tenant", options.tenant);
90
+ const headers = new Headers();
91
+ if (options.authorization) headers.set("authorization", options.authorization);
92
+ const res = await fetcher(url.toString(), { headers });
93
+ if (!res.ok) {
94
+ throw new Error(`oke decide labels: ${res.status} from ${url}`);
95
+ }
96
+ const body = (await res.json()) as { lines?: string; data?: { lines?: string } };
97
+ const lines = body.data?.lines ?? body.lines ?? "";
98
+ const write = options.write ?? ((text: string) => process.stdout.write(text));
99
+ write(`${DECISION_EXPORT_WARNING}\n`);
100
+ await Bun.write(options.out, lines);
101
+ }
102
+
103
+ /**
104
+ * `oke decide labels <name> --export`.
105
+ *
106
+ * @param name - Decision name
107
+ * @param rest - Flags after the name
108
+ */
109
+ async function exportLabelsCommand(name: string | undefined, rest: string[]): Promise<number> {
110
+ if (!name || !rest.includes("--export")) {
111
+ console.error("oke decide: expected `oke decide labels <name> --export`");
112
+ return 1;
113
+ }
114
+ let origin = process.env.OKE_ORIGIN ?? "http://127.0.0.1:6530";
115
+ let out = `${name}.labels.jsonl`;
116
+ let authorization = process.env.OKE_OPERATOR_AUTHORIZATION;
117
+ let tenant: string | undefined;
118
+ for (let i = 0; i < rest.length; i++) {
119
+ const arg = rest[i];
120
+ if (arg === "--origin") origin = rest[++i] ?? origin;
121
+ else if (arg === "--out") out = rest[++i] ?? out;
122
+ else if (arg === "--authorization") authorization = rest[++i];
123
+ else if (arg === "--tenant") tenant = rest[++i];
124
+ }
125
+ await exportDecisionLabelsFile({
126
+ name,
127
+ origin,
128
+ out,
129
+ ...(authorization !== undefined ? { authorization } : {}),
130
+ ...(tenant !== undefined ? { tenant } : {}),
131
+ });
132
+ return 0;
133
+ }
134
+
135
+ /**
136
+ * CLI entry for `oke decide`.
137
+ *
138
+ * @param args - Remaining argv after `decide`
139
+ */
140
+ export async function decideCli(args: string[]): Promise<number> {
141
+ const [sub, name, ...rest] = args;
142
+ if (sub === "--help" || sub === "-h" || !sub) {
143
+ console.log(`oke decide promote <name> [--origin URL] [--lock path]
144
+ oke decide labels <name> --export [--out file] [--origin URL]
145
+
146
+ promote fetches the operator candidate and writes oke-decisions.lock.json.
147
+ labels --export writes reviewed labels as seed JSONL. The file contains production data.
148
+ `);
149
+ return sub ? 0 : 1;
150
+ }
151
+ if (sub === "labels") return exportLabelsCommand(name, rest);
152
+ if (sub !== "promote" || !name) {
153
+ console.error(
154
+ "oke decide: expected `oke decide promote <name>` or `oke decide labels <name> --export`",
155
+ );
156
+ return 1;
157
+ }
158
+ let origin = process.env.OKE_ORIGIN ?? "http://127.0.0.1:6530";
159
+ let lockPath: string | undefined;
160
+ let authorization = process.env.OKE_OPERATOR_AUTHORIZATION;
161
+ for (let i = 0; i < rest.length; i++) {
162
+ const arg = rest[i];
163
+ if (arg === "--origin") origin = rest[++i] ?? origin;
164
+ else if (arg === "--lock") lockPath = rest[++i];
165
+ else if (arg === "--authorization") authorization = rest[++i];
166
+ }
167
+ await promoteDecision({
168
+ name,
169
+ origin,
170
+ ...(lockPath !== undefined ? { lockPath } : {}),
171
+ ...(authorization !== undefined ? { authorization } : {}),
172
+ });
173
+ return 0;
174
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `oke dev` reloads the decision lockfile without a restart.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { mkdtemp } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import {
10
+ DECISION_LOCK_FILENAME,
11
+ getDecisionLock,
12
+ resetDecisionCertificates,
13
+ } from "../elements/ai/decisions/certificate.ts";
14
+ import { watchDecisionLockfile } from "./decision-lock-watch.ts";
15
+
16
+ describe("decision lockfile watch", () => {
17
+ test("a change calls setDecisionLock without restarting", async () => {
18
+ resetDecisionCertificates();
19
+ const root = await mkdtemp(join(tmpdir(), "oke-lock-"));
20
+ const path = join(root, DECISION_LOCK_FILENAME);
21
+ await Bun.write(
22
+ path,
23
+ `${JSON.stringify({ decisions: { triage: { model: "m", questions: {} } } })}\n`,
24
+ );
25
+ let listener: ((event: "rename" | "change", filename: string | null) => void) | undefined;
26
+ const watch = watchDecisionLockfile(root, (_path, _options, next) => {
27
+ listener = next;
28
+ return { close() {} };
29
+ });
30
+ listener?.("change", DECISION_LOCK_FILENAME);
31
+ await watch.flushed();
32
+ expect(getDecisionLock()?.decisions.triage?.model).toBe("m");
33
+
34
+ await Bun.write(
35
+ path,
36
+ `${JSON.stringify({ decisions: { route: { model: "n", questions: {} } } })}\n`,
37
+ );
38
+ listener?.("change", DECISION_LOCK_FILENAME);
39
+ await watch.flushed();
40
+ expect(getDecisionLock()?.decisions.route?.model).toBe("n");
41
+ expect(getDecisionLock()?.decisions.triage).toBeUndefined();
42
+ watch.close();
43
+ resetDecisionCertificates();
44
+ });
45
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `oke dev` reloads `oke-decisions.lock.json` without restarting.
3
+ * Production reads the file once, at boot.
4
+ */
5
+
6
+ import { watch } from "node:fs";
7
+ import {
8
+ DECISION_LOCK_FILENAME,
9
+ loadDecisionLockfile,
10
+ } from "../elements/ai/decisions/certificate.ts";
11
+
12
+ /** Handle for the lockfile watcher. */
13
+ export interface DecisionLockWatch {
14
+ close(): void;
15
+ /** Resolves after the latest reload this watcher started. */
16
+ flushed(): Promise<void>;
17
+ }
18
+
19
+ /**
20
+ * Install `setDecisionLock` whenever the lockfile changes.
21
+ *
22
+ * @param root - App root
23
+ * @param watchFs - Injected watch. The dev server uses `node:fs` on the root.
24
+ */
25
+ export function watchDecisionLockfile(
26
+ root: string,
27
+ watchFs?: (
28
+ path: string,
29
+ options: { readonly recursive: true },
30
+ listener: (event: "rename" | "change", filename: string | null) => void,
31
+ ) => { close(): void },
32
+ ): DecisionLockWatch {
33
+ let pending: Promise<void> = Promise.resolve();
34
+ const reload = (): void => {
35
+ pending = loadDecisionLockfile(root).then(
36
+ () => undefined,
37
+ () => undefined,
38
+ );
39
+ };
40
+ if (watchFs) {
41
+ const handle = watchFs(`${root}/${DECISION_LOCK_FILENAME}`, { recursive: true }, () => {
42
+ reload();
43
+ });
44
+ return { close: () => handle.close(), flushed: () => pending };
45
+ }
46
+ const handle = watch(root, (_event, filename) => {
47
+ if (filename?.toString() !== DECISION_LOCK_FILENAME) return;
48
+ reload();
49
+ });
50
+ return { close: () => handle.close(), flushed: () => pending };
51
+ }
@@ -21,6 +21,8 @@
21
21
 
22
22
  import { resolve } from "node:path";
23
23
  import { pathToFileURL } from "node:url";
24
+ import { closeSharedPostgresClients } from "../drivers/postgres.ts";
25
+ import { installDevHotGeneration, takeDevHotGeneration } from "./dev-hot.ts";
24
26
  import { installGracefulShutdown, type GracefulShutdownApp } from "../kernel/graceful-shutdown.ts";
25
27
  import type { Manifest } from "../manifest/types.ts";
26
28
  import { createBunRuntime } from "../runtime/bun.ts";
@@ -67,22 +69,36 @@ if (manifestPath !== undefined && manifestPath.length > 0) {
67
69
  }
68
70
  }
69
71
 
72
+ // Soft reload re-enters this file in the same process. Stop the previous
73
+ // boot's 1s scheduler and close its Bun.SQL pools before opening new ones.
74
+ // Holder `close()` is a no-op on the shared pool, so this has to be explicit.
75
+ const previous = takeDevHotGeneration();
76
+ if (previous) {
77
+ try {
78
+ await previous.dispose();
79
+ } catch (err) {
80
+ console.error(
81
+ `oke dev: previous hot generation failed to dispose — ${
82
+ err instanceof Error ? err.message : String(err)
83
+ }`,
84
+ );
85
+ }
86
+ }
87
+ await closeSharedPostgresClients();
88
+
70
89
  const absoluteEntry = resolve(entry);
71
90
  const mod = (await import(pathToFileURL(absoluteEntry).href)) as {
72
91
  app?: BootableApp;
73
92
  };
93
+ const app = mod.app;
74
94
 
75
- if (
76
- mod.app === undefined ||
77
- typeof mod.app.boot !== "function" ||
78
- typeof mod.app.fetch !== "function"
79
- ) {
95
+ if (app === undefined || typeof app.boot !== "function" || typeof app.fetch !== "function") {
80
96
  console.error(`oke dev: ${absoluteEntry} must export app with boot() and fetch()`);
81
97
  process.exit(1);
82
98
  }
83
99
 
84
100
  try {
85
- await mod.app.boot({
101
+ await app.boot({
86
102
  rootDir: process.env["OKE_ROOT_DIR"],
87
103
  ...(manifest !== undefined ? { manifest } : {}),
88
104
  });
@@ -94,12 +110,22 @@ try {
94
110
  console.error(formatFatalError(err));
95
111
  process.exit(1);
96
112
  }
97
- const handle = createBunRuntime().serve(mod.app, {
113
+ const handle = createBunRuntime().serve(app, {
98
114
  port,
99
115
  hostname,
100
116
  id: DEV_APP_SERVE_ID,
101
117
  });
102
- installGracefulShutdown({ app: mod.app, handle });
118
+ const { watchDecisionLockfile } = await import("./decision-lock-watch.ts");
119
+ const decisionLockWatch = watchDecisionLockfile(process.env.OKE_ROOT_DIR ?? process.cwd());
120
+ const removeSignals = installGracefulShutdown({ app, handle });
121
+ installDevHotGeneration({
122
+ async dispose() {
123
+ decisionLockWatch.close();
124
+ removeSignals();
125
+ await app.stop();
126
+ await closeSharedPostgresClients();
127
+ },
128
+ });
103
129
 
104
130
  const appUrl = `http://${formatHostForUrl(hostname)}:${handle.port}`;
105
131
  // Parent board (claim + Docker) stays. Bun `--no-clear-screen` + no child wipe.
@@ -0,0 +1,49 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { ensureDevAuthSecret } from "./dev-auth-secret.ts";
6
+
7
+ describe("ensureDevAuthSecret", () => {
8
+ const dirs: string[] = [];
9
+ const prev = process.env.OKE_AUTH_SECRET;
10
+
11
+ afterEach(async () => {
12
+ if (prev === undefined) delete process.env.OKE_AUTH_SECRET;
13
+ else process.env.OKE_AUTH_SECRET = prev;
14
+ await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
15
+ });
16
+
17
+ test("reuses process.env and does not write a file", async () => {
18
+ process.env.OKE_AUTH_SECRET = "from-env";
19
+ const dir = await mkdtemp(join(tmpdir(), "oke-auth-secret-"));
20
+ dirs.push(dir);
21
+ expect(await ensureDevAuthSecret(dir)).toBe("from-env");
22
+ expect(await Bun.file(join(dir, ".env.local")).exists()).toBe(false);
23
+ });
24
+
25
+ test("reads .env.local before minting", async () => {
26
+ delete process.env.OKE_AUTH_SECRET;
27
+ const dir = await mkdtemp(join(tmpdir(), "oke-auth-secret-"));
28
+ dirs.push(dir);
29
+ await Bun.write(
30
+ join(dir, ".env.local"),
31
+ "DATABASE_URL=postgres://localhost/oke\nOKE_AUTH_SECRET=from-file\n",
32
+ );
33
+ expect(await ensureDevAuthSecret(dir)).toBe("from-file");
34
+ });
35
+
36
+ test("mints once and appends to .env.local", async () => {
37
+ delete process.env.OKE_AUTH_SECRET;
38
+ const dir = await mkdtemp(join(tmpdir(), "oke-auth-secret-"));
39
+ dirs.push(dir);
40
+ await Bun.write(join(dir, ".env.local"), "DATABASE_URL=postgres://localhost/oke\n");
41
+ const first = await ensureDevAuthSecret(dir);
42
+ expect(first.startsWith("oke_dev_")).toBe(true);
43
+ const text = await readFile(join(dir, ".env.local"), "utf8");
44
+ expect(text).toContain(`OKE_AUTH_SECRET=${first}`);
45
+ expect(text.startsWith("DATABASE_URL=")).toBe(true);
46
+ delete process.env.OKE_AUTH_SECRET;
47
+ expect(await ensureDevAuthSecret(dir)).toBe(first);
48
+ });
49
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * One HMAC secret for `oke dev`.
3
+ *
4
+ * Console boots the app in the parent; the HTTP server is a child. Each
5
+ * process would otherwise mint its own `gate.auth` secret, and a Bearer API
6
+ * key hashed in Console would not verify on the app. This stores one
7
+ * `OKE_AUTH_SECRET` in `.env.local` and returns it for both.
8
+ */
9
+
10
+ import { join } from "node:path";
11
+ import { mintDevAuthSecret } from "../auth/config.ts";
12
+ import { escapeDotenvValue, parseDotenv } from "../drivers/vault-dotenv-parse.ts";
13
+
14
+ /**
15
+ * Resolve the shared dev auth secret.
16
+ *
17
+ * Order: `process.env.OKE_AUTH_SECRET`, then `.env.local`, then a minted
18
+ * value appended to `.env.local`. Does not write `process.env`.
19
+ *
20
+ * @param cwd - Project root
21
+ */
22
+ export async function ensureDevAuthSecret(cwd: string): Promise<string> {
23
+ const current = process.env.OKE_AUTH_SECRET?.trim();
24
+ if (current) return current;
25
+
26
+ const envPath = join(cwd, ".env.local");
27
+ let text = "";
28
+ const file = Bun.file(envPath);
29
+ if (await file.exists()) {
30
+ try {
31
+ text = await file.text();
32
+ } catch {
33
+ text = "";
34
+ }
35
+ }
36
+ const fromFile = parseDotenv(text).get("OKE_AUTH_SECRET")?.trim();
37
+ if (fromFile) return fromFile;
38
+
39
+ const secret = mintDevAuthSecret();
40
+ const line = `OKE_AUTH_SECRET=${escapeDotenvValue(secret)}`;
41
+ const next =
42
+ text.length === 0
43
+ ? `${line}\n`
44
+ : text.endsWith("\n")
45
+ ? `${text}${line}\n`
46
+ : `${text}\n${line}\n`;
47
+ await Bun.write(envPath, next);
48
+ return secret;
49
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Soft-reload slot: one live generation, disposed before the next boot.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { installDevHotGeneration, takeDevHotGeneration } from "./dev-hot.ts";
7
+
8
+ describe("dev hot generation", () => {
9
+ test("take returns the installed generation once", async () => {
10
+ let disposed = 0;
11
+ installDevHotGeneration({
12
+ async dispose() {
13
+ disposed += 1;
14
+ },
15
+ });
16
+ const first = takeDevHotGeneration();
17
+ expect(first).toBeDefined();
18
+ await first!.dispose();
19
+ expect(disposed).toBe(1);
20
+ expect(takeDevHotGeneration()).toBeUndefined();
21
+ });
22
+
23
+ test("install replaces the previous slot without disposing it", () => {
24
+ installDevHotGeneration({
25
+ async dispose() {
26
+ throw new Error("stale");
27
+ },
28
+ });
29
+ let disposed = 0;
30
+ installDevHotGeneration({
31
+ async dispose() {
32
+ disposed += 1;
33
+ },
34
+ });
35
+ const current = takeDevHotGeneration();
36
+ expect(current).toBeDefined();
37
+ return current!.dispose().then(() => {
38
+ expect(disposed).toBe(1);
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * One live `bun --hot` generation for `oke dev`.
3
+ *
4
+ * Soft reload re-runs the app runner in the same process. The previous
5
+ * boot's scheduler and Postgres pools stay open unless this slot disposes
6
+ * them before the next `boot()`.
7
+ */
8
+
9
+ const DEV_HOT_SLOT = Symbol.for("oke.dev.hot.generation");
10
+
11
+ /** Work the previous runner evaluation must undo before the next boot. */
12
+ export type DevHotGeneration = {
13
+ /** Stop schedulers, drop signal handlers, and close this generation's pools. */
14
+ dispose(): Promise<void>;
15
+ };
16
+
17
+ type HotBox = { current?: DevHotGeneration };
18
+
19
+ function hotBox(): HotBox {
20
+ const g = globalThis as typeof globalThis & { [DEV_HOT_SLOT]?: HotBox };
21
+ let box = g[DEV_HOT_SLOT];
22
+ if (!box) {
23
+ box = {};
24
+ g[DEV_HOT_SLOT] = box;
25
+ }
26
+ return box;
27
+ }
28
+
29
+ /**
30
+ * Take the previous generation, if a soft reload left one.
31
+ *
32
+ * Clears the slot so a failed dispose cannot be retried against a half-stopped app.
33
+ */
34
+ export function takeDevHotGeneration(): DevHotGeneration | undefined {
35
+ const box = hotBox();
36
+ const prev = box.current;
37
+ box.current = undefined;
38
+ return prev;
39
+ }
40
+
41
+ /**
42
+ * Remember this evaluation so the next soft reload can dispose it.
43
+ *
44
+ * @param generation - Stop handle for the boot that just started
45
+ */
46
+ export function installDevHotGeneration(generation: DevHotGeneration): void {
47
+ hotBox().current = generation;
48
+ }
@@ -1191,7 +1191,7 @@ describe("oke dev Console vault config", () => {
1191
1191
  test("writes OKE_MANIFEST_PATH for the app child when a Manifest is available", async () => {
1192
1192
  const runner = await Bun.file(new URL("./dev-app-runner.ts", import.meta.url)).text();
1193
1193
  expect(runner).toContain("OKE_MANIFEST_PATH");
1194
- expect(runner).toMatch(/mod\.app\.boot\(\{/);
1194
+ expect(runner).toMatch(/\bapp\.boot\(\{/);
1195
1195
 
1196
1196
  const dir = await mkdtemp(join(tmpdir(), "oke-dev-manifest-handoff-"));
1197
1197
  await Bun.write(join(dir, "src/app.ts"), "export {}\n");
package/src/cli/dev.ts CHANGED
@@ -1036,6 +1036,13 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1036
1036
  setProcessEnv("OKE_KV_DRIVER", stackKvDriver);
1037
1037
  }
1038
1038
 
1039
+ // One secret for Console (this process) and the app child. Must land
1040
+ // before the first import of the app entry.
1041
+ {
1042
+ const { ensureDevAuthSecret } = await import("./dev-auth-secret.ts");
1043
+ setProcessEnv("OKE_AUTH_SECRET", await ensureDevAuthSecret(cwd));
1044
+ }
1045
+
1039
1046
  // One-shot schema sync for the compose (`dev`) profile: emits
1040
1047
  // drizzle/ for the active dialect, then pushes via drizzle-kit.
1041
1048
  let schemaPushOk = options.noDbPush === true;
@@ -1063,12 +1070,20 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1063
1070
  // Vault gaps before seed / app — missing contracts (e.g. OPENROUTER_API_KEY).
1064
1071
  {
1065
1072
  const { maybeAskVaultGaps } = await import("./ask-vault-gaps.ts");
1066
- const vaultCode = await maybeAskVaultGaps({
1067
- cwd,
1068
- entry,
1069
- write,
1070
- stdinIsTTY: options.stdinIsTTY ?? process.stdin.isTTY,
1071
- });
1073
+ // The secret prompt owns an open cursor line. A board repaint would
1074
+ // move that cursor and split the mask.
1075
+ bootBoard.hold();
1076
+ let vaultCode = 0;
1077
+ try {
1078
+ vaultCode = await maybeAskVaultGaps({
1079
+ cwd,
1080
+ entry,
1081
+ write,
1082
+ stdinIsTTY: options.stdinIsTTY ?? process.stdin.isTTY,
1083
+ });
1084
+ } finally {
1085
+ bootBoard.release();
1086
+ }
1072
1087
  if (vaultCode !== 0) {
1073
1088
  bootBoard.stop();
1074
1089
  restoreProcessEnv();
@@ -1443,6 +1458,8 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1443
1458
  autoPushRunner.trigger();
1444
1459
  }
1445
1460
  });
1461
+ const { watchDecisionLockfile } = await import("./decision-lock-watch.ts");
1462
+ const decisionLockWatch = watchDecisionLockfile(cwd);
1446
1463
 
1447
1464
  let stopped = false;
1448
1465
  const stop = () => {
@@ -1458,6 +1475,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1458
1475
  clientRegen.cancel();
1459
1476
  manifestRefresh.cancel();
1460
1477
  watcher.close();
1478
+ decisionLockWatch.close();
1461
1479
  const host = attachedHost;
1462
1480
  attachedHost = null;
1463
1481
  void host?.stop();
@@ -72,7 +72,7 @@ describe("oke stack", () => {
72
72
  const code = await runStackPreview({
73
73
  cwd: dir,
74
74
  images: {
75
- "channel.email": "axllent/mailpit:v1.31.1",
75
+ "channel.email": "axllent/mailpit:v1.31.2",
76
76
  },
77
77
  write: (t) => {
78
78
  out += t;