elanous 0.2.5 → 0.2.6

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 (300) hide show
  1. package/README.md +5 -4
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/{7527.943753b6ed829f09.js → 1883.c91a2648af4842fd.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/{4853-35fdbe6bdbcdc0fc.js → 3165-95515ca5f299a120.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 3995-bee512a7063036ce.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 4240-7153881b0b377306.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/5329.8b7e4bca684b6eec.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{5348-bbf838e119661eee.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.js → 7633.0654fe6ea259c1ee.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/{9238.2cd5f47f4bf7a2c3.js → 8250.4a2c5553a07cdf47.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/{4daa2798-1363d725c79f3593.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/{f6f3850d-7f324c6566002113.js → 91ff6dda-3b33d42d20657429.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-84cd419494e16b0f.js → page-c3890e90bd232732.js} +1 -1
  84. package/apps/pwa/out/_next/static/chunks/{06ef2eba-d1b99ac6d2ea663c.js → beb5cf10-9adcc10b83930090.js} +1 -1
  85. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-2bf59c720fe9542c.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
  92. package/apps/pwa/out/approvals/index.html +1 -1
  93. package/apps/pwa/out/approvals/index.txt +21 -21
  94. package/apps/pwa/out/autopilot/index.html +1 -1
  95. package/apps/pwa/out/autopilot/index.txt +21 -21
  96. package/apps/pwa/out/board/index.html +1 -1
  97. package/apps/pwa/out/board/index.txt +21 -21
  98. package/apps/pwa/out/botlab/index.html +1 -1
  99. package/apps/pwa/out/botlab/index.txt +20 -20
  100. package/apps/pwa/out/bots/index.html +1 -1
  101. package/apps/pwa/out/bots/index.txt +20 -20
  102. package/apps/pwa/out/chat/index.html +1 -1
  103. package/apps/pwa/out/chat/index.txt +20 -20
  104. package/apps/pwa/out/control/index.html +1 -1
  105. package/apps/pwa/out/control/index.txt +21 -21
  106. package/apps/pwa/out/design-check/index.html +1 -1
  107. package/apps/pwa/out/design-check/index.txt +21 -21
  108. package/apps/pwa/out/editor/index.html +1 -1
  109. package/apps/pwa/out/editor/index.txt +21 -21
  110. package/apps/pwa/out/healthz +1 -1
  111. package/apps/pwa/out/icons/elanous-16.png +0 -0
  112. package/apps/pwa/out/icons/elanous-180.png +0 -0
  113. package/apps/pwa/out/icons/elanous-192.png +0 -0
  114. package/apps/pwa/out/icons/elanous-32.png +0 -0
  115. package/apps/pwa/out/icons/elanous-512.png +0 -0
  116. package/apps/pwa/out/index.html +1 -1
  117. package/apps/pwa/out/index.txt +20 -20
  118. package/apps/pwa/out/intake/index.html +1 -1
  119. package/apps/pwa/out/intake/index.txt +21 -21
  120. package/apps/pwa/out/live/index.html +1 -1
  121. package/apps/pwa/out/live/index.txt +21 -21
  122. package/apps/pwa/out/market/index.html +1 -1
  123. package/apps/pwa/out/market/index.txt +20 -20
  124. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  125. package/apps/pwa/out/missions/placeholder/index.txt +20 -20
  126. package/apps/pwa/out/morning/index.html +1 -1
  127. package/apps/pwa/out/morning/index.txt +21 -21
  128. package/apps/pwa/out/observatory/index.html +1 -1
  129. package/apps/pwa/out/observatory/index.txt +21 -21
  130. package/apps/pwa/out/reflection/index.html +1 -1
  131. package/apps/pwa/out/reflection/index.txt +21 -21
  132. package/apps/pwa/out/scheduler/index.html +1 -1
  133. package/apps/pwa/out/scheduler/index.txt +21 -21
  134. package/apps/pwa/out/sessions/index.html +1 -1
  135. package/apps/pwa/out/sessions/index.txt +20 -20
  136. package/apps/pwa/out/settings/devices/index.html +1 -1
  137. package/apps/pwa/out/settings/devices/index.txt +21 -21
  138. package/apps/pwa/out/settings/index.html +1 -1
  139. package/apps/pwa/out/settings/index.txt +21 -21
  140. package/apps/pwa/out/setup/done/index.html +1 -1
  141. package/apps/pwa/out/setup/done/index.txt +21 -21
  142. package/apps/pwa/out/setup/index.html +1 -1
  143. package/apps/pwa/out/setup/index.txt +21 -21
  144. package/apps/pwa/out/share/index.html +1 -1
  145. package/apps/pwa/out/share/index.txt +21 -21
  146. package/apps/pwa/out/showroom/index.html +1 -1
  147. package/apps/pwa/out/showroom/index.txt +20 -20
  148. package/apps/pwa/out/tasks/index.html +1 -1
  149. package/apps/pwa/out/tasks/index.txt +21 -21
  150. package/apps/pwa/out/term/index.html +1 -1
  151. package/apps/pwa/out/term/index.txt +21 -21
  152. package/apps/pwa/out/trace/index.html +1 -1
  153. package/apps/pwa/out/trace/index.txt +21 -21
  154. package/apps/pwa/out/vault/index.html +1 -1
  155. package/apps/pwa/out/vault/index.txt +21 -21
  156. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  157. package/apps/pwa/out/workflows/chat-ui/index.txt +21 -21
  158. package/apps/pwa/out/workflows/index.html +1 -1
  159. package/apps/pwa/out/workflows/index.txt +21 -21
  160. package/apps/pwa/out/workspace/index.html +1 -1
  161. package/apps/pwa/out/workspace/index.txt +21 -21
  162. package/apps/pwa/out/worktrees/index.html +1 -1
  163. package/apps/pwa/out/worktrees/index.txt +21 -21
  164. package/graphs/release/recipes.yaml +3 -0
  165. package/graphs/release/release-loop.yaml +3 -1
  166. package/package.json +1 -1
  167. package/plugins/card-followup/README.md +33 -0
  168. package/plugins/card-followup/examples/card-sample.json +7 -0
  169. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  170. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  171. package/plugins/card-followup/graphs/run-step.ts +172 -0
  172. package/plugins/card-followup/plugin.json +25 -0
  173. package/plugins/geo-check/README.md +13 -0
  174. package/plugins/geo-check/examples/brand-sample.json +8 -0
  175. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  176. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  177. package/plugins/geo-check/graphs/run-step.ts +177 -0
  178. package/plugins/geo-check/plugin.json +24 -0
  179. package/plugins/job-coach/README.md +30 -0
  180. package/scripts/coord-tracks.json +2 -2
  181. package/scripts/daemon-port-baseline.txt +1 -2
  182. package/scripts/lib/android-test-device.ts +32 -0
  183. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  184. package/scripts/release-loop/gate-diff.ts +29 -0
  185. package/scripts/release-loop/gate-node.ts +46 -13
  186. package/scripts/release-loop/shard-plan.ts +47 -0
  187. package/scripts/ux/record-page.ts +4 -1
  188. package/skills/explainer-video/SKILL.md +18 -0
  189. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
  190. package/skills/explainer-video/engine/build.mjs +1 -1
  191. package/skills/explainer-video/engine/reel.mjs +128 -0
  192. package/skills/explainer-video/engine/reel.sh +8 -0
  193. package/skills/explainer-video/engine/vo.ts +58 -16
  194. package/src/autopilot/mission-origin.ts +20 -4
  195. package/src/autopilot/mission-phase-track.ts +11 -1
  196. package/src/autopilot/track-agent.ts +3 -3
  197. package/src/autopilot/track-courier.ts +2 -2
  198. package/src/cli/nexus-ios-bind.ts +25 -8
  199. package/src/cli/phone-link.ts +117 -0
  200. package/src/cli/plugin-cli.ts +27 -0
  201. package/src/cli/private-ledger-commands.ts +31 -0
  202. package/src/cli/release-cli.ts +14 -11
  203. package/src/domains/outbound-alert.ts +5 -5
  204. package/src/field/field-media.ts +245 -0
  205. package/src/index.ts +18 -24
  206. package/src/nexus/api/field-uploads.ts +143 -0
  207. package/src/nexus/api/graph-approvals.ts +50 -1
  208. package/src/nexus/api/harness-api.ts +219 -5
  209. package/src/nexus/api/http-server.ts +9 -0
  210. package/src/notify/llm-budget-alert.ts +119 -0
  211. package/src/plugins/maker/plugin-maker.ts +208 -0
  212. package/src/relay/channel.ts +184 -0
  213. package/src/relay/mux.ts +182 -0
  214. package/src/relay/test-vectors.json +29 -0
  215. package/src/release-loop/checklist.ts +28 -2
  216. package/src/seat-address/seat-address.ts +34 -0
  217. package/src/self-implement/run-ledger-compact.ts +1 -1
  218. package/src/session/index.ts +23 -10
  219. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  220. package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
  221. package/src/telegram.ts +108 -1
  222. package/src/user-config.ts +4 -0
  223. package/src/version/packed-revision.json +1 -1
  224. package/src/video-pipeline/recipes/explainer.ts +213 -0
  225. package/src/video-pipeline/walk-line.ts +3 -2
  226. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
  227. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
  228. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
  229. package/apps/pwa/out/_next/static/chunks/1751.50b96ea9c37b49b6.js +0 -1
  230. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
  231. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
  233. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
  241. package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
  250. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
  251. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
  252. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
  253. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
  254. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
  255. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
  256. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
  257. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
  258. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
  259. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
  260. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +0 -1
  261. package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +0 -1
  262. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0ac5d053f0444d21.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/morning/page-bccce0babceae30f.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
  300. /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
@@ -0,0 +1,184 @@
1
+ import { createHash, timingSafeEqual } from 'node:crypto';
2
+ import sodium from 'libsodium-wrappers';
3
+
4
+ /** Maximum size of a complete encrypted frame, including sequence and MAC. */
5
+ export const MAX_FRAME_BYTES = 1024 * 1024;
6
+ const SEQUENCE_BYTES = 8;
7
+ const MAC_BYTES = 16;
8
+ const NONCE_BYTES = 24;
9
+ const MAX_SEQUENCE = (1n << 64n) - 1n;
10
+
11
+ export type ChannelResult<T> = { ok: true; value: T } | { ok: false; reason: string };
12
+ export interface DaemonKeyPair {
13
+ publicKey: Uint8Array;
14
+ secretKey: Uint8Array;
15
+ }
16
+ export interface SerializedDaemonKeyPair {
17
+ publicKey: string;
18
+ secretKey: string;
19
+ }
20
+
21
+ function rejected<T>(reason: string): ChannelResult<T> {
22
+ return { ok: false, reason };
23
+ }
24
+
25
+ function validKey(key: Uint8Array): boolean {
26
+ return key instanceof Uint8Array && key.length === 32;
27
+ }
28
+
29
+ function equalKeys(left: Uint8Array, right: Uint8Array): boolean {
30
+ return validKey(left) && validKey(right) && timingSafeEqual(left, right);
31
+ }
32
+
33
+ /** The daemon persists this pair; the phone creates a fresh pair per connection. */
34
+ export async function generateDaemonKeyPair(seed?: Uint8Array): Promise<DaemonKeyPair> {
35
+ await sodium.ready;
36
+ if (seed !== undefined && !validKey(seed)) throw new RangeError('X25519 seed must be 32 bytes');
37
+ const pair = seed === undefined ? sodium.crypto_kx_keypair() : sodium.crypto_kx_seed_keypair(seed);
38
+ return { publicKey: pair.publicKey, secretKey: pair.privateKey };
39
+ }
40
+
41
+ export function serverIdFromPublicKey(publicKey: Uint8Array): string {
42
+ if (!validKey(publicKey)) throw new RangeError('X25519 public key must be 32 bytes');
43
+ return createHash('sha256').update(publicKey).digest('hex');
44
+ }
45
+
46
+ export function serializeDaemonKeyPair(pair: DaemonKeyPair): SerializedDaemonKeyPair {
47
+ if (!validKey(pair.publicKey) || !validKey(pair.secretKey)) throw new RangeError('invalid X25519 key pair');
48
+ return { publicKey: Buffer.from(pair.publicKey).toString('base64'), secretKey: Buffer.from(pair.secretKey).toString('base64') };
49
+ }
50
+
51
+ export async function deserializeDaemonKeyPair(serialized: SerializedDaemonKeyPair): Promise<ChannelResult<DaemonKeyPair>> {
52
+ await sodium.ready;
53
+ if (typeof serialized?.publicKey !== 'string' || typeof serialized?.secretKey !== 'string' ||
54
+ !/^[A-Za-z0-9+/]{43}=$/.test(serialized.publicKey) || !/^[A-Za-z0-9+/]{43}=$/.test(serialized.secretKey)) {
55
+ return rejected('invalid daemon key serialization');
56
+ }
57
+ const publicKey = new Uint8Array(Buffer.from(serialized.publicKey, 'base64'));
58
+ const secretKey = new Uint8Array(Buffer.from(serialized.secretKey, 'base64'));
59
+ try {
60
+ if (!validKey(publicKey) || !validKey(secretKey) ||
61
+ !equalKeys(sodium.crypto_scalarmult_base(secretKey), publicKey)) {
62
+ return rejected('invalid daemon key pair');
63
+ }
64
+ } catch {
65
+ return rejected('invalid daemon key pair');
66
+ }
67
+ return { ok: true, value: { publicKey, secretKey } };
68
+ }
69
+
70
+ function sequenceBytes(sequence: bigint): Uint8Array {
71
+ const bytes = new Uint8Array(SEQUENCE_BYTES);
72
+ new DataView(bytes.buffer).setBigUint64(0, sequence, false);
73
+ return bytes;
74
+ }
75
+
76
+ function nonceFor(sequence: bigint): Uint8Array {
77
+ const nonce = new Uint8Array(NONCE_BYTES);
78
+ nonce.set(sequenceBytes(sequence), NONCE_BYTES - SEQUENCE_BYTES);
79
+ return nonce;
80
+ }
81
+
82
+ /** Each direction uses its own crypto_kx key and monotonically increasing sequence. */
83
+ export class EncryptedChannel {
84
+ private sendSequence = 0n;
85
+ private receiveSequence = 0n;
86
+ private readonly sendKey: Uint8Array;
87
+ private readonly receiveKey: Uint8Array;
88
+
89
+ constructor(sendKey: Uint8Array, receiveKey: Uint8Array) {
90
+ if (!validKey(sendKey) || !validKey(receiveKey)) throw new RangeError('invalid session keys');
91
+ this.sendKey = new Uint8Array(sendKey);
92
+ this.receiveKey = new Uint8Array(receiveKey);
93
+ }
94
+
95
+ seal(plaintext: Uint8Array): ChannelResult<Uint8Array> {
96
+ if (!(plaintext instanceof Uint8Array)) return rejected('invalid plaintext');
97
+ if (plaintext.length + SEQUENCE_BYTES + MAC_BYTES > MAX_FRAME_BYTES) return rejected('frame exceeds 1MB');
98
+ if (this.sendSequence > MAX_SEQUENCE) return rejected('send sequence exhausted');
99
+ const header = sequenceBytes(this.sendSequence);
100
+ try {
101
+ const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
102
+ plaintext, header, null, nonceFor(this.sendSequence), this.sendKey,
103
+ );
104
+ const frame = new Uint8Array(header.length + ciphertext.length);
105
+ frame.set(header);
106
+ frame.set(ciphertext, header.length);
107
+ this.sendSequence++;
108
+ return { ok: true, value: frame };
109
+ } catch {
110
+ return rejected('frame encryption failed');
111
+ }
112
+ }
113
+
114
+ open(frame: Uint8Array): ChannelResult<Uint8Array> {
115
+ if (!(frame instanceof Uint8Array) || frame.length < SEQUENCE_BYTES + MAC_BYTES) return rejected('invalid frame');
116
+ if (frame.length > MAX_FRAME_BYTES) return rejected('frame exceeds 1MB');
117
+ if (this.receiveSequence > MAX_SEQUENCE) return rejected('receive sequence exhausted');
118
+ const sequence = new DataView(frame.buffer, frame.byteOffset, SEQUENCE_BYTES).getBigUint64(0, false);
119
+ if (sequence < this.receiveSequence) return rejected('replayed frame');
120
+ if (sequence > this.receiveSequence) return rejected('out-of-order frame');
121
+ const header = frame.subarray(0, SEQUENCE_BYTES);
122
+ try {
123
+ const plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
124
+ null, frame.subarray(SEQUENCE_BYTES), header, nonceFor(sequence), this.receiveKey,
125
+ );
126
+ this.receiveSequence++;
127
+ return { ok: true, value: plaintext };
128
+ } catch {
129
+ return rejected('frame authentication failed');
130
+ }
131
+ }
132
+ }
133
+
134
+ export interface PhoneChannel {
135
+ /** Sent to the daemon during handshake; the private key stays on the phone. */
136
+ phonePublicKey: Uint8Array;
137
+ serverId: string;
138
+ channel: EncryptedChannel;
139
+ }
140
+
141
+ /** Pin the daemon's public key before deriving any traffic keys. */
142
+ export async function createPhoneChannel(
143
+ daemonPublicKey: Uint8Array,
144
+ expectedDaemonPublicKey: Uint8Array,
145
+ phoneSeed?: Uint8Array,
146
+ ): Promise<ChannelResult<PhoneChannel>> {
147
+ await sodium.ready;
148
+ if (!validKey(daemonPublicKey) || !equalKeys(daemonPublicKey, expectedDaemonPublicKey)) {
149
+ return rejected('wrong daemon public key');
150
+ }
151
+ if (phoneSeed !== undefined && !validKey(phoneSeed)) return rejected('invalid phone seed');
152
+ const phoneKeys = phoneSeed === undefined ? sodium.crypto_kx_keypair() : sodium.crypto_kx_seed_keypair(phoneSeed);
153
+ try {
154
+ const keys = sodium.crypto_kx_client_session_keys(phoneKeys.publicKey, phoneKeys.privateKey, daemonPublicKey);
155
+ return { ok: true, value: {
156
+ phonePublicKey: phoneKeys.publicKey,
157
+ serverId: serverIdFromPublicKey(daemonPublicKey),
158
+ channel: new EncryptedChannel(keys.sharedTx, keys.sharedRx),
159
+ } };
160
+ } catch {
161
+ return rejected('invalid daemon public key');
162
+ }
163
+ }
164
+
165
+ /** Called with the phone's ephemeral public key; crypto_kx supplies opposite Tx/Rx keys. */
166
+ export async function acceptDaemonChannel(
167
+ daemonKeys: DaemonKeyPair,
168
+ phonePublicKey: Uint8Array,
169
+ ): Promise<ChannelResult<EncryptedChannel>> {
170
+ await sodium.ready;
171
+ if (!validKey(daemonKeys?.publicKey) || !validKey(daemonKeys?.secretKey)) {
172
+ return rejected('invalid daemon key pair');
173
+ }
174
+ if (!validKey(phonePublicKey)) return rejected('invalid phone public key');
175
+ try {
176
+ if (!equalKeys(sodium.crypto_scalarmult_base(daemonKeys.secretKey), daemonKeys.publicKey)) {
177
+ return rejected('invalid daemon key pair');
178
+ }
179
+ const keys = sodium.crypto_kx_server_session_keys(daemonKeys.publicKey, daemonKeys.secretKey, phonePublicKey);
180
+ return { ok: true, value: new EncryptedChannel(keys.sharedTx, keys.sharedRx) };
181
+ } catch {
182
+ return rejected('invalid handshake key');
183
+ }
184
+ }
@@ -0,0 +1,182 @@
1
+ import { MAX_FRAME_BYTES } from './channel.js';
2
+ import type { ChannelResult } from './channel.js';
3
+
4
+ export type MuxFrame =
5
+ | { streamId: number; kind: 'open'; protocol: 'http'; method: string; path: string; headers: Record<string, string> }
6
+ | { streamId: number; kind: 'open'; protocol: 'ws'; path: string; headers: Record<string, string> }
7
+ | { streamId: number; kind: 'data'; data: Uint8Array }
8
+ | { streamId: number; kind: 'close'; reason: string };
9
+
10
+ // u32 BE length (remaining bytes), u32 BE stream ID, u8 kind (1=open, 2=data, 3=close).
11
+ // Open: u8 protocol (1=http, 2=ws), [http: u16 method bytes + UTF-8 method],
12
+ // u32 path bytes + UTF-8 path, u16 header count, then u16 name bytes + UTF-8 name,
13
+ // u16 value bytes + UTF-8 value per header. Data: remaining raw bytes.
14
+ // Close: u16 reason bytes + UTF-8 reason. One frame per encrypted plaintext.
15
+ const encoder = new TextEncoder();
16
+ const decoder = new TextDecoder('utf-8', { fatal: true });
17
+ const HEADER_BYTES = 9;
18
+ // EncryptedChannel appends an 8-byte sequence and 16-byte MAC to each plaintext.
19
+ export const MAX_MUX_FRAME_BYTES = MAX_FRAME_BYTES - 24;
20
+
21
+ type Result<T> = ChannelResult<T>;
22
+ function reject<T>(reason: string): Result<T> { return { ok: false, reason }; }
23
+
24
+ class Writer {
25
+ private parts: Uint8Array[] = [];
26
+ length = 0;
27
+ bytes(value: Uint8Array) { this.parts.push(value); this.length += value.length; }
28
+ number(value: number, size: 1 | 2 | 4) {
29
+ const bytes = new Uint8Array(size);
30
+ const view = new DataView(bytes.buffer);
31
+ if (size === 1) view.setUint8(0, value);
32
+ else if (size === 2) view.setUint16(0, value, false);
33
+ else view.setUint32(0, value, false);
34
+ this.bytes(bytes);
35
+ }
36
+ text(value: string, size: 2 | 4): boolean {
37
+ if (typeof value !== 'string') return false;
38
+ const bytes = encoder.encode(value);
39
+ if (bytes.length > (size === 2 ? 0xffff : 0xffffffff)) return false;
40
+ this.number(bytes.length, size);
41
+ this.bytes(bytes);
42
+ return true;
43
+ }
44
+ finish(): Uint8Array {
45
+ const out = new Uint8Array(this.length);
46
+ let offset = 0;
47
+ for (const part of this.parts) { out.set(part, offset); offset += part.length; }
48
+ return out;
49
+ }
50
+ }
51
+
52
+ class Reader {
53
+ offset = 0;
54
+ constructor(readonly bytes: Uint8Array) {}
55
+ number(size: 1 | 2 | 4): number | undefined {
56
+ if (this.offset + size > this.bytes.length) return undefined;
57
+ const view = new DataView(this.bytes.buffer, this.bytes.byteOffset + this.offset, size);
58
+ this.offset += size;
59
+ return size === 1 ? view.getUint8(0) : size === 2 ? view.getUint16(0, false) : view.getUint32(0, false);
60
+ }
61
+ text(size: 2 | 4): string | undefined {
62
+ const length = this.number(size);
63
+ if (length === undefined || this.offset + length > this.bytes.length) return undefined;
64
+ const bytes = this.bytes.subarray(this.offset, this.offset + length);
65
+ this.offset += length;
66
+ return decoder.decode(bytes);
67
+ }
68
+ }
69
+
70
+ function validId(id: number): boolean { return Number.isInteger(id) && id >= 0 && id <= 0xffffffff; }
71
+
72
+ /** Share one instance between the encoder and decoder of a single endpoint. */
73
+ export class MuxStreamState {
74
+ private readonly active = new Set<number>();
75
+
76
+ isOpen(id: number): boolean { return this.active.has(id); }
77
+ opened(id: number): void { this.active.add(id); }
78
+ closed(id: number): void { this.active.delete(id); }
79
+ }
80
+
81
+ /** No network I/O; shares stream IDs with the receiving decoder when supplied a state. */
82
+ export class MuxEncoder {
83
+ constructor(private readonly streams: MuxStreamState = new MuxStreamState()) {}
84
+
85
+ encode(frame: MuxFrame): Result<Uint8Array> {
86
+ if (!frame || !validId(frame.streamId)) return reject('invalid stream ID');
87
+ if (frame.kind !== 'open' && frame.kind !== 'data' && frame.kind !== 'close') return reject('unknown frame kind');
88
+ if (frame.kind === 'open' ? this.streams.isOpen(frame.streamId) : !this.streams.isOpen(frame.streamId)) {
89
+ return reject(frame.kind === 'open' ? 'stream already open' : 'stream not open');
90
+ }
91
+ const writer = new Writer();
92
+ writer.number(0, 4);
93
+ writer.number(frame.streamId, 4);
94
+ if (frame.kind === 'open') {
95
+ writer.number(1, 1);
96
+ if (frame.protocol !== 'http' && frame.protocol !== 'ws') return reject('unknown protocol');
97
+ writer.number(frame.protocol === 'http' ? 1 : 2, 1);
98
+ if (frame.protocol === 'http' && (!frame.method || !writer.text(frame.method, 2))) return reject('invalid method');
99
+ if (!frame.path || !writer.text(frame.path, 4)) return reject('invalid path');
100
+ if (!frame.headers || typeof frame.headers !== 'object' || Array.isArray(frame.headers)) return reject('invalid headers');
101
+ const entries = Object.entries(frame.headers);
102
+ if (entries.length > 0xffff) return reject('invalid headers');
103
+ writer.number(entries.length, 2);
104
+ for (const [name, value] of entries) {
105
+ if (!name || !writer.text(name, 2) || !writer.text(value, 2)) return reject('invalid headers');
106
+ }
107
+ } else if (frame.kind === 'data') {
108
+ if (!(frame.data instanceof Uint8Array)) return reject('invalid data');
109
+ writer.number(2, 1);
110
+ writer.bytes(frame.data);
111
+ } else if (frame.kind === 'close') {
112
+ writer.number(3, 1);
113
+ if (!writer.text(frame.reason, 2)) return reject('invalid reason');
114
+ }
115
+ if (writer.length > MAX_MUX_FRAME_BYTES) return reject('frame exceeds 1MB');
116
+ const out = writer.finish();
117
+ new DataView(out.buffer).setUint32(0, out.length - 4, false);
118
+ if (frame.kind === 'open') this.streams.opened(frame.streamId);
119
+ if (frame.kind === 'close') this.streams.closed(frame.streamId);
120
+ return { ok: true, value: out };
121
+ }
122
+ }
123
+
124
+ /** Rejected frames never change stream state. Share state with the sending encoder per endpoint. */
125
+ export class MuxDecoder {
126
+ constructor(private readonly streams: MuxStreamState = new MuxStreamState()) {}
127
+
128
+ decode(bytes: Uint8Array): Result<MuxFrame> {
129
+ if (!(bytes instanceof Uint8Array)) return reject('invalid frame');
130
+ if (bytes.length > MAX_MUX_FRAME_BYTES) return reject('frame exceeds 1MB');
131
+ if (bytes.length < HEADER_BYTES) return reject('truncated frame');
132
+ const reader = new Reader(bytes);
133
+ const length = reader.number(4)!;
134
+ if (length !== bytes.length - 4) return reject(length > bytes.length - 4 ? 'truncated frame' : 'trailing frame bytes');
135
+ const streamId = reader.number(4)!;
136
+ const kind = reader.number(1)!;
137
+ if (kind !== 1 && kind !== 2 && kind !== 3) return reject('unknown frame kind');
138
+ if (kind === 1 ? this.streams.isOpen(streamId) : !this.streams.isOpen(streamId)) {
139
+ return reject(kind === 1 ? 'stream already open' : 'stream not open');
140
+ }
141
+ try {
142
+ let frame: MuxFrame;
143
+ if (kind === 1) {
144
+ const protocolByte = reader.number(1);
145
+ if (protocolByte !== 1 && protocolByte !== 2) return reject(protocolByte === undefined ? 'truncated frame' : 'unknown protocol');
146
+ const method = protocolByte === 1 ? reader.text(2) : undefined;
147
+ if (protocolByte === 1 && method === undefined) return reject('truncated frame');
148
+ const path = reader.text(4);
149
+ const count = reader.number(2);
150
+ if (path === undefined || count === undefined) return reject('truncated frame');
151
+ if (!path || (protocolByte === 1 && !method)) return reject('invalid open frame');
152
+ const entries: [string, string][] = [];
153
+ const names = new Set<string>();
154
+ for (let i = 0; i < count; i++) {
155
+ const name = reader.text(2);
156
+ const value = reader.text(2);
157
+ if (name === undefined || value === undefined) return reject('truncated frame');
158
+ if (!name || names.has(name)) return reject('invalid headers');
159
+ names.add(name);
160
+ entries.push([name, value]);
161
+ }
162
+ const headers = Object.fromEntries(entries);
163
+ frame = protocolByte === 1
164
+ ? { streamId, kind: 'open', protocol: 'http', method: method!, path, headers }
165
+ : { streamId, kind: 'open', protocol: 'ws', path, headers };
166
+ } else if (kind === 2) {
167
+ frame = { streamId, kind: 'data', data: bytes.slice(reader.offset) };
168
+ reader.offset = bytes.length;
169
+ } else {
170
+ const reason = reader.text(2);
171
+ if (reason === undefined) return reject('truncated frame');
172
+ frame = { streamId, kind: 'close', reason };
173
+ }
174
+ if (reader.offset !== bytes.length) return reject('trailing frame bytes');
175
+ if (kind === 1) this.streams.opened(streamId);
176
+ if (kind === 3) this.streams.closed(streamId);
177
+ return { ok: true, value: frame };
178
+ } catch {
179
+ return reject('invalid UTF-8 frame');
180
+ }
181
+ }
182
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "daemonSeedHex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
3
+ "phoneSeedHex": "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
4
+ "daemonPublicKeyHex": "0e0216223f147143d32615a91189c288c1728cba3cc5f9f621b1026e03d83129",
5
+ "phonePublicKeyHex": "99f4674ecc87c0b8e712f192b8f49e7442a9376b4875967ababa28471019a93e",
6
+ "serverId": "4bc447d530c4044729c44372319cebfcbe079462826a6d39ea4828b149f9c685",
7
+ "phoneToDaemonKeySha256": "1fd0e0a3bd4e5afff9efb91653c4e67f549b2aaf26e8fea9a5fb263cbd987ea0",
8
+ "daemonToPhoneKeySha256": "534f8e2462d2bfe07a31cc43a2fd7a46d6109c31289c971c17626324b9806ee8",
9
+ "nonceHex": "000000000000000000000000000000000000000000000000",
10
+ "request": {
11
+ "streamId": 16909060,
12
+ "kind": "open",
13
+ "protocol": "http",
14
+ "method": "POST",
15
+ "path": "/v1/health",
16
+ "headers": {
17
+ "authorization": "Bearer vector-token"
18
+ }
19
+ },
20
+ "requestMuxHex": "000000400102030401010004504f53540000000a2f76312f6865616c74680001000d617574686f72697a6174696f6e001342656172657220766563746f722d746f6b656e",
21
+ "requestEncryptedHex": "0000000000000000d793343f86630442fcf3812bc1375c4b102aa1ffd546ac4ecbd5f5f4e9e4b3c2de47616a01df19b25b317cd0c5f3c2f0aa7a8b12f307c9872c5f03765a0b1c0e6b6f46dcbda1758ae286b475870eb7ad957f409e",
22
+ "response": {
23
+ "streamId": 16909060,
24
+ "kind": "data",
25
+ "dataHex": "00ff1020"
26
+ },
27
+ "responseMuxHex": "00000009010203040200ff1020",
28
+ "responseEncryptedHex": "0000000000000000bf589589e25e990a1c7da5fb7a773f0a798e84d1f444d51bd8e3e0bdb4"
29
+ }
@@ -6,12 +6,14 @@ import { debug } from '../debug/log.js';
6
6
  import { releaseLedgerRoot } from '../instance/resolve.js';
7
7
 
8
8
  export type ChecklistStatus = 'green' | 'yellow' | 'red' | 'done';
9
+ export type ChecklistDisposition = 'move' | 'known-issue' | 'block';
9
10
  export interface ChecklistItem {
10
11
  id: string;
11
12
  title: string;
12
13
  status: ChecklistStatus;
13
14
  owner?: string;
14
15
  evidence?: string;
16
+ disposition?: ChecklistDisposition;
15
17
  updatedAt: string;
16
18
  updatedBy: string;
17
19
  }
@@ -121,12 +123,13 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
121
123
  });
122
124
  }
123
125
 
124
- export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string }, by: string): Checklist {
126
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition }, by: string): Checklist {
125
127
  return mutate(v, (data) => {
126
128
  const item = data.items.find((i) => i.id === id);
127
129
  if (!item) throw new Error(`없는 칸: ${id}`);
128
130
  if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new Error(`잘못된 상태: ${patch.status}`);
129
- const fields = (['evidence', 'owner', 'status'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
131
+ if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new Error(`잘못된 처분: ${patch.disposition}`);
132
+ const fields = (['evidence', 'owner', 'status', 'disposition'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
130
133
  if (fields.length === 0) return false;
131
134
  const at = new Date().toISOString();
132
135
  for (const field of fields) {
@@ -169,6 +172,29 @@ export function summarizeChecklist(data: Checklist): ChecklistSummary {
169
172
  return summary;
170
173
  }
171
174
 
175
+ export interface ChecklistGate {
176
+ ok: boolean;
177
+ red: string[];
178
+ undecided: string[];
179
+ blocked: string[];
180
+ moved: string[];
181
+ knownIssues: Array<{ id: string; title: string; evidence: string }>;
182
+ }
183
+
184
+ export function checklistGate(v: string): ChecklistGate {
185
+ const result: ChecklistGate = { ok: true, red: [], undecided: [], blocked: [], moved: [], knownIssues: [] };
186
+ for (const item of listChecklist(v).items) {
187
+ if (item.status === 'red') result.red.push(item.id);
188
+ if (item.status !== 'yellow') continue;
189
+ if (item.disposition === 'block') result.blocked.push(item.id);
190
+ else if (item.disposition === 'move') result.moved.push(item.id);
191
+ else if (item.disposition === 'known-issue') result.knownIssues.push({ id: item.id, title: item.title, evidence: item.evidence ?? '' });
192
+ else result.undecided.push(item.id);
193
+ }
194
+ result.ok = result.red.length === 0 && result.undecided.length === 0 && result.blocked.length === 0;
195
+ return result;
196
+ }
197
+
172
198
  export function seedFromRoadmap(v: string, markdown: string): Checklist {
173
199
  return mutate(v, (data) => {
174
200
  let added = false;
@@ -0,0 +1,34 @@
1
+ import trackData from '../../scripts/coord-tracks.json';
2
+
3
+ export type SeatEntry = {
4
+ id: string;
5
+ title?: string;
6
+ alias?: string;
7
+ };
8
+
9
+ export type SeatAddress = { seats: string[]; body: string };
10
+
11
+ const registry: readonly SeatEntry[] = trackData.tracks.filter((entry) => 'title' in entry);
12
+ const addressLine = /^@([A-Za-z][A-Za-z0-9_-]*(?:,[A-Za-z][A-Za-z0-9_-]*)*)(?:[ \t]+|\r?\n|(?![\s\S]))/gm;
13
+
14
+ /** Parse an address only when its @ begins a line; leave the instruction text intact. */
15
+ export function parseSeatAddress(text: string): SeatAddress | null {
16
+ const match = addressLine.exec(text);
17
+ addressLine.lastIndex = 0;
18
+ if (!match) return null;
19
+
20
+ const seats = match[1]!.split(',');
21
+ const body = `${text.slice(0, match.index)}${text.slice(match.index + match[0].length)}`;
22
+ return { seats, body };
23
+ }
24
+
25
+ /** Resolve against the seat registry, preferring titles to ids to legacy aliases. */
26
+ export function resolveSeat(address: string, seats: readonly SeatEntry[] = registry): SeatEntry | undefined {
27
+ const needle = address.replace(/^@/, '').toLocaleLowerCase();
28
+ if (!needle) return undefined;
29
+ for (const field of ['title', 'id', 'alias'] as const) {
30
+ const found = seats.find((seat) => seat[field]?.toLocaleLowerCase() === needle);
31
+ if (found) return found;
32
+ }
33
+ return undefined;
34
+ }
@@ -280,7 +280,7 @@ function recordedString(entries: readonly RunLedgerEntry[], field: string): stri
280
280
  return undefined;
281
281
  }
282
282
 
283
- function summarizeTerminalLedger(runId: string, entries: readonly RunLedgerEntry[]): RunLedgerSummary {
283
+ export function summarizeTerminalLedger(runId: string, entries: readonly RunLedgerEntry[]): RunLedgerSummary {
284
284
  const last = entries.at(-1)?.timestamp;
285
285
  if (!last || !Number.isFinite(Date.parse(last)) || !hasTerminalRunStatus(entries)) throw new Error('ledger changed since plan');
286
286
  const terminator = [...entries].reverse().find((entry) => entry.event === 'human-stop' || entry.event === 'terminal'
@@ -434,12 +434,20 @@ function readIndex(root: string = sessionRoot()): SessionMeta[] {
434
434
  }
435
435
  }
436
436
 
437
- function writeIndex(entries: SessionMeta[], root: string = sessionRoot()): void {
437
+ // 여러 프로세스(`elanous ask` 동시 실행 등)가 같은 목록을 «읽고-고치고-통째로 쓰기» 한다.
438
+ // 쓰기 직전 잠금 안에서 디스크의 최신 목록을 다시 읽어 id 로 합치고, 임시 파일 이름은 프로세스마다 다르게 한다
439
+ // (10-01 MK 재현: 고정 `index.json.tmp` → 한쪽 ENOENT · 덮어쓰기로 남의 세션이 목록에서 사라짐).
440
+ function writeIndex(entries: SessionMeta[], root: string = sessionRoot(), removed: Iterable<string> = []): void {
438
441
  ensureRoots(root);
439
442
  const p = indexPath(root);
440
- const tmp = p + '.tmp';
441
- writeFileSync(tmp, JSON.stringify(entries, null, 2) + '\n', 'utf-8');
442
- renameSync(tmp, p);
443
+ withDirLock(join(root, '.index.lock'), INDEX_LOCK_TIMEOUT_MS, () => {
444
+ const mine = new Set(entries.map((m) => m.id));
445
+ const gone = new Set(removed);
446
+ const peers = readIndex(root).filter((m) => !mine.has(m.id) && !gone.has(m.id));
447
+ const tmp = `${p}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
448
+ writeFileSync(tmp, JSON.stringify([...peers, ...entries], null, 2) + '\n', 'utf-8');
449
+ renameSync(tmp, p);
450
+ });
443
451
  }
444
452
 
445
453
  // ── Tier 1 Phase 3 — listener primitive ──────────────────────────────
@@ -661,7 +669,7 @@ export function rewriteSessionMessages(
661
669
  const body = messages
662
670
  .map(m => JSON.stringify(m.turn_id !== undefined ? m : { ...m, turn_id: mintTurnUri() }))
663
671
  .join('\n');
664
- const tmp = file + '.tmp';
672
+ const tmp = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
665
673
  writeFileSync(tmp, body ? body + '\n' : '', 'utf-8');
666
674
  renameSync(tmp, file);
667
675
 
@@ -800,8 +808,13 @@ function residentSessionLockPath(root: string): string {
800
808
 
801
809
  function withResidentSessionLock<T>(root: string, action: () => T): T {
802
810
  ensureRoots(root);
803
- const lock = residentSessionLockPath(root);
804
- const deadline = Date.now() + RESIDENT_SESSION_LOCK_TIMEOUT_MS;
811
+ return withDirLock(residentSessionLockPath(root), RESIDENT_SESSION_LOCK_TIMEOUT_MS, action);
812
+ }
813
+
814
+ const INDEX_LOCK_TIMEOUT_MS = 10_000;
815
+
816
+ function withDirLock<T>(lock: string, timeoutMs: number, action: () => T): T {
817
+ const deadline = Date.now() + timeoutMs;
805
818
  for (;;) {
806
819
  try {
807
820
  mkdirSync(lock);
@@ -814,7 +827,7 @@ function withResidentSessionLock<T>(root: string, action: () => T): T {
814
827
  continue;
815
828
  }
816
829
  } catch { continue; }
817
- if (Date.now() >= deadline) throw new Error(`timed out acquiring resident session lock for ${root}`);
830
+ if (Date.now() >= deadline) throw new Error(`timed out acquiring lock ${lock}`);
818
831
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
819
832
  void error;
820
833
  }
@@ -855,7 +868,7 @@ export function deleteSession(id: string, root: string = sessionRoot()): boolean
855
868
  const before = idx.length;
856
869
  const next = idx.filter(m => m.id !== id);
857
870
  if (next.length === before) return false;
858
- writeIndex(next, root);
871
+ writeIndex(next, root, [id]);
859
872
  // Clear active marker if it pointed at the deleted session.
860
873
  if (getActiveSessionId() === id) clearActiveSessionId();
861
874
  return true;
@@ -876,7 +889,7 @@ export function deleteSessions(ids: string[], root: string = sessionRoot()): num
876
889
  const idx = readIndex(root);
877
890
  const next = idx.filter(m => !set.has(m.id));
878
891
  const removed = idx.length - next.length;
879
- if (removed > 0) writeIndex(next, root);
892
+ if (removed > 0) writeIndex(next, root, set);
880
893
  const active = getActiveSessionId();
881
894
  if (active && set.has(active)) clearActiveSessionId();
882
895
  return removed;
@@ -0,0 +1,18 @@
1
+ export const POD_BUN_CACHE_HOST_PATH = 'POD_BUN_CACHE_HOST_PATH';
2
+
3
+ /** A host-backed Bun install cache; only opt into it when the mounted directory is writable. */
4
+ export function podBunCacheVolume(hostPath: string) {
5
+ return {
6
+ volume: { name: 'bun-cache', hostPath: { path: hostPath, type: 'DirectoryOrCreate' } },
7
+ volumeMount: { name: 'bun-cache', mountPath: '/bun-cache', readOnly: false },
8
+ shellPrefix: 'if [ -d /bun-cache ] && [ -w /bun-cache ]; then export BUN_INSTALL_CACHE_DIR=/bun-cache; fi;',
9
+ };
10
+ }
11
+
12
+ /** First Bun install timing in a combined shard log (including a no-op install). */
13
+ export function parseInstallSeconds(log: string): number | null {
14
+ const match = /(?:\d+ packages? installed|Checked \d+ installs across \d+ packages(?: \(no changes\))?)\s*\[(\d+(?:\.\d+)?)(ms|s)\]/.exec(log);
15
+ if (!match) return null;
16
+ const duration = Number(match[1]);
17
+ return match[2] === 'ms' ? duration / 1000 : duration;
18
+ }