elanous 0.2.4 → 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 (455) hide show
  1. package/README.md +7 -6
  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/1883.c91a2648af4842fd.js +1 -0
  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/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.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/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
  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/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.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/8250.4a2c5553a07cdf47.js +1 -0
  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/{2ea96958-e8c7e112de50b067.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/{8046cadf-211ca3c0e00f2b72.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-c3890e90bd232732.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
  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-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +11 -1
  172. package/graphs/release/release-loop.yaml +9 -2
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/card-followup/README.md +33 -0
  177. package/plugins/card-followup/examples/card-sample.json +7 -0
  178. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  179. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  180. package/plugins/card-followup/graphs/run-step.ts +172 -0
  181. package/plugins/card-followup/plugin.json +25 -0
  182. package/plugins/geo-check/README.md +13 -0
  183. package/plugins/geo-check/examples/brand-sample.json +8 -0
  184. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  185. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  186. package/plugins/geo-check/graphs/run-step.ts +177 -0
  187. package/plugins/geo-check/plugin.json +24 -0
  188. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  189. package/plugins/job-coach/README.md +42 -0
  190. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  191. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  192. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  193. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  194. package/plugins/job-coach/graphs/run-step.ts +211 -17
  195. package/plugins/job-coach/plugin.json +16 -3
  196. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  197. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  198. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  199. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  200. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  201. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  202. package/scripts/backup/elanous-backup.sh +4 -1
  203. package/scripts/build-docs-site.ts +4 -3
  204. package/scripts/coord-channel-watch.sh +23 -4
  205. package/scripts/coord-post.sh +49 -12
  206. package/scripts/coord-tracks.json +5 -5
  207. package/scripts/daemon-port-baseline.txt +1 -2
  208. package/scripts/eval-bot-vs-grok.ts +79 -0
  209. package/scripts/eval-grok-live-baseline.ts +36 -0
  210. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  211. package/scripts/fleet/node-setup.sh +46 -7
  212. package/scripts/landing-heal/must-fix-match.ts +26 -0
  213. package/scripts/landing-heal/run-mission.ts +114 -20
  214. package/scripts/lib/android-test-device.ts +32 -0
  215. package/scripts/lib/front-route-measurement.ts +131 -0
  216. package/scripts/lib/tui-sim-keys.ts +42 -0
  217. package/scripts/measure-front-route-classifier.ts +56 -0
  218. package/scripts/media/ocr-leak-scan.sh +21 -0
  219. package/scripts/public-export.ts +2 -3
  220. package/scripts/public-import-pr.ts +80 -6
  221. package/scripts/publish-internal-dist.ts +2 -3
  222. package/scripts/red-sweep-diff.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +72 -0
  224. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  225. package/scripts/release-loop/gate-diff.ts +29 -0
  226. package/scripts/release-loop/gate-node.ts +366 -45
  227. package/scripts/release-loop/publish-node.ts +7 -3
  228. package/scripts/release-loop/release-docs.ts +20 -13
  229. package/scripts/release-loop/shard-plan.ts +47 -0
  230. package/scripts/release-loop/test-durations.ts +63 -0
  231. package/scripts/release-loop/upgrade-node.ts +3 -2
  232. package/scripts/test-audit-coverage-per-file.sh +16 -0
  233. package/scripts/test-audit-meaningless.ts +88 -0
  234. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  235. package/scripts/test-deterministic.ts +27 -5
  236. package/scripts/tui-sim.ts +59 -14
  237. package/scripts/ux/bump-installed.sh +20 -0
  238. package/scripts/ux/lib/cdp.ts +72 -0
  239. package/scripts/ux/record-page.ts +53 -0
  240. package/skills/explainer-video/SKILL.md +117 -0
  241. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  242. package/skills/explainer-video/engine/build.mjs +431 -0
  243. package/skills/explainer-video/engine/reel.mjs +128 -0
  244. package/skills/explainer-video/engine/reel.sh +8 -0
  245. package/skills/explainer-video/engine/vo.ts +79 -0
  246. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  247. package/src/agent/nested-agent-env.ts +1 -0
  248. package/src/agent-mission/driver.ts +95 -16
  249. package/src/agent-mission/mission-cli.ts +6 -0
  250. package/src/agent-mission/resource-ladder.ts +73 -25
  251. package/src/auth/acp-token.ts +16 -0
  252. package/src/auth/github-app-token.ts +112 -0
  253. package/src/autopilot/gh-rest-fallback.ts +177 -0
  254. package/src/autopilot/mission-origin.ts +20 -4
  255. package/src/autopilot/mission-phase-track.ts +11 -1
  256. package/src/autopilot/pr-manager.ts +51 -4
  257. package/src/autopilot/track-agent.ts +3 -3
  258. package/src/autopilot/track-courier.ts +2 -2
  259. package/src/boot/acp-server.ts +2 -16
  260. package/src/boot/daemon-tools/web-search.ts +2 -1
  261. package/src/boot/ws-bridge.ts +11 -0
  262. package/src/chat/index.ts +37 -8
  263. package/src/chat/log-scroll-key.ts +12 -0
  264. package/src/chat/submit-trace.ts +30 -0
  265. package/src/cli/agent-cli.ts +7 -2
  266. package/src/cli/decisions-cli.ts +94 -0
  267. package/src/cli/doctor-cli.ts +40 -54
  268. package/src/cli/doctor-readiness.ts +16 -24
  269. package/src/cli/intake-cli.ts +1 -1
  270. package/src/cli/mcp-call.ts +9 -0
  271. package/src/cli/mcp-gateway-cli.ts +7 -3
  272. package/src/cli/nexus-ios-bind.ts +25 -8
  273. package/src/cli/phone-link.ts +117 -0
  274. package/src/cli/plugin-cli.ts +136 -11
  275. package/src/cli/pr-lineage.ts +2 -1
  276. package/src/cli/private-ledger-commands.ts +31 -0
  277. package/src/cli/pwa-start.ts +3 -0
  278. package/src/cli/release-cli.ts +97 -7
  279. package/src/cli/self-update.ts +7 -4
  280. package/src/cli/start-cli.ts +33 -14
  281. package/src/dashboard/acp-boot.ts +3 -0
  282. package/src/dashboard/index.ts +62 -13
  283. package/src/dashboard/input/no-provider-reply.ts +13 -0
  284. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  285. package/src/dashboard/stale-install-check.ts +77 -0
  286. package/src/decisions/decision-ledger.ts +404 -0
  287. package/src/domains/outbound-alert.ts +13 -7
  288. package/src/domains/telegram-kind-route.ts +21 -0
  289. package/src/execution-loop/dispatch-task.ts +139 -0
  290. package/src/execution-loop/gate-decision-summary.ts +29 -0
  291. package/src/execution-loop/gates.ts +197 -0
  292. package/src/field/field-media.ts +245 -0
  293. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  294. package/src/graph-kinds/plugin-nodes.ts +10 -2
  295. package/src/graph-runner/runner.ts +35 -5
  296. package/src/grounding/token.ts +12 -6
  297. package/src/harness/harness-cli-command.ts +68 -9
  298. package/src/harness/harness-pod-dispatch.ts +8 -2
  299. package/src/harness/harness-stop.ts +147 -18
  300. package/src/harness/self-send-decide.ts +26 -17
  301. package/src/harness/self-send-target.ts +22 -1
  302. package/src/harness/worktree-branch-prefix.ts +4 -3
  303. package/src/index.ts +50 -13
  304. package/src/instance/resolve.ts +6 -0
  305. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  306. package/src/intake-plane/digest.ts +11 -2
  307. package/src/live/detail-switch.ts +8 -3
  308. package/src/llm/usable-llm.ts +32 -0
  309. package/src/loops/registry.ts +27 -5
  310. package/src/market/publish.ts +59 -3
  311. package/src/native-tool-catalog.ts +2 -2
  312. package/src/nexus/api/field-uploads.ts +143 -0
  313. package/src/nexus/api/graph-approvals.ts +174 -0
  314. package/src/nexus/api/harness-api.ts +219 -5
  315. package/src/nexus/api/http-server.ts +69 -6
  316. package/src/nexus/api/log-fabric.ts +10 -1
  317. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  318. package/src/nexus/api/merge-approvals.ts +2 -1
  319. package/src/nexus/api/meta-api.ts +14 -0
  320. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  321. package/src/nexus/api/plugins-market.ts +87 -3
  322. package/src/nexus/api/pod-credential-api.ts +68 -14
  323. package/src/nexus/api/public-routes.ts +7 -0
  324. package/src/nexus/api/trace.ts +57 -6
  325. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  326. package/src/nexus/index.ts +33 -32
  327. package/src/nexus/outbound/router.ts +1 -1
  328. package/src/notify/llm-budget-alert.ts +119 -0
  329. package/src/onboarding.ts +25 -2
  330. package/src/plugins/core/host.ts +14 -0
  331. package/src/plugins/install/market-fetch.ts +4 -3
  332. package/src/plugins/install/plugin-credentials.ts +106 -0
  333. package/src/plugins/install/plugin-install.ts +163 -32
  334. package/src/plugins/maker/plugin-maker.ts +208 -0
  335. package/src/prompt-library/universal-preamble.ts +8 -2
  336. package/src/relay/channel.ts +184 -0
  337. package/src/relay/mux.ts +182 -0
  338. package/src/relay/test-vectors.json +29 -0
  339. package/src/release-loop/checklist.ts +217 -0
  340. package/src/release-loop/release-note.ts +6 -3
  341. package/src/seat-address/seat-address.ts +34 -0
  342. package/src/self-dev/dev-cli.ts +40 -20
  343. package/src/self-dev/dev-pipeline.ts +77 -1
  344. package/src/self-implement/gate-baseline.ts +3 -1
  345. package/src/self-implement/goal-author.ts +18 -1
  346. package/src/self-implement/host-regate.ts +6 -2
  347. package/src/self-implement/orchestrator.ts +7 -2
  348. package/src/self-implement/run-ledger-compact.ts +1 -1
  349. package/src/self-implement/seams.ts +4 -2
  350. package/src/session/index.ts +23 -10
  351. package/src/skills/tools/dev-harness.ts +42 -0
  352. package/src/steward/steward-cards.ts +45 -0
  353. package/src/steward/triage.ts +50 -14
  354. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  355. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  356. package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  358. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  359. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  360. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  361. package/src/telegram-report.ts +12 -6
  362. package/src/telegram.ts +108 -1
  363. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  364. package/src/tui.ts +3 -2
  365. package/src/user-config.ts +32 -2
  366. package/src/util/spawn-sync-output.ts +15 -0
  367. package/src/version/code-revision.ts +1 -1
  368. package/src/version/packed-revision.json +1 -1
  369. package/src/video-pipeline/recipes/explainer.ts +213 -0
  370. package/src/video-pipeline/walk-line.ts +3 -2
  371. package/src/web-search/free.ts +77 -0
  372. package/src/web-search/index.ts +2 -0
  373. package/src/workflow-runtime/deps-bridge.ts +4 -3
  374. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  375. package/src/workflow-runtime/types.ts +2 -1
  376. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  388. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  454. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  455. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
@@ -0,0 +1,208 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, relative, resolve, sep } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { elanousStateRoot } from '../../autopilot/state-paths.js';
5
+ import { debug } from '../../debug/log.js';
6
+ import { runGraph } from '../../graph-runner/runner.js';
7
+ import { parseGraphTemplateYaml } from '../../self-implement/graph-yaml.js';
8
+ import { installPlugin } from '../install/plugin-install.js';
9
+
10
+ const CAPABILITIES = ['fs:workdir', 'proc:bun', 'proc:elanous'];
11
+ const isRecord = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value);
12
+
13
+ export interface MakePluginDeps {
14
+ codex: (dir: string, prompt: string) => Promise<void>;
15
+ install?: typeof installPlugin;
16
+ runGraph?: typeof runGraph;
17
+ }
18
+ export interface MakePluginOptions {
19
+ request: string;
20
+ name?: string;
21
+ parentDir?: string;
22
+ run?: boolean;
23
+ input?: unknown;
24
+ deps?: MakePluginDeps;
25
+ }
26
+ export interface MakePluginResult {
27
+ status: 'installed' | 'ran' | 'failed';
28
+ dir: string;
29
+ plugin: string;
30
+ graph: string;
31
+ errors: string[];
32
+ runStatus?: Awaited<ReturnType<typeof runGraph>>['status'];
33
+ timings: { scaffold: number; write: number; validate: number; repair?: number; install: number; run?: number };
34
+ }
35
+
36
+ export async function codexWrite(dir: string, prompt: string): Promise<void> {
37
+ const child = Bun.spawn(['codex', 'exec', '-C', dir, '-s', 'workspace-write', '--skip-git-repo-check', '-o', join(dir, '.codex-last-message.txt'), prompt], {
38
+ cwd: dir, stdin: 'ignore', stdout: 'ignore', stderr: 'pipe',
39
+ });
40
+ const [code, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()]);
41
+ if (code !== 0) throw new Error(`codex exec failed (${code}): ${stderr.trim().slice(0, 500)}`);
42
+ }
43
+
44
+ function scaffold(dir: string, slug: string, request: string): void {
45
+ mkdirSync(join(dir, 'graphs'));
46
+ mkdirSync(join(dir, 'examples'));
47
+ writeFileSync(join(dir, 'plugin.json'), JSON.stringify({ name: slug, version: '0.1.0', description: request,
48
+ extensions: { 'ai.elanous': { graphs: [`./graphs/${slug}.yaml`], capabilities: CAPABILITIES } } }, null, 2) + '\n');
49
+ writeFileSync(join(dir, 'graphs', `${slug}.yaml`), `graph_id: ${slug}\nversion: 1\nentry_node: main\nterminal_nodes: [done, failed]\nnodes:\n - { node_id: main, kind: agent, recipe: 'cmd:main', max_visits: 1 }\n - { node_id: done, kind: gate, recipe: none, max_visits: 1 }\n - { node_id: failed, kind: gate, recipe: none, max_visits: 1 }\nedges:\n - { from: main, on: outcome, map: { ok: done, fail: failed } }\n`);
50
+ writeFileSync(join(dir, 'graphs', 'recipes.yaml'), `main:\n command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" main'\n timeout_ms: 120000\n`);
51
+ writeFileSync(join(dir, 'graphs', 'run-step.ts'), `import { readFileSync } from 'node:fs';\nconst context = JSON.parse(readFileSync(process.env.ELANOUS_GRAPH_CONTEXT!, 'utf8'));\nconsole.log(JSON.stringify({ outcome: 'ok', input: context.input }));\n`);
52
+ writeFileSync(join(dir, 'examples', 'input.json'), '{}\n');
53
+ writeFileSync(join(dir, 'README.md'), `# ${slug}\n\n${request}\n\nRun the installed graph with examples/input.json as input. No messages are sent.\n`);
54
+ }
55
+
56
+ function inside(dir: string, path: string): string | undefined {
57
+ const full = resolve(dir, path);
58
+ return full.startsWith(`${dir}${sep}`) ? full : undefined;
59
+ }
60
+
61
+ function realTarget(path: string): string {
62
+ let ancestor = path;
63
+ while (!existsSync(ancestor)) {
64
+ const next = dirname(ancestor);
65
+ if (next === ancestor) throw new Error(`cannot resolve plugin directory: ${path}`);
66
+ ancestor = next;
67
+ }
68
+ return resolve(realpathSync(ancestor), relative(ancestor, path));
69
+ }
70
+
71
+ function assertLocalParent(parent: string): void {
72
+ const repoPlugins = realpathSync(resolve(import.meta.dir, '../../../plugins'));
73
+ const target = realTarget(parent);
74
+ if (target === repoPlugins || target.startsWith(`${repoPlugins}${sep}`)) {
75
+ throw new Error('repository plugins/ is not a local plugin directory');
76
+ }
77
+ }
78
+
79
+ export async function validatePluginDir(dir: string): Promise<string[]> {
80
+ const errors: string[] = [];
81
+ let manifest: unknown;
82
+ try {
83
+ const file = join(dir, 'plugin.json');
84
+ if (lstatSync(file).isSymbolicLink()) errors.push('안전하지 않은 plugin.json 링크');
85
+ else manifest = JSON.parse(readFileSync(file, 'utf8'));
86
+ }
87
+ catch (error) { errors.push(`plugin.json 파싱 실패: ${String(error)}`); }
88
+ const extension = isRecord(manifest) && isRecord(manifest.extensions) ? manifest.extensions['ai.elanous'] : undefined;
89
+ const graphs = isRecord(extension) ? extension.graphs : undefined;
90
+ if (isRecord(extension) && extension.contributes !== undefined) errors.push('extension contributes 덮어쓰기 금지');
91
+ if (!isRecord(manifest) || typeof manifest.name !== 'string' || !/^[a-z0-9][a-z0-9-]{1,39}$/.test(manifest.name) || typeof manifest.version !== 'string') errors.push('plugin.json 이름/버전 오류');
92
+ if (isRecord(manifest) && (manifest.capabilities !== undefined || manifest.contributes !== undefined || manifest.main !== undefined)) errors.push('plugin.json 최상위 권한/기여/main 덮어쓰기 금지');
93
+ if (!isRecord(extension) || !Array.isArray(extension.capabilities) || JSON.stringify(extension.capabilities) !== JSON.stringify(CAPABILITIES)) {
94
+ errors.push('capabilities 는 ["fs:workdir","proc:bun","proc:elanous"] 이어야 한다');
95
+ }
96
+ if (!Array.isArray(graphs) || graphs.length === 0 || !graphs.every(g => typeof g === 'string')) errors.push('plugin.json graphs 경로 없음');
97
+ let recipes: unknown;
98
+ try {
99
+ const file = join(dir, 'graphs', 'recipes.yaml');
100
+ if (lstatSync(file).isSymbolicLink()) errors.push('안전하지 않은 recipes.yaml 링크');
101
+ else recipes = parseYaml(readFileSync(file, 'utf8'));
102
+ }
103
+ catch (error) { errors.push(`recipes.yaml 파싱 실패: ${String(error)}`); }
104
+ if (!isRecord(recipes)) errors.push('recipes.yaml 은 맵이어야 한다');
105
+ else for (const [id, recipe] of Object.entries(recipes)) {
106
+ if (!isRecord(recipe) || typeof recipe.command !== 'string' || !recipe.command.trim() ||
107
+ (recipe.timeout_ms !== undefined && (!Number.isSafeInteger(recipe.timeout_ms) || Number(recipe.timeout_ms) <= 0))) errors.push(`잘못된 recipe: ${id}`);
108
+ }
109
+ for (const graph of Array.isArray(graphs) ? graphs : []) {
110
+ if (typeof graph !== 'string') continue;
111
+ const file = inside(dir, graph);
112
+ if (!file || !/^\.\/graphs\/[a-zA-Z0-9_-]+\.ya?ml$/.test(graph)) { errors.push(`안전하지 않은 graph 경로: ${graph}`); continue; }
113
+ try {
114
+ if (lstatSync(file).isSymbolicLink()) { errors.push(`안전하지 않은 graph 링크: ${graph}`); continue; }
115
+ const parsed = parseGraphTemplateYaml(readFileSync(file, 'utf8'), file);
116
+ errors.push(...parsed.errors.map(issue => `${issue.path}: ${issue.message}`));
117
+ const template = parsed.template;
118
+ if (!template) continue;
119
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(template.graphId)) errors.push(`잘못된 graph_id: ${template.graphId}`);
120
+ if (template.terminalNodes.length !== 2 || !template.terminalNodes.includes('done') || !template.terminalNodes.includes('failed')) errors.push('terminal_nodes 는 done, failed 이어야 한다');
121
+ const executionNodes = template.nodes.filter(node => !template.terminalNodes.includes(node.nodeId));
122
+ if (executionNodes.length < 2 || executionNodes.length > 6) errors.push(`실행 노드는 2~6개여야 한다: ${executionNodes.length}개`);
123
+ for (const node of template.nodes) {
124
+ if (node.recipe.startsWith('cmd:')) {
125
+ const id = node.recipe.slice(4);
126
+ if (!isRecord(recipes) || !isRecord(recipes[id]) || typeof recipes[id].command !== 'string' || !recipes[id].command) errors.push(`recipe ${id} 없음`);
127
+ } else if (node.recipe !== 'none') errors.push(`지원하지 않는 recipe: ${node.recipe}`);
128
+ }
129
+ } catch (error) { errors.push(`${graph} 파싱 실패: ${String(error)}`); }
130
+ }
131
+ const visit = (path: string): void => {
132
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
133
+ const file = join(path, entry.name);
134
+ if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) { errors.push(`안전하지 않은 파일: ${file}`); continue; }
135
+ if (entry.isDirectory()) visit(file);
136
+ else if (/\.[cm]?[jt]sx?$/.test(entry.name)) {
137
+ const source = readFileSync(file, 'utf8');
138
+ for (const match of source.matchAll(/\b(?:import|export)\s*(?:[\s\S]*?\s+from\s*)?["']([^"']+)["']|\b(?:import|require)\s*\(\s*["']([^"']+)["']/g)) {
139
+ const target = match[1] ?? match[2]!;
140
+ if (/(?:^|\/)src\//.test(target) && (target.startsWith('/') || target.startsWith('.'))) errors.push(`src import 금지: ${file}: ${target}`);
141
+ }
142
+ }
143
+ }
144
+ };
145
+ try { visit(dir); } catch (error) { errors.push(`파일 검사 실패: ${String(error)}`); }
146
+ try {
147
+ const example = join(dir, 'examples', 'input.json');
148
+ if (lstatSync(example).isSymbolicLink()) errors.push('안전하지 않은 examples/input.json 링크');
149
+ else JSON.parse(readFileSync(example, 'utf8'));
150
+ } catch (error) { errors.push(`examples/input.json 파싱 실패: ${String(error)}`); }
151
+ const step = join(dir, 'graphs', 'run-step.ts');
152
+ try {
153
+ if (!existsSync(step)) errors.push('run-step.ts 없음');
154
+ else if (errors.some(error => error.startsWith('안전하지 않은 파일:'))) return errors;
155
+ else {
156
+ const build = await Bun.build({ entrypoints: [step], target: 'bun' });
157
+ if (!build.success) errors.push(...build.logs.map(log => `run-step.ts 문법 오류: ${log.message}`));
158
+ }
159
+ } catch (error) { errors.push(`run-step.ts 문법 오류: ${String(error)}`); }
160
+ return errors;
161
+ }
162
+
163
+ export async function makePlugin({ request, name, parentDir, run, input, deps }: MakePluginOptions): Promise<MakePluginResult> {
164
+ const slug = (name ?? request).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 32).replace(/-$/, '') || `plugin-${Date.now()}`;
165
+ if (!request.trim() || !/^[a-z0-9][a-z0-9-]{1,39}$/.test(slug)) throw new Error(`invalid plugin request or name: ${slug}`);
166
+ if (name !== undefined && !/^[a-z0-9][a-z0-9-]{1,31}$/.test(name)) throw new Error(`invalid --name slug: ${name}`);
167
+ const root = elanousStateRoot();
168
+ const parent = resolve(parentDir ?? join(root, 'plugins-local'));
169
+ assertLocalParent(parent);
170
+ const dir = join(parent, slug);
171
+ if (existsSync(dir)) throw new Error(`plugin directory already exists: ${dir}`);
172
+ const timings: MakePluginResult['timings'] = { scaffold: 0, write: 0, validate: 0, install: 0 };
173
+ const result: MakePluginResult = { status: 'failed', dir, plugin: slug, graph: join(dir, 'graphs', `${slug}.yaml`), errors: [], timings };
174
+ const step = async (stage: keyof MakePluginResult['timings'], action: () => void | Promise<void>): Promise<void> => {
175
+ const start = performance.now();
176
+ try { await action(); }
177
+ finally { const ms = Math.round(performance.now() - start); timings[stage] = ms; debug.log('plugin.maker', stage, { ms, errors: result.errors }); }
178
+ };
179
+ try {
180
+ await step('scaffold', () => { mkdirSync(parent, { recursive: true }); mkdirSync(dir); scaffold(dir, slug, request); });
181
+ const codex = deps?.codex ?? codexWrite;
182
+ const prompt = `요청 원문:\n${request}\n\n이 폴더의 뼈대를 수정해 작동하는 로컬 그래프 플러그인을 작성하라. plugin.json extensions["ai.elanous"].graphs 와 capabilities 는 유지하고, graphs/<id>.yaml 은 entry_node, terminal_nodes: [done, failed], nodes 의 recipe: 'cmd:<id>', edges 의 on: outcome, map: { ok, fail } 을 써라. graphs/recipes.yaml 에 모든 cmd:<id> 의 command: 'bun "$ELANOUS_GRAPH_DIR/run-step.ts" <id>' 와 timeout_ms 를 선언하라. graphs/run-step.ts 의 각 단계 stdout 마지막 줄은 JSON {outcome, ...} 이며 이전 출력은 ELANOUS_GRAPH_CONTEXT JSON 의 outputs 에 있다. src/ import 금지. 엘라누스 기능은 CLI elanous ask --json 또는 elanous research --json 로만 사용. 아무것도 보내지 말 것. 실행 노드는 2~6개. examples/input.json 에 실제로 돌릴 입력을 넣을 것. 폴더 밖에는 쓰지 말 것.`;
183
+ await step('write', () => codex(dir, prompt));
184
+ await step('validate', async () => { result.errors = await validatePluginDir(dir); });
185
+ if (result.errors.length) {
186
+ await step('repair', () => codex(dir, `${prompt}\n\n아래 오류를 모두 수리하라:\n${result.errors.join('\n')}`));
187
+ await step('validate', async () => { result.errors = await validatePluginDir(dir); });
188
+ }
189
+ if (result.errors.length) return result;
190
+ let installedPath = '';
191
+ await step('install', async () => { installedPath = (await (deps?.install ?? installPlugin)(dir, { yes: true, root })).path; });
192
+ result.status = 'installed';
193
+ const graph = JSON.parse(readFileSync(join(dir, 'plugin.json'), 'utf8')).extensions['ai.elanous'].graphs[0] as string;
194
+ result.graph = resolve(installedPath, graph);
195
+ if (run) {
196
+ await step('run', async () => {
197
+ const value = input === undefined ? JSON.parse(readFileSync(join(dir, 'examples', 'input.json'), 'utf8')) : input;
198
+ result.runStatus = (await (deps?.runGraph ?? runGraph)(result.graph, { input: value })).status;
199
+ });
200
+ result.status = result.runStatus === 'done' ? 'ran' : 'failed';
201
+ if (result.status === 'failed') result.errors.push(`graph run: ${result.runStatus}`);
202
+ }
203
+ } catch (error) {
204
+ result.status = 'failed';
205
+ result.errors.push(error instanceof Error ? error.message : String(error));
206
+ }
207
+ return result;
208
+ }
@@ -59,6 +59,10 @@ export const PROJECT_ANCHOR_NON_CANDIDATE_FILENAMES = ['CLAUDE.md'] as const;
59
59
  export const PROJECT_TREE_MAX_CHARS = 8 * 1024;
60
60
  export const PROJECT_TREE_DIR_ENTRY_LIMIT = 20;
61
61
 
62
+ const NO_TOOLS_VERIFICATION_GUIDANCE = `## Chat without tools — verify rather than guess
63
+ This conversation has tools disabled. Do not guess repository or real-time facts that require verification, such as file counts, file contents, commits, or the current time. Say you cannot check them here. The project tree above is for navigation, not evidence for counts.
64
+ To check these facts, enable tools with \`elanous agent …\` or \`elanous chat --tools …\`.`;
65
+
62
66
  // P4 (2026-05-03) — Family-agnostic coding-agent lifecycle. Maps the
63
67
  // three user-facing pipelines to an explicit tool-batch sequence so every
64
68
  // model (not just codex) follows the same shape. Pattern source:
@@ -673,8 +677,7 @@ export function loadProjectAnchor(cwd: string): ProjectAnchorResult {
673
677
  return result;
674
678
  }
675
679
 
676
- /** Build the surface-agnostic preamble. Currently MVP — emits zero or
677
- * one system message (project anchor). Surfaces spread the result
680
+ /** Build the surface-agnostic preamble. Surfaces spread the result
678
681
  * into their own preamble; e.g.
679
682
  *
680
683
  * ```ts
@@ -729,6 +732,9 @@ export function buildUniversalPreamble(ctx: UniversalPreambleContext): LLMMessag
729
732
  content: `## Project Layout (top-level + 1 depth)\n\n\`\`\`\n${tree.content}\n\`\`\`\n\nUse this layout to pick directories directly instead of issuing broad project-root Grep/Glob calls. When AGENTS.md / CLAUDE.md describe a track or subsystem, this layout shows where its files actually live.`,
730
733
  });
731
734
  }
735
+ if (!ctx.enabledTools?.length) {
736
+ out.push({ role: 'system', content: NO_TOOLS_VERIFICATION_GUIDANCE });
737
+ }
732
738
  // P4 (2026-05-03) — Family-agnostic coding-agent lifecycle. Append
733
739
  // before family addendum so codex (which has its own discipline
734
740
  // overlay) sees both: lifecycle (what shape) + addendum (how to behave
@@ -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
+ }