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,16 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { generateAuthToken } from '../acp/transport/index.js';
4
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
5
+
6
+ /** Load the persistent ACP bearer token, creating it on first boot. */
7
+ export function ensureAuthToken(configDir: string = getElanousConfigDir()): { token: string; path: string } {
8
+ const path = join(configDir, 'acp-token');
9
+ mkdirSync(configDir, { recursive: true });
10
+ if (existsSync(path)) {
11
+ return { token: readFileSync(path, 'utf-8').trim(), path };
12
+ }
13
+ const token = generateAuthToken();
14
+ writeFileSync(path, token, { mode: 0o600 });
15
+ return { token, path };
16
+ }
@@ -0,0 +1,112 @@
1
+ import { createHash, createSign } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { dirname, isAbsolute, join } from 'node:path';
5
+ import { getElanousConfigDir, getElanousConfigDirOverride } from '../elanous-config-dir.js';
6
+ import { prodInstanceRoot } from '../instance/resolve.js';
7
+ import { debug } from '../debug/log.js';
8
+
9
+ interface AppConfig { id: number; installation_id: number; pem: string }
10
+ interface TokenResponse { token?: unknown; expires_at?: unknown }
11
+ export interface GithubInstallationTokenScope { repository: string }
12
+ interface InstallationTokenRequest {
13
+ repositories: [string];
14
+ permissions: { contents: 'write'; pull_requests: 'write' };
15
+ }
16
+
17
+ /** Synchronous HTTP seam: CmdRunner must remain synchronous for existing callers. */
18
+ export interface GithubAutomationTokenDeps {
19
+ configPath?: string;
20
+ now?: () => number;
21
+ scope?: GithubInstallationTokenScope;
22
+ fetch?: (url: string, jwt: string, body?: InstallationTokenRequest) => TokenResponse;
23
+ }
24
+
25
+ const REFRESH_MARGIN_MS = 10 * 60_000;
26
+ const JWT_LIFETIME_SECONDS = 9 * 60;
27
+ let cached: { fingerprint: string; token: string; expiresAt: number } | undefined;
28
+
29
+ function requestToken(url: string, jwt: string, body?: InstallationTokenRequest): TokenResponse {
30
+ const response = spawnSync('curl', [
31
+ '--silent', '--show-error', '--fail', '--max-time', '30', '--request', 'POST',
32
+ '--header', 'Accept: application/vnd.github+json',
33
+ '--header', '@-',
34
+ '--header', 'X-GitHub-Api-Version: 2022-11-28',
35
+ ...(body ? ['--header', 'Content-Type: application/json', '--data-raw', JSON.stringify(body)] : []),
36
+ url,
37
+ ], { encoding: 'utf8', timeout: 35_000, input: `Authorization: Bearer ${jwt}\n`, env: process.env });
38
+ // Never expose curl's stderr: it may contain request credentials.
39
+ if (response.error || response.status !== 0) throw new Error('http-failed');
40
+ return JSON.parse(response.stdout) as TokenResponse;
41
+ }
42
+
43
+ /** Missing App config is a no-op; an unusable configured App falls back to the caller's gh credentials. */
44
+ export function githubAutomationToken(deps: GithubAutomationTokenDeps = {}): string | null {
45
+ return githubInstallationCredential(deps)?.token ?? null;
46
+ }
47
+
48
+ /** Scoped requests always mint anew; the returned expiry comes from GitHub's response. */
49
+ export function githubInstallationCredential(deps: GithubAutomationTokenDeps = {}): { token: string; expires_at: string } | null {
50
+ let path: string;
51
+ // The App credential is machine-level (like `gh auth` in ~/.config/gh), not per universe: a tree-derived test
52
+ // universe resolves its config dir to `<tree>/.elanous-test`, where the file never is — 09-29 #21879 was inert there.
53
+ // Order: explicit path → this universe's config dir → the prod root `~/.elanous`.
54
+ try {
55
+ const rel = join('secrets', 'github-app', 'app.json');
56
+ const own = join(getElanousConfigDir(), rel);
57
+ // An explicit config-dir override (tests · `--config-dir`) is a deliberate boundary — never reach past it.
58
+ path = deps.configPath ?? (existsSync(own) || getElanousConfigDirOverride() ? own : join(prodInstanceRoot(), rel));
59
+ }
60
+ catch {
61
+ try { debug.log('auth.github-app', 'token-failed', { reason: 'config-path-failed' }); } catch { /* fail open */ }
62
+ return null;
63
+ }
64
+ let configText: string;
65
+ try { configText = readFileSync(path, 'utf8'); }
66
+ catch (error) {
67
+ cached = undefined;
68
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
69
+ try { debug.log('auth.github-app', 'token-failed', { reason: 'config-read-failed' }); } catch { /* fail open */ }
70
+ return null;
71
+ }
72
+ try {
73
+ const now = (deps.now ?? Date.now)();
74
+ const config = JSON.parse(configText) as AppConfig;
75
+ if (!config || !Number.isSafeInteger(config.id) || config.id <= 0 ||
76
+ !Number.isSafeInteger(config.installation_id) || config.installation_id <= 0 ||
77
+ typeof config.pem !== 'string' || !config.pem) throw new Error('invalid-config');
78
+ const pem = isAbsolute(config.pem) ? readFileSync(config.pem, 'utf8')
79
+ : config.pem.includes('-----BEGIN') ? config.pem
80
+ : readFileSync(join(dirname(path), config.pem), 'utf8');
81
+ const repository = deps.scope?.repository;
82
+ if (deps.scope && (typeof repository !== 'string' || !/^[a-zA-Z0-9_.-]+$/.test(repository) || repository === '.' || repository === '..')) {
83
+ throw new Error('invalid-repository');
84
+ }
85
+ const request: InstallationTokenRequest | undefined = repository
86
+ ? { repositories: [repository], permissions: { contents: 'write', pull_requests: 'write' } }
87
+ : undefined;
88
+ const fingerprint = createHash('sha256').update(JSON.stringify([path, config.id, config.installation_id, pem])).digest('hex');
89
+ if (!request && cached?.fingerprint === fingerprint && now < cached.expiresAt - REFRESH_MARGIN_MS) return { token: cached.token, expires_at: new Date(cached.expiresAt).toISOString() };
90
+ if (!request) cached = undefined;
91
+ const issuedAt = Math.floor(now / 1000) - 60;
92
+ const encode = (value: object) => Buffer.from(JSON.stringify(value)).toString('base64url');
93
+ const unsigned = `${encode({ alg: 'RS256', typ: 'JWT' })}.${encode({ iat: issuedAt, exp: issuedAt + JWT_LIFETIME_SECONDS, iss: String(config.id) })}`;
94
+ const signer = createSign('RSA-SHA256');
95
+ signer.update(unsigned);
96
+ const jwt = `${unsigned}.${signer.sign(pem, 'base64url')}`;
97
+ const url = `https://api.github.com/app/installations/${config.installation_id}/access_tokens`;
98
+ const response = (deps.fetch ?? requestToken)(url, jwt, request);
99
+ const expiresAt = typeof response.expires_at === 'string' ? Date.parse(response.expires_at) : NaN;
100
+ if (typeof response.token !== 'string' || !response.token || !Number.isFinite(expiresAt) ||
101
+ expiresAt <= now + REFRESH_MARGIN_MS) throw new Error('invalid-response');
102
+ if (!request) cached = { fingerprint, token: response.token, expiresAt };
103
+ try { debug.log('auth.github-app', 'token-minted', { installationId: config.installation_id, expiresAt: new Date(expiresAt).toISOString() }); }
104
+ catch { /* logging must not undo a valid token */ }
105
+ return { token: response.token, expires_at: response.expires_at as string };
106
+ } catch {
107
+ if (!deps.scope) cached = undefined;
108
+ // Do not log exceptions: crypto/HTTP failures can include key or bearer material.
109
+ try { debug.log('auth.github-app', 'token-failed', { reason: 'mint-failed' }); } catch { /* fail open */ }
110
+ return null;
111
+ }
112
+ }
@@ -0,0 +1,177 @@
1
+ import { debug } from '../debug/log.js';
2
+ import type { CmdResult, CmdRunner } from './pr-manager.js';
3
+
4
+ type Pull = Record<string, unknown>;
5
+
6
+ function flag(args: readonly string[], name: string): string | undefined {
7
+ const index = args.indexOf(name);
8
+ return index >= 0 ? args[index + 1] : undefined;
9
+ }
10
+
11
+ function hasOnlyFlags(args: readonly string[], start: number, valued: readonly string[], bare: readonly string[] = []): boolean {
12
+ for (let i = start; i < args.length; i++) {
13
+ if (bare.includes(args[i]!)) continue;
14
+ if (!valued.includes(args[i]!) || args[++i] === undefined) return false;
15
+ }
16
+ return true;
17
+ }
18
+
19
+ function repoFromOrigin(run: CmdRunner, opts?: { cwd?: string }): { owner: string; repo: string } | null {
20
+ const remote = run('git', ['remote', 'get-url', 'origin'], opts);
21
+ if (!remote.ok) return null;
22
+ const value = remote.out.trim();
23
+ let path: string;
24
+ try {
25
+ const url = new URL(value);
26
+ if (!['https:', 'ssh:'].includes(url.protocol) || url.hostname.toLowerCase() !== 'github.com') return null;
27
+ path = url.pathname;
28
+ } catch {
29
+ const scp = /^(?:[^@/]+@)?github\.com:([^\s]+)$/.exec(value);
30
+ if (!scp) return null;
31
+ path = scp[1]!;
32
+ }
33
+ const match = /^\/?([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/.exec(path);
34
+ return match ? { owner: match[1]!, repo: match[2]! } : null;
35
+ }
36
+
37
+ function prNumber(value: string, owner: string, repo: string): string | null {
38
+ if (/^[1-9]\d*$/.test(value)) return value;
39
+ const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/([1-9]\d*)\/?$/.exec(value);
40
+ return match && match[1] === owner && match[2] === repo ? match[3]! : null;
41
+ }
42
+
43
+ function readPull(result: CmdResult): Pull | null {
44
+ if (!result.ok) return null;
45
+ try {
46
+ const value: unknown = JSON.parse(result.out);
47
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Pull : null;
48
+ } catch { return null; }
49
+ }
50
+
51
+ function pullField(pull: Pull, field: string): unknown {
52
+ switch (field) {
53
+ case 'state': return pull.merged_at ? 'MERGED' : pull.state === 'open' ? 'OPEN' : pull.state === 'closed' && pull.merged_at === null ? 'CLOSED' : undefined;
54
+ case 'isDraft': return pull.draft;
55
+ case 'baseRefName': return (pull.base as Pull | undefined)?.ref;
56
+ case 'headRefName': return (pull.head as Pull | undefined)?.ref;
57
+ case 'headRepository': {
58
+ const nameWithOwner = ((pull.head as Pull | undefined)?.repo as Pull | undefined)?.full_name;
59
+ return typeof nameWithOwner === 'string' ? { nameWithOwner } : undefined;
60
+ }
61
+ case 'mergeable': return pull.mergeable === true ? 'MERGEABLE' : pull.mergeable === false ? 'CONFLICTING' : pull.mergeable === null ? 'UNKNOWN' : undefined;
62
+ case 'mergeStateStatus': {
63
+ const state = pull.mergeable_state;
64
+ return typeof state === 'string' && ['behind', 'blocked', 'clean', 'dirty', 'draft', 'has_hooks', 'unknown', 'unstable'].includes(state)
65
+ ? state.toUpperCase() : undefined;
66
+ }
67
+ default: return undefined;
68
+ }
69
+ }
70
+
71
+ function restCall(run: CmdRunner, args: string[], opts?: { cwd?: string }): CmdResult {
72
+ return run('gh', ['api', ...args], opts);
73
+ }
74
+
75
+ /** Retry exactly once via REST after the secondary GraphQL limit; unsupported shapes fail closed. */
76
+ export function withGhRestFallback(run: CmdRunner): CmdRunner {
77
+ return (cmd, args, opts) => {
78
+ const original = run(cmd, args, opts);
79
+ if (cmd !== 'gh' || args[0] !== 'pr' || original.ok ||
80
+ !/GraphQL/i.test(`${original.out}\n${original.err ?? ''}`) ||
81
+ !/API rate limit/i.test(`${original.out}\n${original.err ?? ''}`)) return original;
82
+ const sub = args[1];
83
+ if (!['list', 'view', 'create', 'edit', 'merge', 'comment'].includes(sub ?? '')) return original;
84
+ if (sub === 'list' && !args.includes('--json')) return original;
85
+ const location = repoFromOrigin(run, opts);
86
+ if (!location) return original;
87
+ const { owner, repo } = location;
88
+ const path = `repos/${owner}/${repo}`;
89
+ let result: CmdResult | null = null;
90
+
91
+ if (sub === 'list' && args[2] === '--head' && args[3] && args[4] === '--state' && args[5] === 'open' &&
92
+ args[6] === '--json' && args[7] === 'url' &&
93
+ (args.length === 8 || (args.length === 10 && args[8] === '--jq' && args[9] === '.[0].url // ""'))) {
94
+ const listed = restCall(run, [`${path}/pulls?head=${encodeURIComponent(`${owner}:${args[3]}`)}&state=open`], opts);
95
+ if (listed.ok) {
96
+ try {
97
+ const items: unknown = JSON.parse(listed.out);
98
+ if (Array.isArray(items) && items.every((item) => item && typeof item.html_url === 'string')) {
99
+ const urls = items.map((item) => ({ url: item.html_url as string }));
100
+ result = { ok: true, out: args[8] === '--jq' ? (urls[0]?.url ?? '') : JSON.stringify(urls) };
101
+ }
102
+ } catch { /* malformed REST response */ }
103
+ } else result = listed;
104
+ } else if (sub === 'view' && args[2] && args[3] === '--json' && args[4] &&
105
+ hasOnlyFlags(args, 5, ['--jq', '-q'])) {
106
+ const number = prNumber(args[2], owner, repo);
107
+ const fields = args[4].split(',');
108
+ const jq = flag(args, '--jq') ?? flag(args, '-q');
109
+ if (number && fields.every((field) => ['state', 'isDraft', 'mergeable', 'baseRefName', 'mergeStateStatus', 'headRefName', 'headRepository'].includes(field)) &&
110
+ (!jq || (fields.length === 1 && jq === `.${fields[0]}`))) {
111
+ const response = restCall(run, [`${path}/pulls/${number}`], opts);
112
+ const pull = readPull(response);
113
+ if (pull) {
114
+ const mapped = Object.fromEntries(fields.map((field) => [field, pullField(pull, field)]));
115
+ if (Object.values(mapped).every((value) => value !== undefined)) {
116
+ const selected = mapped[fields[0]!];
117
+ result = { ok: true, out: jq
118
+ ? typeof selected === 'string' ? selected : JSON.stringify(selected)
119
+ : JSON.stringify(mapped) };
120
+ }
121
+ } else if (!response.ok) result = response;
122
+ }
123
+ } else if (sub === 'create' && hasOnlyFlags(args, 2, ['--head', '--title', '--body', '--base'], ['--draft']) &&
124
+ flag(args, '--head') && flag(args, '--title') !== undefined && flag(args, '--body') !== undefined) {
125
+ let base = flag(args, '--base');
126
+ if (!base) {
127
+ const repository = restCall(run, [path], opts);
128
+ if (!repository.ok) result = repository;
129
+ else {
130
+ const defaultBranch = readPull(repository)?.default_branch;
131
+ if (typeof defaultBranch === 'string' && defaultBranch) base = defaultBranch;
132
+ }
133
+ }
134
+ if (base) {
135
+ const parameters = ['-f', `head=${flag(args, '--head')}`, '-f', `title=${flag(args, '--title')}`, '-f', `body=${flag(args, '--body')}`, '-f', `base=${base}`];
136
+ if (args.includes('--draft')) parameters.push('-F', 'draft=true');
137
+ const response = restCall(run, ['--method', 'POST', `${path}/pulls`, ...parameters], opts);
138
+ const pull = readPull(response);
139
+ if (pull && typeof pull.html_url === 'string') result = { ok: true, out: pull.html_url };
140
+ else if (!response.ok) result = response;
141
+ }
142
+ } else if (sub === 'edit' && args[2] && hasOnlyFlags(args, 3, ['--title', '--body', '--base']) &&
143
+ flag(args, '--title') !== undefined && flag(args, '--body') !== undefined) {
144
+ const number = prNumber(args[2], owner, repo);
145
+ if (number) {
146
+ const parameters = ['-f', `title=${flag(args, '--title')}`, '-f', `body=${flag(args, '--body')}`];
147
+ if (flag(args, '--base')) parameters.push('-f', `base=${flag(args, '--base')}`);
148
+ const response = restCall(run, ['--method', 'PATCH', `${path}/pulls/${number}`, ...parameters], opts);
149
+ if (readPull(response)) result = { ok: true, out: '' };
150
+ else if (!response.ok) result = response;
151
+ }
152
+ } else if (sub === 'merge' && args[2] && args[3] === '--squash' && args.length === 4) {
153
+ const number = prNumber(args[2], owner, repo);
154
+ if (number) {
155
+ const head = restCall(run, [`${path}/pulls/${number}`], opts);
156
+ const sha = (readPull(head)?.head as Pull | undefined)?.sha;
157
+ if (typeof sha === 'string' && /^[a-f0-9]{40}$/.test(sha)) {
158
+ const response = restCall(run, ['--method', 'PUT', `${path}/pulls/${number}/merge`, '-f', 'merge_method=squash', '-f', `sha=${sha}`], opts);
159
+ const body = readPull(response);
160
+ if (body?.merged === true) result = { ok: true, out: '' };
161
+ else if (!response.ok) result = response;
162
+ } else if (!head.ok) result = head;
163
+ }
164
+ } else if (sub === 'comment' && args[2] && args[3] === '--body' && args[4] !== undefined && args.length === 5) {
165
+ const number = prNumber(args[2], owner, repo);
166
+ if (number) {
167
+ const response = restCall(run, ['--method', 'POST', `${path}/issues/${number}/comments`, '-f', `body=${args[4]}`], opts);
168
+ if (readPull(response)) result = { ok: true, out: '' };
169
+ else if (!response.ok) result = response;
170
+ }
171
+ }
172
+ if (!result) return original;
173
+ debug.log('autopilot.pr', 'rest-fallback', { sub, ok: result.ok });
174
+ process.stderr.write(`[gh] REST 폴백: pr ${sub}\n`);
175
+ return result;
176
+ };
177
+ }
@@ -13,20 +13,36 @@ import { getUserConfig } from '../user-config.js';
13
13
  import { resolveChannelBotToken } from '../channel-bot-token.js';
14
14
  import { join } from 'node:path';
15
15
 
16
- export interface MissionOrigin {
17
- channel: 'telegram' | 'pwa' | 'voice' | 'cli' | 'api' | 'tui';
16
+ interface MissionOriginBase {
18
17
  /** 텔레그램: 답장할 chat id. */
19
18
  chatId?: number;
20
19
  /** 텔레그램: 수신 봇 식별(botId=토큰 prefix). 발송 시 config 에서 토큰 해석. */
21
20
  botId?: string;
22
- /** 텔레그램 스레드(포럼) — 있으면 답장에 붙임. */
23
- threadId?: number;
24
21
  /** HITL 승인/거절 버튼을 단 알림 메시지의 message_id. 크로스서피스 싱크용 —
25
22
  * PWA/텔레그램 어느 쪽에서 해소돼도 이 좌표(chatId+botId+이 id)로 텔레그램 메시지를
26
23
  * edit("승인됨/거절됨")하고 버튼을 제거한다. 버튼 발송 성공 시에만 기록. */
27
24
  hitlMessageId?: number;
28
25
  }
29
26
 
27
+ export type MissionOrigin = MissionOriginBase & (
28
+ | {
29
+ channel: 'telegram' | 'pwa' | 'voice' | 'cli' | 'api' | 'tui';
30
+ /** 텔레그램 스레드(포럼) — 있으면 답장에 붙임. */
31
+ threadId?: number;
32
+ channelId?: never;
33
+ }
34
+ | {
35
+ channel: 'discord';
36
+ /** Discord origin channel (snowflake). */
37
+ channelId: string;
38
+ /** Discord thread (snowflake), if the request came from a thread.
39
+ * ⛔ `threadId` 가 아니다 — 텔레그램 `threadId?: number` 와 같은 이름이면 유니온에서 `string|number` 로 넓어져
40
+ * 텔레그램 발송부 20여 곳의 타입이 깨진다(2026-10-01 PWA 빌드 실패 · #22246). */
41
+ discordThreadId?: string;
42
+ threadId?: never;
43
+ }
44
+ );
45
+
30
46
  function originDir(): string {
31
47
  // core 미션 fabric = autopilot/ (conatus/ 는 투자 customer 네임스페이스·대표 정정 2026-07-11).
32
48
  // [ISO-3] ELANOUS_STATE_DIR 존중 — 격리 테스트 데몬은 운영 origin 을 못 본다.
@@ -1,7 +1,17 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
- type TrackEntry = { id: string; mark: string; owns: string[] };
4
+ type TrackEntry = { id: string; mark: string; owns: string[]; alias?: string };
5
+
6
+ // Seat ids became two letters on 09-30 (#22085) with the old one-letter id kept as `alias`.
7
+ // A track named either way is the same seat — posts and inputs still carry the old letter during the switch.
8
+ export function findTrack(idOrAlias: string, registry: TrackEntry[] = loadTrackRegistry()): TrackEntry | undefined {
9
+ return registry.find((track) => track.id === idOrAlias || track.alias === idOrAlias);
10
+ }
11
+ export function trackPostIds(idOrAlias: string, registry: TrackEntry[] = loadTrackRegistry()): string[] {
12
+ const track = findTrack(idOrAlias, registry);
13
+ return [...new Set([idOrAlias, ...(track ? [track.id, ...(track.alias ? [track.alias] : [])] : [])])];
14
+ }
5
15
 
6
16
  export function loadTrackRegistry(path = resolve(import.meta.dir, '../../scripts/coord-tracks.json')): TrackEntry[] {
7
17
  try {
@@ -9,11 +9,14 @@
9
9
  // 구현을 교체(누적 아님). 기존 PR 없으면 새로 생성.
10
10
 
11
11
  import { spawnSync } from 'node:child_process';
12
+ import { githubAutomationToken } from '../auth/github-app-token.js';
12
13
  import { debug } from '../debug/log.js';
14
+ import { POD_GITHUB_CREDENTIAL_TOKEN_ENV, POD_GITHUB_CREDENTIAL_URL_ENV } from '../nexus/api/pod-credential-api.js';
13
15
  import { runGitWithRetry } from '../git-fs/retry.js';
14
16
  import { DEFAULT_BRANCH_WORKTREE_BASE } from '../git-fs/worktree.js';
15
17
  import { isElanousRuntimeArtifactPath } from '../self-implement/gate-scope.js';
16
18
  import { isTransientExecutionFailure } from '../self-dev/execution-transient.js';
19
+ import { withGhRestFallback } from './gh-rest-fallback.js';
17
20
 
18
21
  /** ★ stderr 캡처(대표 2026-07-21·관측 갭) — 종전엔 stdout 만 담아 git/gh 실패 사유가 소실됐다
19
22
  * (705308: makePr 실패가 bare null 로 뭉뚱그려져 triage 가 "인증·네트워크"로 환각 오라우팅).
@@ -22,12 +25,55 @@ export interface CmdResult { ok: boolean; out: string; err?: string }
22
25
  /** 명령 실행 추상화(주입형·테스트 스텁) — git/gh 를 실제로 돌리지 않고 로직 검증. */
23
26
  export type CmdRunner = (cmd: string, args: readonly string[], opts?: { cwd?: string }) => CmdResult;
24
27
 
28
+ const GH_TOKEN_REFRESH_WINDOW_MS = 10 * 60_000;
29
+ let cachedRelayToken: { url: string; bearer: string; original: string | undefined; token: string; expiresAt: number } | undefined;
30
+
31
+ /** The runner is synchronous; ask the host relay synchronously before starting gh. */
32
+ function relayGhToken(env: NodeJS.ProcessEnv): string | null {
33
+ const url = env[POD_GITHUB_CREDENTIAL_URL_ENV];
34
+ const bearer = env[POD_GITHUB_CREDENTIAL_TOKEN_ENV];
35
+ if (!url || !bearer) return null;
36
+ const now = Date.now();
37
+ const original = env.GH_TOKEN;
38
+ if (cachedRelayToken?.url === url && cachedRelayToken.bearer === bearer && cachedRelayToken.original === original
39
+ && now + GH_TOKEN_REFRESH_WINDOW_MS < cachedRelayToken.expiresAt) return cachedRelayToken.token;
40
+ try {
41
+ const response = spawnSync('curl', [
42
+ '--silent', '--show-error', '--fail', '--noproxy', '*', '--max-time', '10',
43
+ '--request', 'POST', '--header', '@-', url,
44
+ ], { input: `Authorization: Bearer ${bearer}\n`, encoding: 'utf8', timeout: 12_000, maxBuffer: 64 * 1024, env });
45
+ if (response.error || response.status !== 0) throw new Error('request-failed');
46
+ const body: unknown = JSON.parse(response.stdout);
47
+ const token = body && typeof body === 'object' ? (body as { token?: unknown }).token : undefined;
48
+ const expiry = body && typeof body === 'object' ? (body as { expires_at?: unknown }).expires_at : undefined;
49
+ const expiresAt = typeof expiry === 'string' ? Date.parse(expiry) : NaN;
50
+ if (typeof token !== 'string' || !token || !Number.isFinite(expiresAt) || expiresAt <= now + GH_TOKEN_REFRESH_WINDOW_MS) throw new Error('invalid-response');
51
+ cachedRelayToken = { url, bearer, original, token, expiresAt };
52
+ return token;
53
+ } catch {
54
+ debug.log('pr-manager', 'gh-token', { reason: 'refresh-failed' }, { level: 'warn' });
55
+ return cachedRelayToken?.url === url && cachedRelayToken.bearer === bearer && cachedRelayToken.original === original
56
+ && now < cachedRelayToken.expiresAt ? cachedRelayToken.token : null;
57
+ }
58
+ }
59
+
60
+ /** Environment for a `gh` call made by automation: the Pod relay token, else the GitHub App token when the
61
+ * caller set no GH_TOKEN — so PRs, merges and PR comments all act as the App, not the person logged into `gh`. */
62
+ export function ghAutomationEnv(env: NodeJS.ProcessEnv = process.env, appToken: () => string | null = githubAutomationToken): NodeJS.ProcessEnv {
63
+ const token = relayGhToken(env) ?? (!Object.hasOwn(env, 'GH_TOKEN') ? appToken() : null);
64
+ return token ? { ...env, GH_TOKEN: token } : env;
65
+ }
66
+
25
67
  /** 기본 러너 — spawnSync(dep-free). 실패/예외는 ok=false. stdout·stderr 모두 캡처. */
26
68
  export const defaultCmdRunner: CmdRunner = (cmd, args, opts) => {
27
69
  try {
28
- // env: process.env — 최소 PATH(cron)에서도 ensure-bin-path 보강 PATH 로 gh 를 찾도록 명시 전달.
29
- const r = spawnSync(cmd, [...args], { encoding: 'utf-8', timeout: 120_000, env: process.env, ...(opts?.cwd ? { cwd: opts.cwd } : {}) });
30
- return { ok: r.status === 0, out: (r.stdout ?? '').trim(), err: (r.stderr ?? '').trim() };
70
+ // Relay only applies to gh; without it, retain caller GH_TOKEN and the existing App fallback.
71
+ const env = cmd === 'gh' ? ghAutomationEnv(process.env) : process.env;
72
+ // bun ≥1.4 applies Node's 1MB default maxBuffer and silently truncates (`gh pr diff`, file lists) — raise it and
73
+ // never read an overflow as success.
74
+ const r = spawnSync(cmd, [...args], { encoding: 'utf-8', timeout: 120_000, maxBuffer: 256 * 1024 * 1024, env, ...(opts?.cwd ? { cwd: opts.cwd } : {}) });
75
+ const overflow = (r.error as NodeJS.ErrnoException | undefined)?.code === 'ENOBUFS';
76
+ return { ok: r.status === 0 && !overflow, out: (r.stdout ?? '').trim(), err: [(r.stderr ?? '').trim(), overflow ? 'output exceeded 256MiB (ENOBUFS)' : ''].filter(Boolean).join('\n') };
31
77
  } catch (e) { return { ok: false, out: '', err: e instanceof Error ? e.message : String(e) }; }
32
78
  };
33
79
 
@@ -259,7 +305,8 @@ function isTransientPushFailure(detail: string): boolean {
259
305
  }
260
306
 
261
307
  /** ★ PR 매니저 팩토리(대표 2026-07-12) — CmdRunner 주입(테스트 스텁). 실 배선은 기본 러너. */
262
- export function makePrManager(run: CmdRunner = defaultCmdRunner): PrManager {
308
+ export function makePrManager(runner: CmdRunner = defaultCmdRunner): PrManager {
309
+ const run = withGhRestFallback(runner);
263
310
  const findPrForBranchOutcome = (branch: string, cwd?: string): FindPrForBranchOutcome => {
264
311
  // ⛔ 표식은 base 를 「생략했다」는 뜻이지 브랜치 이름이 아니다. 그대로 `gh --head` 로 넘기면
265
312
  // gh 가 `no pull requests found for branch "elanous:default-branch"` 를 stderr 로 뱉는다.
@@ -6,7 +6,7 @@ import { debug, redactSecretText } from '../debug/log.js';
6
6
  import { getOrCreatePersonaSession } from '../session/index.js';
7
7
  import { getUserConfig } from '../user-config.js';
8
8
  import { decideBudget, readBudgetInputsLive } from '../self-implement/budget-gate.js';
9
- import { loadTrackRegistry } from './mission-phase-track.js';
9
+ import { findTrack } from './mission-phase-track.js';
10
10
  import { effectiveInstanceRoot } from '../instance/resolve.js';
11
11
 
12
12
  export type TrackAgentInput = { missionId: string; taskId: string; title: string; prompt: string; track: string };
@@ -45,7 +45,7 @@ const forbidden = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merg
45
45
  let running = 0;
46
46
 
47
47
  function context(input: TrackAgentInput, repo: string): string {
48
- const track = loadTrackRegistry().find(({ id }) => id === input.track);
48
+ const track = findTrack(input.track);
49
49
  if (!track) throw new Error('unknown track');
50
50
  const docs = resolve(repo, 'docs');
51
51
  let handoff = '';
@@ -94,7 +94,7 @@ export async function decideTrackAction(input: TrackAgentInput, deps: RunnerDeps
94
94
  export async function executeTrackAction(decision: TrackAgentDecision, input: TrackAgentInput, deps: RunnerDeps = {}): Promise<TrackAgentResult> {
95
95
  if (decision.action !== 'say') return { status: 'held', detail: decision.reason ?? '사람 확인' };
96
96
  if (forbidden.test(`${input.title}\n${input.prompt}`)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
97
- if (!loadTrackRegistry().some(({ id }) => id === input.track)) return { status: 'held', detail: '트랙 미정 — 사람 확인' };
97
+ if (!findTrack(input.track)) return { status: 'held', detail: '트랙 미정 — 사람 확인' };
98
98
  try {
99
99
  const checkBudget = deps.checkBudget ?? (async () => {
100
100
  const action = decideBudget(await readBudgetInputsLive()).action;
@@ -6,7 +6,7 @@ import { join, resolve } from 'node:path';
6
6
  import { debug, redactSecretText } from '../debug/log.js';
7
7
  import { TaskStore } from '../task-orchestrator/store.js';
8
8
  import { getUserConfig, type UserConfig } from '../user-config.js';
9
- import { inferPhaseTrack, loadTrackRegistry } from './mission-phase-track.js';
9
+ import { inferPhaseTrack, loadTrackRegistry, trackPostIds } from './mission-phase-track.js';
10
10
  import type { TrackAgentInput, TrackAgentRunner } from './track-agent.js';
11
11
 
12
12
  type CourierConfig = NonNullable<NonNullable<UserConfig['autopilot']>['trackCourier']>;
@@ -66,7 +66,7 @@ export async function trackAlive(track: string, deps: CourierDeps = {}): Promise
66
66
  const comments = await (deps.listComments ?? (() => defaultComments(config.channelPr!, now, minutesFor(config))))();
67
67
  if (!Array.isArray(comments)) throw new Error('comments response is not an array');
68
68
  const timestamps = comments
69
- .filter((c) => c && typeof c.body === 'string' && c.body.startsWith(`**[${track}]**`))
69
+ .filter((c) => c && typeof c.body === 'string' && trackPostIds(track).some((id) => c.body.startsWith(`**[${id}]**`)))
70
70
  .map((c) => Date.parse(c.created_at))
71
71
  .filter((at) => Number.isFinite(at) && at <= now);
72
72
  if (!timestamps.length) return { alive: false, lastPostAt: null, reason: 'no-post' };
@@ -24,14 +24,14 @@
24
24
  // no dashboard, no tui-client — verified by the expanded guard
25
25
  // test.
26
26
 
27
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
27
+ import { mkdirSync } from 'node:fs';
28
28
  import { getElanousConfigDir } from '../elanous-config-dir.js';
29
29
  import { dirname, join as joinPath } from 'node:path';
30
+ import { ensureAuthToken } from '../auth/acp-token.js';
30
31
 
31
32
  import {
32
33
  listenUnixSocket,
33
34
  listenWebSocket,
34
- generateAuthToken,
35
35
  createAuthVerifier,
36
36
  } from '../acp/transport/index.js';
37
37
  import { runAcpServer, type AcpServerOptions } from '../acp/server.js';
@@ -110,20 +110,6 @@ export function parseAcpBootArgs(argv: readonly string[]): AcpBootOptions {
110
110
  return opts;
111
111
  }
112
112
 
113
- /** Load or mint a persistent auth token for websocket boot. Stored
114
- * at `~/.elanous/acp-token` with 0600 perms so the socket-holding
115
- * user can bind-mount it into a parent client config. */
116
- function ensureAuthToken(): { token: string; path: string } {
117
- const tokenPath = joinPath(getElanousConfigDir(), 'acp-token');
118
- mkdirSync(dirname(tokenPath), { recursive: true });
119
- if (existsSync(tokenPath)) {
120
- return { token: readFileSync(tokenPath, 'utf-8').trim(), path: tokenPath };
121
- }
122
- const token = generateAuthToken();
123
- writeFileSync(tokenPath, token, { mode: 0o600 });
124
- return { token, path: tokenPath };
125
- }
126
-
127
113
  /** Resolved daemon-runtime status surfaced in the startup banner.
128
114
  * Matches the shape returned by `createDaemonRuntime()` so callers
129
115
  * can spread it directly. Lets every transport (stdio / unix-socket
@@ -39,7 +39,8 @@ export function buildWebSearchTool(): LLMToolSpec {
39
39
  name: 'WebSearch',
40
40
  description:
41
41
  'Search the public web via the daemon\'s configured provider (Grok / ' +
42
- 'Firecrawl / etc.). Returns a ranked list of URLs + titles + snippets. ' +
42
+ 'Firecrawl / etc.). When no key is available, falls back to keyless DuckDuckGo. ' +
43
+ 'Returns a ranked list of URLs + titles + snippets. ' +
43
44
  'Pair with a follow-up Read or external WebFetch when the model needs ' +
44
45
  'the full article body.',
45
46
  parameters: {
@@ -26,6 +26,7 @@ import type {
26
26
  PwaVoiceSession,
27
27
  } from '../voice/channel-adapters/pwa-voice-adapter.js';
28
28
  import { PWA_VOICE_FRAME_KIND } from '../voice/channel-adapters/pwa-voice-adapter.js';
29
+ import { isSameOriginRequest } from './check-same-origin.js';
29
30
  import {
30
31
  buildDevProxyWsUrl,
31
32
  closeDevProxyUpstream,
@@ -47,6 +48,8 @@ export type WsLike = {
47
48
 
48
49
  export type BunServerLike = {
49
50
  upgrade: (req: Request, opts?: { data?: unknown; headers?: HeadersInit }) => boolean;
51
+ /** Bun's transport peer — used only for the same-origin exemption (HTTP 과 같은 규칙). */
52
+ requestIP?: (req: Request) => { address: string } | null;
50
53
  };
51
54
 
52
55
  export type WsHandlers = {
@@ -246,6 +249,14 @@ export function createWsBridge(opts: WsBridgeOpts): WsBridge {
246
249
  });
247
250
  }
248
251
  authed = true;
252
+ } else if (isSameOriginRequest(req, server.requestIP?.(req)?.address)) {
253
+ // ⭐ HTTP 의 same-origin 면제(`meta-api.ts` decideAuth)와 «같은» 규칙 — 루프백 직결 ⊕ 프록시 표지 없음 ⊕
254
+ // Sec-Fetch-Site/Origin 이 같은 출처. 이 데몬이 내준 PWA 페이지가 토큰 없이 연 소켓이다.
255
+ // 🩸 2026-09-30 0.2.5 회귀: #21937 이 토큰을 «늘» 만들게 되면서 WS 검증기가 늘 켜졌고, 토큰을
256
+ // 저장하지 않은 PWA(새 설치 · 격리 넥서스)의 ACP 가 첫 `initialize` 에서 1008 auth_failed 로 닫혔다
257
+ // — HTTP 는 이 면제로 살아 있어 «목록은 뜨는데 채팅·터미널만 죽는» 모양이 됐다.
258
+ authed = true;
259
+ trace('acp.ws.same-origin', { path: url.pathname });
249
260
  }
250
261
  }
251
262
  const acpState: AcpPerSocketState = {