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
@@ -1,12 +1,13 @@
1
1
  import { getDefaultLogStore, resolveLogInstanceName, LogStore, type LogStoreRow, type LogQuery } from '../../mss/logging/log-store.js';
2
2
  import { readLogInstances, type LogInstanceView } from '../../mss/logging/instance-registry.js';
3
- import { existsSync } from 'node:fs';
3
+ import { existsSync, statSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { prodInstanceRoot } from '../../instance/resolve.js';
6
6
  import { debug, redactSecretText, redactSecrets } from '../../debug/log.js';
7
- import { activeStoreNames, parseSinceParam, type LogFabricDeps } from './log-fabric.js';
7
+ import { activeStoreNames, parseSinceParam, type LogFabricDeps, registeredLogStoreCount } from './log-fabric.js';
8
8
  import { checkAuth, type MetaApiOpts } from './meta-api.js';
9
9
  import { jsonResponse } from './json-response.js';
10
+ import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
10
11
 
11
12
  export type TraceLevel = 'L0' | 'L1' | 'L2' | 'L3';
12
13
  export interface TraceEvent {
@@ -201,12 +202,19 @@ export function buildRunTrace(
201
202
 
202
203
  export interface TraceDeps extends LogFabricDeps {
203
204
  queryLog?: (details: { stores: string[]; level: TraceLevel; count: number; truncated: boolean; mode: 'rows' | 'runs'; runs: number }) => void;
205
+ ledgerStat?: (path: string) => void;
204
206
  }
205
207
  function storeNames(url: URL, deps: TraceDeps): string[] {
206
208
  const names = [...new Set(url.searchParams.getAll('store').flatMap((v) => v.split(',')).map((v) => v.trim()).filter(Boolean))];
207
- return names.includes('@active') ? [...new Set([...activeStoreNames(deps), ...names.filter((n) => n !== '@active')])]
208
- : (names.length ? names : [url.searchParams.get('universe') || resolveLogInstanceName()]);
209
+ if (names.includes('@active')) {
210
+ let active: string[];
211
+ try { active = activeStoreNames(deps); }
212
+ catch { active = [resolveLogInstanceName()]; }
213
+ return [...new Set([...active, ...names.filter((n) => n !== '@active')])];
214
+ }
215
+ return names.length ? names : [url.searchParams.get('universe') || resolveLogInstanceName()];
209
216
  }
217
+ const registeredStoreCount = (deps: TraceDeps): number | null => registeredLogStoreCount(deps);
210
218
  function selectedStores(names: string[], deps: TraceDeps): { name: string; store: LogStore | null; remote: boolean; error?: string }[] {
211
219
  const selfName = resolveLogInstanceName();
212
220
  const self = names.includes(selfName) ? (deps.store ?? getDefaultLogStore)() : null;
@@ -266,6 +274,22 @@ function sortRows(a: LogStoreRow, b: LogStoreRow): number {
266
274
  return b.ts_ms - a.ts_ms || b.id - a.id || a.instance.localeCompare(b.instance);
267
275
  }
268
276
 
277
+ function findRunUniverse(runId: string, selected: readonly string[], deps: TraceDeps): { universe?: string; checked: number } {
278
+ let checked = 0;
279
+ const views = (deps.instances ?? readLogInstances)();
280
+ for (const view of [...views.filter((v) => selected.includes(v.name)), ...views.filter((v) => !selected.includes(v.name))]) {
281
+ checked++;
282
+ try {
283
+ (deps.ledgerStat ?? statSync)(runLedgerPath(runId, runLedgerDir(view.stateDir)));
284
+ return { universe: view.name, checked };
285
+ } catch (error) {
286
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue;
287
+ throw new Error(`unable to check run ledger for '${view.name}': ${error instanceof Error ? error.message : String(error)}`);
288
+ }
289
+ }
290
+ return { checked };
291
+ }
292
+
269
293
  /** Read-only bounded log-fabric fan-in, authenticated before any store access. */
270
294
  export function handleTrace(req: Request, opts: MetaApiOpts, deps: TraceDeps = {}): Response {
271
295
  if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
@@ -300,7 +324,7 @@ export function handleTrace(req: Request, opts: MetaApiOpts, deps: TraceDeps = {
300
324
  const trace = buildRunTrace(runs, parsed.level as 'L0' | 'L1');
301
325
  trace.truncated = runLimitReached || failures.length > 0;
302
326
  try { (deps.queryLog ?? ((details) => debug.log('logs.trace', 'query', details)))({ stores: names, level: parsed.level, count: trace.events.length, truncated: trace.truncated, mode: 'runs', runs: runs.length }); } catch { /* query logging must not block reads */ }
303
- return jsonResponse({ ok: true, ...trace, stores: names, ...(failures.length ? { failedStores: failures } : {}) }, 200);
327
+ return jsonResponse({ ok: true, ...trace, stores: names, registeredStores: registeredStoreCount(deps), ...(failures.length ? { failedStores: failures } : {}) }, 200);
304
328
  }
305
329
  const failures: Array<{ name: string; reason: string }> = [];
306
330
  const rows: LogStoreRow[] = [];
@@ -321,10 +345,37 @@ export function handleTrace(req: Request, opts: MetaApiOpts, deps: TraceDeps = {
321
345
  } catch (e) { failures.push({ name: entry.name, reason: e instanceof Error ? e.message : String(e) }); }
322
346
  }
323
347
  if (!opened) return jsonResponse({ ok: false, error: 'log-store-unavailable', failedStores: failures }, 503);
348
+ const runId = url.searchParams.get('runId');
349
+ let resolution: { universe?: string; checked: number } | undefined;
350
+ if ((parsed.level === 'L2' || parsed.level === 'L3') && runId && !rows.some((row) => matches(row, url)) && !requestedUniverse) {
351
+ // A store with no log db at all («has no log store yet» — e.g. the daemon's own tree-derived universe) cannot
352
+ // hold the run, so it must not block the ledger lookup. A store that exists but failed to open/read still does:
353
+ // «could not read» is never answered as «not there». The reply keeps `failedStores` either way.
354
+ const blocking = failures.filter((failure) => !/has no log store yet/.test(failure.reason));
355
+ if (blocking.length) return jsonResponse({ ok: false, error: 'log-store-unavailable', failedStores: failures }, 503);
356
+ try { resolution = findRunUniverse(runId, names, deps); }
357
+ catch (error) { return jsonResponse({ ok: false, error: 'run-universe-unavailable', reason: error instanceof Error ? error.message : String(error) }, 503); }
358
+ try { debug.log('nexus.trace', 'run-universe-resolved', { runId, universe: resolution.universe ?? 'not-found', checked: resolution.checked }); } catch { /* observation must not block reads */ }
359
+ if (resolution.universe && !names.includes(resolution.universe)) {
360
+ const name = resolution.universe;
361
+ names.push(name);
362
+ const [entry] = selectedStores([name], deps);
363
+ if (!entry?.store) return jsonResponse({ ok: false, error: 'log-store-unavailable', failedStores: [{ name, reason: entry?.error ?? 'log-store-unavailable' }] }, 503);
364
+ stores.push(entry);
365
+ try {
366
+ const query: LogQuery = { sinceMs: parsed.from, untilMs: parsed.to, limit: TRACE_LIMIT + 1 };
367
+ const search = url.searchParams.get('q');
368
+ if (search) query.grep = search;
369
+ const page = entry.store.queryTraceRun(runId, query);
370
+ if (page.length > TRACE_LIMIT) candidateLimitReached = true;
371
+ rows.push(...page.map((row) => ({ ...row, instance: row.instance || name })));
372
+ } catch (e) { return jsonResponse({ ok: false, error: 'log-store-unavailable', failedStores: [{ name, reason: e instanceof Error ? e.message : String(e) }] }, 503); }
373
+ }
374
+ }
324
375
  const filtered = rows.filter((row) => matches(row, url)).sort(sortRows);
325
376
  const trace = buildTrace(filtered.slice(0, parsed.limit), parsed.level, filtered.length > parsed.limit || candidateLimitReached || failures.length > 0);
326
377
  try { (deps.queryLog ?? ((details) => debug.log('logs.trace', 'query', details)))({ stores: names, level: parsed.level, count: trace.events.length, truncated: trace.truncated, mode: 'rows', runs: trace.nodes.filter((node) => node.level === 'L1' && node.count > 0).length }); } catch { /* query logging must not block reads */ }
327
- return jsonResponse({ ok: true, ...trace, stores: names, ...(failures.length ? { failedStores: failures } : {}) }, 200);
378
+ return jsonResponse({ ok: true, ...trace, stores: names, registeredStores: registeredStoreCount(deps), ...(resolution ? resolution.universe ? { resolvedFrom: 'ledger' as const, runUniverse: resolution.universe } : { runUniverse: 'not-found' as const, checked: resolution.checked } : {}), ...(failures.length ? { failedStores: failures } : {}) }, 200);
328
379
  } finally { closeStores(stores, deps); }
329
380
  }
330
381
 
@@ -28,13 +28,21 @@
28
28
  // daemon exit.
29
29
 
30
30
  import type { McpServerSpec } from '../../user-config.js';
31
- import { McpClient, McpConnectionError } from '../../mcp/client.js';
31
+ import { McpClient, McpConnectionError, type McpToolCallResult } from '../../mcp/client.js';
32
32
  import {
33
33
  createMcpProxyRuntime,
34
34
  createMcpToolAuthorizer,
35
35
  } from '../../mcp/proxy-runtime.js';
36
36
  import { registerToolRuntime, unregisterToolRuntime } from '../../tool-runtime/registry.js';
37
37
  import { debug, redactSecretText } from '../../debug/log.js';
38
+ import { elanousStateRoot } from '../../autopilot/state-paths.js';
39
+ import { spawn as nodeSpawn } from 'node:child_process';
40
+ import { listNodeKinds } from '../../graph-kinds/registry.js';
41
+ import { syncInstalledPluginNodes } from '../../graph-kinds/installed-plugin-nodes.js';
42
+ import { listInstalledPlugins } from '../../plugins/install/plugin-install.js';
43
+ import { pluginEnv, credentialStatus } from '../../plugins/install/plugin-credentials.js';
44
+ import { existsSync, realpathSync } from 'node:fs';
45
+ import { resolve, sep } from 'node:path';
38
46
 
39
47
  export interface McpClientsHandle {
40
48
  /** Live clients (one per successfully started server). */
@@ -172,24 +180,64 @@ export async function registerMcpClients(
172
180
  // breadcrumbs when a child (e.g. xcrun mcpbridge) deadlocked elanous's
173
181
  // boot. The trace category prefix `mcp.client.boot.*` keeps these
174
182
  // distinct from any per-call instrumentation a future PR adds.
183
+ const injectedValuesByServer = new Map<string, string[]>();
184
+ const redactPluginValues = (text: string): string => {
185
+ try {
186
+ const root = elanousStateRoot();
187
+ const current = [...new Set(listInstalledPlugins(root).map(item => item.name))]
188
+ .flatMap(name => Object.values(pluginEnv(name, root)));
189
+ return redactValues(text, [...current, ...injectedValuesByServer.values()].flat());
190
+ } catch { return '[REDACTED]'; }
191
+ };
175
192
  const bootLogger = (id: string) => (event: string, data?: Record<string, unknown>) => {
176
193
  try {
177
- debug.log('mcp.client.boot', event, { id, ...(data ?? {}) });
194
+ const safe = data ? Object.fromEntries(Object.entries(data).map(([key, value]) =>
195
+ [key, typeof value === 'string' ? redactPluginValues(value) : value])) : {};
196
+ debug.log('mcp.client.boot', event, { id, ...safe });
178
197
  } catch { /* logger must never throw the boot */ }
179
198
  };
180
199
  const createClient =
181
200
  opts.createClient ??
182
- ((spec: McpServerSpec) =>
183
- spec.transport === 'http'
184
- ? new McpClient({
185
- id: spec.id,
186
- url: spec.url,
187
- logger: bootLogger(spec.id),
188
- ...(spec.oauthIssuer ? { oauthIssuer: spec.oauthIssuer } : {}),
189
- ...(spec.oauthTokenEndpoint ? { oauthTokenEndpoint: spec.oauthTokenEndpoint } : {}),
190
- ...(spec.bearerTokenEnv ? { bearerTokenEnv: spec.bearerTokenEnv } : {}),
191
- })
192
- : new McpClient({ id: spec.id, command: spec.command, logger: bootLogger(spec.id) }));
201
+ ((spec: McpServerSpec) => {
202
+ if (spec.transport === 'http') return new McpClient({
203
+ id: spec.id, url: spec.url, logger: bootLogger(spec.id),
204
+ ...(spec.oauthIssuer ? { oauthIssuer: spec.oauthIssuer } : {}),
205
+ ...(spec.oauthTokenEndpoint ? { oauthTokenEndpoint: spec.oauthTokenEndpoint } : {}),
206
+ ...(spec.bearerTokenEnv ? { bearerTokenEnv: spec.bearerTokenEnv } : {}),
207
+ });
208
+ const root = elanousStateRoot();
209
+ const installed = listInstalledPlugins(root);
210
+ const owners = [...new Set(listNodeKinds('workflow').filter(entry =>
211
+ !entry.core && entry.plugin && entry.run && 'mcp' in entry.run && entry.run.mcp.server === spec.id,
212
+ ).map(entry => entry.plugin!))];
213
+ const commandOwners = [...new Set(installed.filter(item => {
214
+ const dir = realpathSync(item.path);
215
+ return spec.command.some(arg => {
216
+ if (typeof arg !== 'string' || !existsSync(arg)) return false;
217
+ try { return realpathSync(resolve(arg)).startsWith(`${dir}${sep}`); }
218
+ catch { return false; }
219
+ });
220
+ }).map(item => item.name))];
221
+ const selected = commandOwners.length === 1 && owners.length === 1 && owners[0] === commandOwners[0]
222
+ ? commandOwners[0] : undefined;
223
+ return new McpClient({ id: spec.id, command: spec.command, logger: bootLogger(spec.id),
224
+ spawn: (cmd, args) => {
225
+ const declaredEnv = [...new Set(listInstalledPlugins(root).map(item => item.name))]
226
+ .flatMap(name => credentialStatus(name, root).fields.map(field => field.env));
227
+ const childEnv = { ...process.env };
228
+ for (const key of declaredEnv) delete childEnv[key];
229
+ injectedValuesByServer.delete(spec.id);
230
+ if (selected) {
231
+ const env = pluginEnv(selected, root);
232
+ const fields = credentialStatus(selected, root).fields.filter(field => field.set).map(field => field.name);
233
+ debug.log('plugin.credentials', 'injected', { plugin: selected, fields, count: fields.length });
234
+ Object.assign(childEnv, env);
235
+ injectedValuesByServer.set(spec.id, Object.values(env));
236
+ }
237
+ return nodeSpawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'], env: childEnv });
238
+ },
239
+ });
240
+ });
193
241
  const handshakeTimeoutMs = opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
194
242
  const setTimeoutFn = opts.setTimeoutFn
195
243
  ?? ((cb: () => void, ms: number) => {
@@ -204,6 +252,11 @@ export async function registerMcpClients(
204
252
  const perServer: Record<string, McpServerBootResult> = {};
205
253
  let registered = 0;
206
254
 
255
+ if (opts.servers.some(spec => spec.transport === 'stdio') && !opts.createClient) {
256
+ try { syncInstalledPluginNodes(elanousStateRoot()); }
257
+ catch (error) { debug.log('graph.kinds', 'installed-sync-failed', { reason: String(error) }); }
258
+ }
259
+
207
260
  for (const spec of opts.servers) {
208
261
  const serverHandshakeTimeoutMs = spec.handshakeTimeoutMs ?? handshakeTimeoutMs;
209
262
  if (spec.enabled === false) {
@@ -213,6 +266,20 @@ export async function registerMcpClients(
213
266
  let client: McpClient | undefined;
214
267
  try {
215
268
  client = createClient(spec) as McpClient;
269
+ if (!opts.createClient && spec.transport === 'stdio') {
270
+ const originalCallTool = client.callTool.bind(client);
271
+ client.callTool = async (name: string, args: Record<string, unknown>): Promise<McpToolCallResult> => {
272
+ const values = injectedValuesByServer.get(spec.id) ?? [];
273
+ try {
274
+ const result = await originalCallTool(name, args);
275
+ return maskInjectedValues(result, values) as McpToolCallResult;
276
+ } catch (err) {
277
+ if (err instanceof Error) err.message = redactValues(err.message, values);
278
+ else throw new Error(redactValues(String(err), values));
279
+ throw err;
280
+ }
281
+ };
282
+ }
216
283
  await withTimeout(`mcp.${spec.id}.start`, () => client!.start(), serverHandshakeTimeoutMs, setTimeoutFn, clearTimeoutFn);
217
284
  const tools = await withTimeout(
218
285
  `mcp.${spec.id}.listTools`,
@@ -287,7 +354,7 @@ export async function registerMcpClients(
287
354
  }
288
355
  const msg = err instanceof Error ? err.message : String(err);
289
356
  const reasonClass = classifyMcpConnectFailure(err);
290
- const detail = redactSecretText(msg);
357
+ const detail = redactPluginValues(redactSecretText(msg));
291
358
  perServer[spec.id] = { status: 'failed', toolCount: 0, reason: detail, reasonClass };
292
359
  try {
293
360
  debug.log('mcp.client.boot', 'connect-failed', { id: spec.id, reason: reasonClass, detail });
@@ -303,6 +370,7 @@ export async function registerMcpClients(
303
370
  if (client) {
304
371
  try { await client.dispose(); } catch { /* best-effort */ }
305
372
  }
373
+ injectedValuesByServer.delete(spec.id);
306
374
  }
307
375
  }
308
376
 
@@ -328,6 +396,7 @@ export async function registerMcpClients(
328
396
  }),
329
397
  ),
330
398
  );
399
+ injectedValuesByServer.clear();
331
400
  },
332
401
  };
333
402
  }
@@ -338,3 +407,19 @@ function defaultLogger(): McpBootLogger {
338
407
  warn: (line) => console.warn(line),
339
408
  };
340
409
  }
410
+
411
+ function redactValues(text: string, values: readonly string[]): string {
412
+ return values.filter(value => value.length > 0)
413
+ .reduce((safe, value) => safe.replaceAll(value, '[REDACTED]'), text);
414
+ }
415
+
416
+ /** Mask injected credential values anywhere in an MCP result — string values **and object keys**
417
+ * (a server can echo a secret back as a key, e.g. `structuredContent: { [secret]: "ok" }`). */
418
+ export function maskInjectedValues(value: unknown, values: readonly string[]): unknown {
419
+ if (typeof value === 'string') return redactValues(value, values);
420
+ if (Array.isArray(value)) return value.map(item => maskInjectedValues(item, values));
421
+ if (value && typeof value === 'object') {
422
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [redactValues(key, values), maskInjectedValues(item, values)]));
423
+ }
424
+ return value;
425
+ }
@@ -79,6 +79,8 @@ import { type PtyBackend, createWebtermSpawn, resolveWebtermCwd } from './webter
79
79
  import { SettingsTabController } from './config/settings-controller.js';
80
80
  import { tryRegisterSettings } from './boot/register-settings.js';
81
81
  import { registerMcpClients, type McpClientsHandle } from './boot/register-mcp-clients.js';
82
+ import { syncInstalledPluginNodes } from '../graph-kinds/installed-plugin-nodes.js';
83
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
82
84
  import type { McpReloadOutcome, McpReloadServerResult } from './api/admin-mcp-reload.js';
83
85
  import { loadOrCreateSigningKey, NonceStore } from './api/edit-in-pwa-core.js';
84
86
  import { subscribeErrorSnapshotWriter } from './supervisor/error-snapshot.js';
@@ -238,9 +240,8 @@ import {
238
240
  type PwaVoiceAdapter,
239
241
  } from '../voice/channel-adapters/pwa-voice-adapter.js';
240
242
  import { getIntakeStore } from '../intake-plane/runtime.js';
241
- import { existsSync, readFileSync } from 'node:fs';
242
243
  import { join as joinPath } from 'node:path';
243
- import { getElanousConfigDir } from '../elanous-config-dir.js';
244
+ import { ensureAuthToken } from '../auth/acp-token.js';
244
245
  import type { SupervisorSpawnBackend } from './supervisor/spawn.js';
245
246
  import type { HealthProbeBackend } from './supervisor/health.js';
246
247
  import { gracefulExit, cleanExit } from './supervisor/graceful-exit.js';
@@ -264,8 +265,10 @@ import { createGlobalSubagentCallable } from '../agent/subagent-callable.js';
264
265
  import { setAgentHopCap } from '../agent/registry.js';
265
266
  import type { SidebarTabSurface } from '../ui/widgets/sidebar-tab-surface.js';
266
267
  import { defaultIO, runOnboardingStep, type WizardIO } from '../onboarding.js';
268
+ import { packageVersion } from '../version/code-revision.js';
267
269
 
268
- export const NEXUS_VERSION = '0.17.0';
270
+ // 데몬 배너·`nexus status`·연결 라벨이 보이는 판 = 제품 판(`package.json`) — 옛 내부 상수 `0.17.0` 이 0.2.x 제품 옆에 떴다(🅞 M5 · M5c).
271
+ export const NEXUS_VERSION = packageVersion();
269
272
  export const NEXUS_PHASE = 'N-5 PR χ (graceful exit + restart pending)';
270
273
 
271
274
  type TelegramAgentRunTurnFactory = typeof import('../telegram-agent.js').makeTelegramAgentRunTurn;
@@ -397,6 +400,8 @@ export interface RunNexusOptions {
397
400
  * registry stays empty so the dispatcher's webterm interception
398
401
  * is a no-op). */
399
402
  webtermSpawn?: (spec: { id: string; cwd?: string }) => PtyBackend;
403
+ /** Explicitly disable HTTP and ACP websocket token auth for this run. */
404
+ noAuth?: boolean;
400
405
  /** Skip the HTTP API server boot. Default: true in tests via
401
406
  * detachForTesting; production runs always start the server. Tests
402
407
  * that DO want the server can pass false explicitly. */
@@ -653,7 +658,7 @@ export interface RunNexusHandle {
653
658
  /** ACP WS auth gate for the NEXUS HTTP bridge.
654
659
  * Same shape as `src/boot/acp-server.ts` websocket auth: pass
655
660
  * `createAuthVerifier` only when a bearer token is configured;
656
- * omit the field entirely for first-boot tokenless dogfood.
661
+ * explicit no-auth opt-out omits the verifier.
657
662
  * ⛔ `noAuth` was declared on WsBridgeOpts but the bridge NEVER read it
658
663
  * (0 executable references) while this file assigned it — a value that
659
664
  * flowed in and died. We removed the field rather than leave a knob that
@@ -710,7 +715,7 @@ export function buildNexusWsBridgeAuth(
710
715
  bearerToken: string | undefined,
711
716
  paths?: TokenStorePaths,
712
717
  ): Pick<NexusWsBridgeInit, 'wsAuthVerifier'> {
713
- // ⛔ 첫 부팅 무인증 도그푸드는 그대로다 — 부팅 토큰이 «없으면» 검증기를 안 넘긴다.
718
+ // Explicit opt-out supplies no boot token, so the bridge has no verifier.
714
719
  if (!bearerToken) return {};
715
720
  return {
716
721
  // ⭐ 배열이 아니라 «공급자» — 매 핸드셰이크마다 봉투를 다시 읽는다.
@@ -764,6 +769,13 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
764
769
  }
765
770
 
766
771
  const leasePortFromLauncher = checkLauncherLeaseBoot(opts, process.env, () => resolveCurrentInstance().kind);
772
+ const noAuth = opts.noAuth === true || process.env.ELANOUS_NEXUS_NO_AUTH === '1';
773
+ // Resolve before acquiring the daemon lock or binding HTTP. A broken token
774
+ // store must fail closed without leaving a half-booted Nexus behind.
775
+ const httpBootEnabled = !(opts.skipHttpServer ?? !!opts.detachForTesting);
776
+ const needsToken = httpBootEnabled && !(opts.skipRuntimeApi ?? !!opts.detachForTesting) && !noAuth;
777
+ const bearerToken = needsToken ? ensureAuthToken().token : undefined;
778
+ if (needsToken && !bearerToken) throw new Error('Nexus auth token is empty');
767
779
 
768
780
  // 설치본 전환 RFC 0b — 키는 launchd plist 가 아니라 키 캐시에서(캐시 우선 · 자식도 물려받는다). 값은 안 찍고 «이름만».
769
781
  if (!opts.detachForTesting) {
@@ -973,6 +985,11 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
973
985
  });
974
986
  const startLevel = resolvedLevel.level;
975
987
  dbgMod.debug.setLevel(startLevel);
988
+ if (httpBootEnabled && noAuth) {
989
+ dbgMod.debug.log('nexus.auth', 'explicit-opt-out', {
990
+ source: opts.noAuth === true ? '--no-auth' : 'ELANOUS_NEXUS_NO_AUTH',
991
+ }, { level: 'warn' });
992
+ }
976
993
  dbgMod.debug.log('logging.level', 'startup-resolved', {
977
994
  level: startLevel, source: resolvedLevel.source,
978
995
  gateOpen: scopedMod.hotPathGateOpen(startLevel), surface: 'nexus',
@@ -1340,6 +1357,10 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
1340
1357
  current: () => false,
1341
1358
  };
1342
1359
 
1360
+ // Installed graph kinds do not depend on the runtime API or MCP server configuration.
1361
+ try { syncInstalledPluginNodes(elanousStateRoot()); }
1362
+ catch (error) { debug.log('graph.kinds', 'installed-sync-failed', { reason: String(error) }); }
1363
+
1343
1364
  if (!skipRuntime) {
1344
1365
  // Tool surface — flag · env · UserConfig switch · switch default.
1345
1366
  // See resolveToolsKind() jsdoc for resolution order.
@@ -2000,11 +2021,6 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
2000
2021
  let metaApiOpts: import('./api/meta-api.js').MetaApiOpts | undefined;
2001
2022
 
2002
2023
  if (!skipRuntime && runtimeHistory) {
2003
- // Bearer token — read from getElanousConfigDir()/acp-token (the
2004
- // canonical store `elanous serve` uses). Loopback default is
2005
- // noAuth; tests opt in by writing a token file before boot.
2006
- const bearerToken = readAcpToken();
2007
-
2008
2024
  // Voice REST + adapter. createVoiceRestHandler is stateless;
2009
2025
  // the STT provider singleton (getDaemonSttProvider) returns
2010
2026
  // null until `initDaemonSttProvider()` is called — fine, the
@@ -2081,9 +2097,8 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
2081
2097
  const surfaceForOpts = toolSurface(toolsKind);
2082
2098
  const toolCwdForOpts = resolveToolCwd({ tools: toolsKind, toolCwd: opts.toolCwd });
2083
2099
 
2084
- // Same shape as src/boot/acp-server.ts websocket auth: pass
2085
- // createAuthVerifier only when a token is present; omit it
2086
- // entirely for first-boot dogfood (tokenless ACP upgrade).
2100
+ // ACP websocket and REST share the same boot token; explicit
2101
+ // opt-out omits the websocket verifier as well.
2087
2102
  wsBridgeOpts = {
2088
2103
  ...(acpHandlerRef ? {
2089
2104
  acpOnConnection: async (conn) => {
@@ -2099,7 +2114,7 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
2099
2114
  metaApiOpts = {
2100
2115
  voiceRest,
2101
2116
  ...(bearerToken ? { bearerToken } : {}),
2102
- noAuth: !bearerToken,
2117
+ noAuth,
2103
2118
  history: runtimeHistory,
2104
2119
  intakeStore: getIntakeStore(),
2105
2120
  toolSurface: surfaceForOpts,
@@ -3048,7 +3063,7 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
3048
3063
  }
3049
3064
  runtime.httpPort = httpServer.port;
3050
3065
  runtime.httpHost = httpServer.hostname;
3051
- runtime.httpAuth = 'off';
3066
+ runtime.httpAuth = metaApiOpts && !metaApiOpts.noAuth ? 'on' : 'off';
3052
3067
  // The actual bound port (not the requested port or registry entry) is the member endpoint.
3053
3068
  try {
3054
3069
  const configured = (getUserConfig().raw?.nexus as { primary?: unknown } | undefined)?.primary;
@@ -3475,6 +3490,9 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
3475
3490
  }
3476
3491
 
3477
3492
  printBootBanner(runtime, pwaStaticDir);
3493
+ if (runtime.httpPort && noAuth) {
3494
+ console.warn(' ⚠ NEXUS AUTH DISABLED (--no-auth / ELANOUS_NEXUS_NO_AUTH=1) — HTTP API and ACP websocket are unprotected.');
3495
+ }
3478
3496
 
3479
3497
  // P.3 — first-boot PWA share wizard. Only runs in interactive TUI mode
3480
3498
  // when the switch is still 'ask'. Headless / non-TTY boots skip it (the
@@ -3784,23 +3802,6 @@ function shouldAskPwaShareSwitch(): boolean {
3784
3802
  }
3785
3803
  }
3786
3804
 
3787
- /** PR k — load the persistent ACP bearer token at
3788
- * `getElanousConfigDir()/acp-token`. Mirrors the helper in
3789
- * `boot/acp-server.ts` that `elanous serve` uses, so the same token
3790
- * gates daemon and NEXUS HTTP. Returns undefined when the token
3791
- * file is absent (loopback noAuth default — first-boot dogfood
3792
- * works without configuration). Read failures are swallowed. */
3793
- export function readAcpToken(): string | undefined {
3794
- try {
3795
- const tokenPath = joinPath(getElanousConfigDir(), 'acp-token');
3796
- if (existsSync(tokenPath)) {
3797
- const tok = readFileSync(tokenPath, 'utf-8').trim();
3798
- return tok || undefined;
3799
- }
3800
- } catch { /* swallow */ }
3801
- return undefined;
3802
- }
3803
-
3804
3805
  function printBootBanner(runtime: NexusRuntimeMeta, pwaStaticDir?: string): void {
3805
3806
  const lines = [
3806
3807
  '',
@@ -141,7 +141,7 @@ export function resolveChannels(
141
141
  async function deliverTelegram(cfg: UserConfig, msg: OutboundMsg, deps: RouterDeps): Promise<ChannelResult> {
142
142
  const fmt = formatForChannel('telegram', msg);
143
143
  const send = deps.telegramSend ?? sendTelegramReport;
144
- const ok = await send(cfg, fmt.text, { markdown: fmt.markdown ?? msg.markdown, fetchImpl: deps.fetchImpl });
144
+ const ok = await send(cfg, fmt.text, { markdown: fmt.markdown ?? msg.markdown, fetchImpl: deps.fetchImpl, kind: msg.kind });
145
145
  return ok ? { type: 'telegram', ok: true } : { type: 'telegram', ok: false, error: 'not-configured' };
146
146
  }
147
147
 
@@ -0,0 +1,119 @@
1
+ // NT1 — LLM 예산 알림 «모양» (UX · 대표 2026-09-30 22:30 재요청 · OP 결정 B6 ⊕ NT1).
2
+ // 숫자·정책은 TC 의 B6 로더가 채운다(`내부 문서 `RFC-llm-policy-single-file-2026-09-30``) — 이 파일은 문구만 정한다.
3
+ //
4
+ // 규칙 셋(대표 가 받은 알림이 어긴 것들):
5
+ // ① 한 알림 = 한 결정·한 행동 — 맨 윗줄이 «무엇이 바뀌었나», 마지막 줄이 «할 일».
6
+ // ② 바뀐 게 없으면 침묵 — 같은 상태(key)면 send=false.
7
+ // ③ 모순 금지 — «한도 참»과 «크레딧으로 진행 중»을 한 알림에 같이 쓰지 않는다. 상태 하나만 고른다.
8
+ // ⊕ 내부 낱말 0 — «자»·«판정»·계정 내부 이름 나열을 쓰지 않는다(계정은 «계정 N개»로 센다).
9
+
10
+ export interface LlmBudgetAccount {
11
+ name: string;
12
+ /** 구독 잔량 %(0~100). 모르면 undefined. */
13
+ subscriptionRemainingPct?: number;
14
+ /** 이 계정의 크레딧 잔액. */
15
+ credits?: number;
16
+ resetInHours?: number;
17
+ }
18
+
19
+ export interface LlmBudgetSnapshot {
20
+ /** ISO 시각(Asia/Seoul 로 날짜를 자른다). */
21
+ at: string;
22
+ credits: {
23
+ total: number;
24
+ usedToday: number;
25
+ /** 하루 목표 소진량(정책 `credits.pace`). */
26
+ paceTarget: number;
27
+ /** ISO 날짜 — 크레딧이 사라지는 날. */
28
+ expiresAt?: string;
29
+ /** 정책이 «크레딧 먼저»인가(`until` 전). */
30
+ useFirst?: boolean;
31
+ };
32
+ accounts: LlmBudgetAccount[];
33
+ selected: { account?: string; reason: 'subscription' | 'credits' | 'fallback' | 'none' };
34
+ fallback?: { provider: string; remainingPct?: number };
35
+ }
36
+
37
+ export interface LlmBudgetAlert {
38
+ send: boolean;
39
+ /** 상태 열쇠 — 다음 호출의 prevKey 로 넘긴다. */
40
+ key: string;
41
+ text: string;
42
+ }
43
+
44
+ const EXPIRY_WARN_DAYS = 7;
45
+ /** 이 시각(서울) 뒤에야 «오늘 페이스가 느리다»를 말한다 — 아침에 0 인 건 정상이다. */
46
+ const PACE_CHECK_HOUR = 18;
47
+
48
+ const n = (v: number) => Math.round(v).toLocaleString('ko-KR');
49
+
50
+ function seoulParts(iso: string): { date: string; hour: number } {
51
+ const d = new Date(iso);
52
+ const f = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hourCycle: 'h23' });
53
+ const p = Object.fromEntries(f.formatToParts(d).map((x) => [x.type, x.value]));
54
+ return { date: `${p.year}-${p.month}-${p.day}`, hour: Number(p.hour) };
55
+ }
56
+
57
+ function daysBetween(fromIso: string, toIso: string): number {
58
+ const a = Date.parse(seoulParts(fromIso).date);
59
+ const b = Date.parse(seoulParts(toIso).date);
60
+ return Math.round((b - a) / 86_400_000);
61
+ }
62
+
63
+ function mmdd(iso: string): string {
64
+ const { date } = seoulParts(iso);
65
+ return `${date.slice(5, 7)}-${date.slice(8, 10)}`;
66
+ }
67
+
68
+ function creditsLine(s: LlmBudgetSnapshot): string {
69
+ const parts = [`오늘 ${n(s.credits.usedToday)} / 목표 ${n(s.credits.paceTarget)}`, `남은 크레딧 ${n(s.credits.total)}`];
70
+ if (s.credits.expiresAt) parts.push(`${mmdd(s.credits.expiresAt)} 만료`);
71
+ return parts.join(' · ');
72
+ }
73
+
74
+ /** 지금 상태 하나를 고른다(위에서부터 먼저 맞는 것). 아무것도 아니면 침묵. */
75
+ export function buildLlmBudgetAlert(now: LlmBudgetSnapshot, prevKey?: string): LlmBudgetAlert {
76
+ const { date, hour } = seoulParts(now.at);
77
+ const accounts = now.accounts.length;
78
+ let key = 'ok';
79
+ let lines: string[] = [];
80
+
81
+ const expiryDays = now.credits.expiresAt ? daysBetween(now.at, now.credits.expiresAt) : undefined;
82
+
83
+ if (now.selected.reason === 'none') {
84
+ key = 'blocked';
85
+ lines = [
86
+ '⛔ LLM 을 쓸 곳이 없어 작업이 멈췄습니다.',
87
+ `구독 계정 ${accounts}개 한도 · 크레딧·대체 모델도 못 씁니다.`,
88
+ '할 일: 크레딧 사용을 허락하거나 대체 모델 키를 확인해 주세요.',
89
+ ];
90
+ } else if (now.selected.reason === 'fallback' && now.fallback) {
91
+ const soonest = now.accounts.map((a) => a.resetInHours).filter((h): h is number => typeof h === 'number').sort((a, b) => a - b)[0];
92
+ key = `fallback:${now.fallback.provider}`;
93
+ lines = [
94
+ `⚠️ codex 를 못 써서 ${now.fallback.provider} 로 일하고 있습니다${typeof now.fallback.remainingPct === 'number' ? `(남은 ${Math.round(now.fallback.remainingPct)}%)` : ''}.`,
95
+ soonest !== undefined ? `codex 는 ${Math.ceil(soonest)}시간 뒤 돌아옵니다.` : 'codex 가 언제 돌아오는지 아직 모릅니다.',
96
+ '할 일: 없음 — 참고만 하세요.',
97
+ ];
98
+ } else if (expiryDays !== undefined && expiryDays <= EXPIRY_WARN_DAYS && now.credits.total > 0) {
99
+ key = `expiry:${Math.max(0, expiryDays)}`;
100
+ lines = [
101
+ `💳 크레딧 ${n(now.credits.total)} 이 ${expiryDays <= 0 ? '오늘' : `${expiryDays}일 뒤`} 사라집니다.`,
102
+ creditsLine(now),
103
+ '할 일: 무거운 작업을 지금 맡기면 크레딧으로 돕니다.',
104
+ ];
105
+ } else if (now.credits.useFirst && now.credits.total > 0 && hour >= PACE_CHECK_HOUR && now.credits.usedToday < now.credits.paceTarget * 0.5) {
106
+ // 하루 한 번만 — 날짜를 열쇠에 넣는다.
107
+ key = `pace-behind:${date}`;
108
+ lines = [
109
+ now.selected.reason === 'credits'
110
+ ? '💳 크레딧을 먼저 쓰는 중인데 오늘 쓴 양이 목표의 절반이 안 됩니다.'
111
+ : '💳 크레딧을 먼저 쓰라는 방침인데 오늘은 구독 계정이 먼저 쓰였습니다.',
112
+ creditsLine(now),
113
+ now.selected.reason === 'credits' ? '할 일: 없음 — 맡긴 일이 적었던 날입니다.' : '할 일: 없음 — 기술 담당이 순서를 고치는 중입니다.',
114
+ ];
115
+ }
116
+
117
+ const send = key !== 'ok' && key !== prevKey;
118
+ return { send, key, text: lines.join('\n') };
119
+ }
package/src/onboarding.ts CHANGED
@@ -1193,6 +1193,27 @@ export function shouldRefuseInteractiveOnboarding(
1193
1193
  return o.ctx !== 'production' || !o.stdinIsTTY;
1194
1194
  }
1195
1195
 
1196
+ export class OnboardingRefusedError extends Error {
1197
+ readonly code = 'onboarding-refused' as const;
1198
+
1199
+ constructor(message: string, readonly reason: 'non-tty' | 'autonomous-empty-universe') {
1200
+ super(message);
1201
+ this.name = 'OnboardingRefusedError';
1202
+ }
1203
+ }
1204
+
1205
+ /** Handle only typed refusals; the agent setup hint applies to its non-TTY path alone. */
1206
+ export function handleOnboardingRefusal(error: unknown, entrance: 'agent' | 'other'): boolean {
1207
+ if (!(error instanceof OnboardingRefusedError) || error.code !== 'onboarding-refused') return false;
1208
+ if (entrance === 'agent' && error.reason === 'non-tty') {
1209
+ console.error('elanous agent needs a configured LLM. Run `elanous setup` in a terminal, or `elanous setup --non-interactive --config <path>` for unattended setup.');
1210
+ } else {
1211
+ console.error(error.message);
1212
+ }
1213
+ process.exitCode = 2;
1214
+ return true;
1215
+ }
1216
+
1196
1217
  function refuseInteractiveOnboardingIfNeeded(opts: RunWizardOpts, path: string): void {
1197
1218
  const ctx = getRunContext();
1198
1219
  const stdinIsTTY = !!input.isTTY;
@@ -1208,15 +1229,17 @@ function refuseInteractiveOnboardingIfNeeded(opts: RunWizardOpts, path: string):
1208
1229
  : '자율 컨텍스트에서 대화형 온보딩 요청 — 우주가 비어 있다(물질화 누락)',
1209
1230
  }, { level: 'error' });
1210
1231
  if (nonTTY) {
1211
- throw new Error(
1232
+ throw new OnboardingRefusedError(
1212
1233
  '대화형 온보딩은 stdin TTY가 있는 자리에서만 실행할 수 있다. '
1213
1234
  + '무인 설정은 `elanous setup --non-interactive --config <path>`를 사용하라.',
1235
+ 'non-tty',
1214
1236
  );
1215
1237
  }
1216
- throw new Error(
1238
+ throw new OnboardingRefusedError(
1217
1239
  `온보딩 마법사는 자율 컨텍스트(${ctx})에서 뜰 수 없다 — config 가 비어 있다(${path}). `
1218
1240
  + '자식 우주가 물질화되지 않았다는 뜻이다: `elanous config sync-test --state-dir <그 우주>` '
1219
1241
  + '로 깔거나, 스포너가 provisionDerivedUniverse 를 부르는지 확인하라.',
1242
+ 'autonomous-empty-universe',
1220
1243
  );
1221
1244
  }
1222
1245