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
@@ -11,6 +11,8 @@
11
11
  // the report channel while interactive Q&A stays on the main bot.
12
12
 
13
13
  import { TelegramBot } from './telegram.js';
14
+ import { kindRouteTarget } from './domains/telegram-kind-route.js';
15
+ export { DEFAULT_KIND_ROLES, roleForKind } from './domains/telegram-kind-route.js';
14
16
  import { resolveChannelBotToken } from './channel-bot-token.js';
15
17
  import type { UserConfig } from './user-config.js';
16
18
  import { findSessionByTelegramChat, createSession, appendMessage } from './session/index.js';
@@ -49,11 +51,13 @@ export interface ReportTarget {
49
51
  chatId: number;
50
52
  }
51
53
 
52
- /** Resolve the report-channel send target from config, or null when the
53
- * feature is unconfigured. `botToken` falls back to the main Q&A bot
54
- * token when the report channel is served by the same bot. Returns null
55
- * when neither a report-channel token nor a main token is available. */
56
- export function resolveReportTarget(cfg: UserConfig): ReportTarget | null {
54
+ /** Resolve the send target. With a purpose `kind` that maps to a role and explicit `telegram.channels`,
55
+ * the channel holding that role wins (fallback: the `default` channel). Otherwise the legacy report
56
+ * channel: `botToken` falls back to the main Q&A bot token when the report channel is served by the
57
+ * same bot. Returns null when nothing is configured. */
58
+ export function resolveReportTarget(cfg: UserConfig, kind?: string): ReportTarget | null {
59
+ const routed = kindRouteTarget(cfg, kind);
60
+ if (routed) return routed;
57
61
  const rc = cfg.telegram.reportChannel;
58
62
  if (!rc || !Number.isFinite(rc.chatId)) return null;
59
63
  const botToken = rc.botToken ?? resolveChannelBotToken('telegram', cfg)?.token;
@@ -67,6 +71,8 @@ export interface SendReportOpts {
67
71
  markdown?: boolean;
68
72
  /** DI seam for tests — passed through to the send-only TelegramBot. */
69
73
  fetchImpl?: typeof fetch;
74
+ /** Purpose of the message — routes to a channel role (see `DEFAULT_KIND_ROLES`). */
75
+ kind?: string;
70
76
  }
71
77
 
72
78
  /** Send a report to the configured report channel. Returns false (a
@@ -78,7 +84,7 @@ export async function sendTelegramReport(
78
84
  text: string,
79
85
  opts: SendReportOpts = {},
80
86
  ): Promise<boolean> {
81
- const target = resolveReportTarget(cfg);
87
+ const target = resolveReportTarget(cfg, opts.kind);
82
88
  if (!target || !text) return false;
83
89
  const bot = new TelegramBot({
84
90
  token: target.botToken,
package/src/telegram.ts CHANGED
@@ -67,6 +67,8 @@ import { runUrlRoute } from './skills/url-route-exec.js';
67
67
  import { recordRoutedLinkAbsorbed } from './intake-plane/link-ledger.js';
68
68
  import { classifyFrontInput } from './intake-plane/front-classifier.js';
69
69
  import { effectiveInstanceRoot } from './instance/resolve.js';
70
+ import { getElanousConfigDir } from './elanous-config-dir.js';
71
+ import { defaultFieldEvent, FieldUploadError, parseFieldCaption, resolveFieldMime, saveFieldMedia } from './field/field-media.js';
70
72
 
71
73
  export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
72
74
 
@@ -105,6 +107,10 @@ export interface TgIncoming {
105
107
  botId?: string;
106
108
  /** 답장 대상 메시지의 텍스트(force_reply 정정 가로채기용). parseUpdate 가 채운다. */
107
109
  replyToText?: string;
110
+ /** 앨범(media group) id — `#현장` 캡션은 앨범의 첫 항목에만 붙으므로 나머지를 같은 행사로 묶는 데 쓴다. */
111
+ mediaGroupId?: string;
112
+ /** 영상 — `#현장` 저장 전용. `attachments` 에 넣지 않는다(LLM 경로의 동작을 안 바꾸려고). */
113
+ video?: TgIncomingAttachment;
108
114
  }
109
115
 
110
116
  /** Streaming surface passed to the message handler. The handler can
@@ -200,6 +206,10 @@ export interface TelegramBotOpts {
200
206
  * independently of the LLM chat path. Best-effort: errors are
201
207
  * swallowed by the caller — never blocks chat reply. */
202
208
  onTriggerTap?: (event: TgTriggerEvent) => void | Promise<void>;
209
+ /** `#현장` 기본 행사 — 없으면 `slashContext.userConfig.telegram.fieldDefaultEvent` · 그것도 없으면 `field-<로컬 날짜>`. */
210
+ fieldDefaultEvent?: string;
211
+ /** 시험 seam — 현장 폴더 뿌리(기본 `getElanousConfigDir()`). */
212
+ fieldRootDir?: () => string;
203
213
  }
204
214
 
205
215
  /** Surface-unification v2 (FU-2) — Telegram trigger event passed to
@@ -314,6 +324,13 @@ export class TelegramBot {
314
324
  * the bot under Telegram's ~30/sec global cap without blocking
315
325
  * further if load is bursty but average-safe. */
316
326
  private readonly globalWindow: number[] = [];
327
+ private readonly fieldDefaultEventOpt?: string;
328
+ private readonly fieldRootDir: () => string;
329
+ /** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
330
+ private readonly fieldGroups = new Map<string, {
331
+ event: string; at: number; chatId: number; threadId?: number; replyTo: number; count: number;
332
+ timer?: ReturnType<typeof setTimeout>;
333
+ }>();
317
334
 
318
335
  constructor(opts: TelegramBotOpts) {
319
336
  if (!opts.token) throw new Error('TelegramBot: token required');
@@ -340,6 +357,8 @@ export class TelegramBot {
340
357
  this.slashCommands = opts.slashCommands ?? [];
341
358
  this.slashContext = opts.slashContext;
342
359
  this.voiceAdapter = opts.voiceAdapter;
360
+ this.fieldDefaultEventOpt = opts.fieldDefaultEvent;
361
+ this.fieldRootDir = opts.fieldRootDir ?? getElanousConfigDir;
343
362
  if (opts.onTriggerTap) {
344
363
  // Assign through `as unknown` to bypass `readonly` + private —
345
364
  // the tap is set once at construction and never reassigned.
@@ -1347,11 +1366,82 @@ export class TelegramBot {
1347
1366
  return false;
1348
1367
  }
1349
1368
 
1369
+ /** `#현장` 캡션 사진·영상(이미지·영상 문서 포함)을 현장 폴더에 저장하고 한 줄로 답한다.
1370
+ * 처리했으면 true. 저장 규칙은 HTTP 입구와 같은 `saveFieldMedia` 다. */
1371
+ private async tryHandleFieldUpload(ctx: TgIncoming): Promise<boolean> {
1372
+ const media = [
1373
+ ...ctx.attachments.filter((a) => a.kind === 'photo'
1374
+ || (a.kind === 'document' && resolveFieldMime(a.mimeType, a.fileName ?? '') !== null)),
1375
+ ...(ctx.video ? [ctx.video] : []),
1376
+ ];
1377
+ if (media.length === 0) return false;
1378
+ const now = this.nowImpl();
1379
+ for (const [id, g] of this.fieldGroups) if (now - g.at > 10 * 60_000 && !g.timer) this.fieldGroups.delete(id);
1380
+ const tag = parseFieldCaption(ctx.text);
1381
+ const group = ctx.mediaGroupId ? this.fieldGroups.get(ctx.mediaGroupId) : undefined;
1382
+ if (!tag && !group) return false;
1383
+ const event = tag
1384
+ ? tag.event ?? defaultFieldEvent(
1385
+ this.fieldDefaultEventOpt ?? this.slashContext?.userConfig?.telegram?.fieldDefaultEvent, new Date(now))
1386
+ : group!.event;
1387
+
1388
+ let count: number | null = null;
1389
+ let failure: string | null = null;
1390
+ try {
1391
+ const inputs = [];
1392
+ for (const att of media) {
1393
+ const downloaded = await this.downloadFile(att.fileId);
1394
+ try {
1395
+ inputs.push({
1396
+ originalName: att.fileName ?? downloaded.fileName ?? 'media',
1397
+ mimeType: att.mimeType ?? (att.kind === 'photo' ? 'image/jpeg' : undefined),
1398
+ bytes: new Uint8Array(readFileSync(downloaded.localPath)),
1399
+ });
1400
+ } finally {
1401
+ try { unlinkSync(downloaded.localPath); } catch { /* 이미 없음 */ }
1402
+ }
1403
+ }
1404
+ // 글(자막)은 캡션이 달린 항목에만 있다 — 앨범이면 첫 장에만 붙는다(MK 2026-09-30).
1405
+ count = saveFieldMedia(inputs, { rootDir: this.fieldRootDir(), event, device: 'telegram', ...(tag?.text ? { caption: tag.text } : {}) }).count;
1406
+ debug.log('field.upload', 'saved', { surface: 'telegram', files: inputs.length, count, album: !!ctx.mediaGroupId });
1407
+ } catch (err) {
1408
+ failure = err instanceof FieldUploadError ? err.code : 'download-failed';
1409
+ debug.log('field.upload', 'refused', { surface: 'telegram', reason: failure });
1410
+ }
1411
+
1412
+ if (failure !== null) {
1413
+ await this.sendMessage(ctx.chatId, `현장 저장 실패 · ${event} · ${failure}`, { replyTo: ctx.messageId, threadId: ctx.threadId });
1414
+ return true;
1415
+ }
1416
+ const line = (n: number) => `현장 폴더에 저장 · ${event} · 지금 ${n}장`;
1417
+ if (!ctx.mediaGroupId) {
1418
+ await this.sendMessage(ctx.chatId, line(count!), { replyTo: ctx.messageId, threadId: ctx.threadId });
1419
+ return true;
1420
+ }
1421
+ // 앨범은 항목마다 답하지 않는다 — 마지막 항목 뒤 한 번(디바운스)만 최신 수로 답한다.
1422
+ const entry = group ?? { event, at: now, chatId: ctx.chatId, threadId: ctx.threadId, replyTo: ctx.messageId, count: 0 };
1423
+ entry.at = now;
1424
+ entry.count = count!;
1425
+ if (entry.timer) clearTimeout(entry.timer);
1426
+ entry.timer = setTimeout(() => {
1427
+ entry.timer = undefined;
1428
+ void this.sendMessage(entry.chatId, line(entry.count), { replyTo: entry.replyTo, threadId: entry.threadId })
1429
+ .catch((e) => this.log(`field reply failed: ${e instanceof Error ? e.message : String(e)}`));
1430
+ }, 1500);
1431
+ (entry.timer as { unref?: () => void }).unref?.();
1432
+ this.fieldGroups.set(ctx.mediaGroupId, entry);
1433
+ return true;
1434
+ }
1435
+
1350
1436
  private async handleIncoming(ctx: TgIncoming): Promise<void> {
1351
1437
  if (!this.isOwnerAllowed(ctx.userId)) {
1352
1438
  await this.refuseMessage(ctx);
1353
1439
  return;
1354
1440
  }
1441
+ // 현장 업로드 — `#현장` 캡션(또는 태그된 앨범의 나머지)의 사진·영상은 LLM 대신 현장 폴더로.
1442
+ if (await this.tryHandleFieldUpload(ctx)) return;
1443
+ // 영상은 `#현장` 저장 전용으로만 파싱한다 — 그 밖의 영상은 종전처럼 무시한다.
1444
+ if (ctx.video && ctx.attachments.length === 0) return;
1355
1445
  // Cascade-zyu U2 — capture utterance intent at update ingress.
1356
1446
  try {
1357
1447
  const { userIntentLogger } = await import('./user-intent/index.js');
@@ -2069,6 +2159,7 @@ export class TelegramBot {
2069
2159
  interface PhotoSize { file_id: string; file_size?: number; width: number; height: number }
2070
2160
  interface VoiceLike { file_id: string; file_size?: number; mime_type?: string; duration?: number }
2071
2161
  interface DocumentLike { file_id: string; file_size?: number; mime_type?: string; file_name?: string }
2162
+ interface VideoLike { file_id: string; file_size?: number; mime_type?: string; file_name?: string; duration?: number }
2072
2163
 
2073
2164
  interface RawUpdate {
2074
2165
  update_id: number;
@@ -2083,6 +2174,8 @@ interface RawUpdate {
2083
2174
  voice?: VoiceLike;
2084
2175
  audio?: VoiceLike;
2085
2176
  document?: DocumentLike;
2177
+ video?: VideoLike;
2178
+ media_group_id?: string;
2086
2179
  reply_to_message?: { text?: string; message_id?: number };
2087
2180
  };
2088
2181
  callback_query?: {
@@ -2175,8 +2268,20 @@ export function parseUpdate(u: RawUpdate): TgIncoming | null {
2175
2268
  });
2176
2269
  }
2177
2270
 
2271
+ // 영상은 `#현장` 저장 전용 — 태그된 캡션이거나 앨범 항목일 때만 싣는다(그 밖의 영상은 종전처럼 무시).
2272
+ const video = m.video && (parseFieldCaption(m.caption) || m.media_group_id)
2273
+ ? {
2274
+ kind: 'document' as const,
2275
+ fileId: m.video.file_id,
2276
+ fileSize: m.video.file_size,
2277
+ mimeType: m.video.mime_type ?? 'video/mp4',
2278
+ fileName: m.video.file_name,
2279
+ duration: m.video.duration,
2280
+ }
2281
+ : undefined;
2282
+
2178
2283
  // Skip updates with zero signal: no text AND no attachments.
2179
- if (typeof m.text !== 'string' && attachments.length === 0) return null;
2284
+ if (typeof m.text !== 'string' && attachments.length === 0 && !video) return null;
2180
2285
 
2181
2286
  const isDm = m.chat.type === 'private';
2182
2287
  return {
@@ -2191,6 +2296,8 @@ export function parseUpdate(u: RawUpdate): TgIncoming | null {
2191
2296
  isGroup: !isDm,
2192
2297
  attachments,
2193
2298
  ...(m.reply_to_message?.text ? { replyToText: m.reply_to_message.text } : {}),
2299
+ ...(m.media_group_id ? { mediaGroupId: m.media_group_id } : {}),
2300
+ ...(video ? { video } : {}),
2194
2301
  };
2195
2302
  }
2196
2303
 
@@ -25,6 +25,7 @@ import {
25
25
  AcpTransportError,
26
26
  type AcpTransportConnection,
27
27
  } from '../acp/transport/index.js';
28
+ import { debug } from '../debug/log.js';
28
29
 
29
30
  export interface ConnectWebSocketClientOpts {
30
31
  /** Full WebSocket URL — e.g. `ws://mbp.tailnet:31415/v1/acp`. */
@@ -42,11 +43,15 @@ export interface ConnectWebSocketClientOpts {
42
43
  * ⛔ 없으면 서버가 핸드셰이크에 답하지 않을 때 이 Promise 가 «영원히» 안 풀린다 —
43
44
  * 그러면 호출자는 산출도 종료 코드도 못 얻는다(2026-08-31 실측: 100초 상한을 걸 때까지 매달렸다). */
44
45
  connectTimeoutMs?: number;
46
+ onDisconnect?(info: { reason: 'close' | 'error'; code?: number; afterMs: number; at: string; lastEventId?: string }): void;
45
47
  }
46
48
 
47
49
  /** ⏱️ 기본 연결 상한. 테일넷 왕복은 보통 수백 ms 라 넉넉하다. */
48
50
  export const DEFAULT_WS_CONNECT_TIMEOUT_MS = 10_000;
49
51
 
52
+ /** Maximum NDJSON line inspected for an event id; ACP stream delivery is not limited. */
53
+ const MAX_EVENT_ID_LINE_LENGTH = 64 * 1024;
54
+
50
55
  /** Connect to a Elanous daemon over its public WebSocket endpoint.
51
56
  * Resolves with the bidirectional stream pair once the (optional)
52
57
  * auth handshake has completed; rejects with `AcpTransportError` on
@@ -72,6 +77,65 @@ export async function connectWebSocketClient(
72
77
  ws.binaryType = 'arraybuffer';
73
78
 
74
79
  let settled = false;
80
+ let connectedAt: number | null = null;
81
+ let disconnected = false;
82
+ let lastEventId: string | undefined;
83
+ let incomingLine = '';
84
+ let oversizedLine = false;
85
+ const lineDecoder = new TextDecoder();
86
+ const recordEventId = (line: string): void => {
87
+ try {
88
+ const message: unknown = JSON.parse(line);
89
+ if (message !== null && typeof message === 'object' && 'id' in message
90
+ && typeof message.id === 'string') lastEventId = message.id;
91
+ } catch { /* Non-JSON or incomplete ACP payload. */ }
92
+ };
93
+ const trackEventIds = (text: string): void => {
94
+ let start = 0;
95
+ while (start < text.length) {
96
+ const newline = text.indexOf('\n', start);
97
+ const end = newline < 0 ? text.length : newline;
98
+ const length = end - start;
99
+ if (!oversizedLine) {
100
+ if (incomingLine.length + length <= MAX_EVENT_ID_LINE_LENGTH) {
101
+ incomingLine += text.slice(start, end);
102
+ } else {
103
+ incomingLine = '';
104
+ oversizedLine = true;
105
+ }
106
+ }
107
+ if (newline < 0) break;
108
+ if (!oversizedLine) recordEventId(incomingLine);
109
+ incomingLine = '';
110
+ oversizedLine = false;
111
+ start = newline + 1;
112
+ }
113
+ };
114
+ const recordConnected = (): void => {
115
+ connectedAt = Date.now();
116
+ try {
117
+ const host = new URL(opts.url).hostname;
118
+ debug.log('tui-client.ws', 'connected', {
119
+ hostKind: host === 'localhost' || host === '127.0.0.1' || host === '::1' ? 'local' : 'remote',
120
+ });
121
+ } catch { /* observation must not prevent connection */ }
122
+ };
123
+ const disconnect = (reason: 'close' | 'error', code?: number): void => {
124
+ finishReadable();
125
+ if (connectedAt === null || disconnected) return;
126
+ disconnected = true;
127
+ trackEventIds(lineDecoder.decode());
128
+ if (incomingLine && !oversizedLine) recordEventId(incomingLine);
129
+ const info = {
130
+ reason,
131
+ ...(code !== undefined ? { code } : {}),
132
+ afterMs: Math.max(0, Date.now() - connectedAt),
133
+ at: new Date().toISOString(),
134
+ ...(lastEventId !== undefined ? { lastEventId } : {}),
135
+ };
136
+ try { debug.log('tui-client.ws', 'disconnect', info); } catch { /* observation must not prevent notification */ }
137
+ opts.onDisconnect?.(info);
138
+ };
75
139
  let authed = !opts.token; // noAuth mode = no handshake needed
76
140
  // ⛔ 「안 됐다」를 한 값으로 접지 않는다 — «어느 단계»에서 멎었는지 이름을 붙인다.
77
141
  // 소켓조차 안 열린 것과, 열렸는데 서버가 인증에 «답을 안 한» 것은 다른 결함이다.
@@ -152,6 +216,7 @@ export async function connectWebSocketClient(
152
216
  settled = true;
153
217
  clearConnectTimer();
154
218
  opts.signal?.removeEventListener('abort', onAbort);
219
+ recordConnected();
155
220
  resolve(buildConnection());
156
221
  return;
157
222
  }
@@ -183,6 +248,7 @@ export async function connectWebSocketClient(
183
248
  settled = true;
184
249
  clearConnectTimer();
185
250
  opts.signal?.removeEventListener('abort', onAbort);
251
+ recordConnected();
186
252
  resolve(buildConnection());
187
253
  }
188
254
  } else {
@@ -191,10 +257,11 @@ export async function connectWebSocketClient(
191
257
  return;
192
258
  }
193
259
  // Authed: forward bytes to the consumer's ReadableStream.
194
- if (!readableController) return;
195
260
  const bytes = typeof ev.data === 'string'
196
261
  ? new TextEncoder().encode(ev.data)
197
262
  : new Uint8Array(ev.data as ArrayBuffer);
263
+ trackEventIds(lineDecoder.decode(bytes, { stream: true }));
264
+ if (!readableController) return;
198
265
  try { readableController.enqueue(bytes); }
199
266
  catch { /* stream already closed */ }
200
267
  });
@@ -221,7 +288,7 @@ export async function connectWebSocketClient(
221
288
  ws.addEventListener('error', () => {
222
289
  // Bun/browser WebSocket fires error then close; only treat the
223
290
  // pre-settled error as a connect failure. Post-settled errors
224
- // close the readable stream — the consumer surfaces it.
291
+ // close the readable stream and notify the dashboard once.
225
292
  if (!settled) {
226
293
  // ⛔ 「연결 오류」 한 값으로 접지 않는다 — open «전»이면 소켓이 안 열린 것이고,
227
294
  // open «뒤»면 서버가 붙여 놓고 인증에 답하지 않은 것이다. 다른 결함이다.
@@ -229,17 +296,17 @@ export async function connectWebSocketClient(
229
296
  ? `socket never opened: connection error (${opts.url})`
230
297
  : `server accepted the socket but never answered the auth handshake: connection error (${opts.url})`);
231
298
  } else {
232
- finishReadable();
299
+ disconnect('error');
233
300
  }
234
301
  });
235
302
 
236
- ws.addEventListener('close', () => {
303
+ ws.addEventListener('close', (ev: CloseEvent) => {
237
304
  if (!settled) {
238
305
  failConnect(stage === 'opening'
239
306
  ? `socket never opened: closed before ready (${opts.url})`
240
307
  : `server accepted the socket but never answered the auth handshake: closed before ready (${opts.url})`);
241
308
  } else {
242
- finishReadable();
309
+ disconnect('close', ev.code);
243
310
  }
244
311
  });
245
312
  });
package/src/tui.ts CHANGED
@@ -326,7 +326,8 @@ function parseKey(data: string | Buffer): Key {
326
326
  if (s === '\x1b[3~') return K('delete');
327
327
 
328
328
  // Special keys
329
- if (s === '\r' || s === '\n') return K('enter');
329
+ if (s === '\r') return K('enter');
330
+ if (s === '\n') return K('j', true); // Ctrl+J → LF
330
331
  if (s === ' ') return K('space');
331
332
  if (s === '\x1b') return K('escape');
332
333
  if (s === '\t') return K('tab');
@@ -341,7 +342,7 @@ function parseKey(data: string | Buffer): Key {
341
342
  // fallback for plain terminals. Ctrl+1 has no distinct wire form;
342
343
  // Ctrl+3 collides with Escape — so those must come via modKeys.
343
344
  if (s === '\x00') return K('2', true); // Ctrl+2 → NUL
344
- if (s === '\x1c') return K('4', true); // Ctrl+4 → FS
345
+ if (s === '\x1c') return K('\\', true); // Ctrl+\\ → FS; Ctrl+4 shares this byte, so requires kitty/modifyOtherKeys
345
346
  if (s === '\x1d') return K('5', true); // Ctrl+5 → GS
346
347
  if (s.length === 1 && s.charCodeAt(0) < 32) {
347
348
  return K(String.fromCharCode(s.charCodeAt(0) + 96), true);
@@ -784,6 +784,12 @@ export interface TelegramChannel {
784
784
  }
785
785
 
786
786
  /** 저장된 telegram.channels[] 파싱. 각 항목은 botToken+chatId 필수. fail-soft. */
787
+ function normalizeKindRoles(raw: unknown): Record<string, string> | undefined {
788
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
789
+ const out = Object.fromEntries(Object.entries(raw as Record<string, unknown>).filter((e): e is [string, string] => typeof e[1] === 'string' && e[1].length > 0));
790
+ return Object.keys(out).length ? out : undefined;
791
+ }
792
+
787
793
  function normalizeTelegramChannels(raw: unknown): TelegramChannel[] | undefined {
788
794
  if (!Array.isArray(raw)) return undefined;
789
795
  const out: TelegramChannel[] = [];
@@ -824,6 +830,11 @@ export interface TelegramConfig {
824
830
  /** 누가 Q&A 폴링을 하나 — 기본(없음·`'nexus'`)은 넥서스 데몬, `'standalone'` 이면
825
831
  * 넥서스는 폴링하지 않고 `elanous telegram run` 이 맡는다. */
826
832
  poller?: 'nexus' | 'standalone';
833
+ /** 발송 목적(kind) → 채널 역할. 명시 `channels` 가 있을 때만 쓴다 — 표에 없는 kind 는 옛 `reportChannel` 로 간다.
834
+ * 기본표는 `DEFAULT_KIND_ROLES`(telegram-report.ts) · 여기 값이 덮어쓴다. */
835
+ kindRoles?: Record<string, string>;
836
+ /** `#현장` 캡션에 행사 슬러그가 없을 때 쓸 기본 행사(예: `marketers-night-2026-10`). 없으면 `field-<로컬 날짜>`. */
837
+ fieldDefaultEvent?: string;
827
838
  }
828
839
 
829
840
  const TELEGRAM_DEFAULTS: TelegramConfig = { enabled: false, allowedUsers: [] };
@@ -3315,12 +3326,12 @@ export function resolveRoleModel(role: ModelRole, config: UserConfig = getUserCo
3315
3326
 
3316
3327
  export interface UserConfig {
3317
3328
  /** Steward loop: observe-only until an independently approved act implementation exists. */
3318
- loops?: { steward?: { mode?: 'observe' | 'act'; linearTeam?: string; roles?: Record<string, { maxConcurrent?: number }>; budget?: number } };
3329
+ loops?: { steward?: { mode?: 'observe' | 'act'; linearTeam?: string; roles?: Record<string, { maxConcurrent?: number }>; budget?: number; tracks?: Record<string, string> } };
3319
3330
  /** 🔐 Pod 의 Grok API 키 과금은 명시 opt-in 만. 생략·잘못된 값은 false.
3320
3331
  * budgetGate 와 같은 `harness` 객체다 — 중복 선언이면 뒤 타입이 앞을 지운다. */
3321
3332
  harness?: { pod?: { grokApiKeyOptIn: boolean }; budgetGate?: HarnessBudgetGateConfig; defaultRepo?: string; substrate?: 'local' | 'pod'; podPool?: string };
3322
3333
  /** ☸️ Pod 실행 칸 — `pool` = 기존 기본 풀(`컨텍스트[@ssh호스트][:상한]` 쉼표 · 앞이 우선). harness 실행은 인자·ELANOUS_POD_POOL·harness.podPool 다음으로 읽는다. */
3323
- pod?: { pool?: string; /** 원격 그라운딩 엔드포인트(호스트 nexus · tailnet 주소) — Pod 가 토큰으로 «질의→인용»만 묻는다(P13). */ groundingUrl?: string };
3334
+ pod?: { pool?: string; /** 호스트 Git 미러 디렉터리 — Pod Job 에 읽기 전용으로 마운트한다. */ hostMirror?: string; /** 원격 그라운딩 엔드포인트(호스트 nexus · tailnet 주소) — Pod 가 토큰으로 «질의→인용»만 묻는다(P13). */ groundingUrl?: string };
3324
3335
  skillRouter: SkillRouterConfig;
3325
3336
  llm: LLMConfig;
3326
3337
  skills: SkillsConfig;
@@ -4106,6 +4117,15 @@ function observeRetiredConfigKeysOnce(raw: Record<string, unknown>): void {
4106
4117
  } catch { /* observation must never break config loading */ }
4107
4118
  }
4108
4119
 
4120
+ /** Owner tracks the steward may assign (key → what that track owns). The one place the role table lives. */
4121
+ function parseStewardTracks(input: unknown): Record<string, string> | undefined {
4122
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return undefined;
4123
+ const out = Object.fromEntries(Object.entries(input as Record<string, unknown>)
4124
+ .filter((e): e is [string, string] => /^[A-Za-z][\w-]{0,15}$/.test(e[0]) && typeof e[1] === 'string' && e[1].trim().length > 0)
4125
+ .map(([k, v]) => [k, v.trim()]));
4126
+ return Object.keys(out).length ? out : undefined;
4127
+ }
4128
+
4109
4129
  function parseStewardLoopsConfig(input: unknown): UserConfig['loops'] {
4110
4130
  if (!input || typeof input !== 'object' || Array.isArray(input)) return undefined;
4111
4131
  const value = (input as Record<string, unknown>).steward;
@@ -4124,6 +4144,7 @@ function parseStewardLoopsConfig(input: unknown): UserConfig['loops'] {
4124
4144
  ...(typeof s.linearTeam === 'string' && s.linearTeam.trim() ? { linearTeam: s.linearTeam.trim() } : {}),
4125
4145
  ...(Object.keys(roles).length ? { roles } : {}),
4126
4146
  ...(typeof s.budget === 'number' && Number.isFinite(s.budget) && s.budget >= 0 ? { budget: s.budget } : {}),
4147
+ ...(parseStewardTracks(s.tracks) ? { tracks: parseStewardTracks(s.tracks)! } : {}),
4127
4148
  } };
4128
4149
  }
4129
4150
 
@@ -4233,6 +4254,7 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
4233
4254
  return {
4234
4255
  pod: {
4235
4256
  ...(typeof legacyPod.pool === 'string' && legacyPod.pool.trim() ? { pool: legacyPod.pool.trim() } : {}),
4257
+ ...(typeof legacyPod.hostMirror === 'string' && legacyPod.hostMirror.trim() ? { hostMirror: legacyPod.hostMirror.trim() } : {}),
4236
4258
  ...(typeof legacyPod.groundingUrl === 'string' ? { groundingUrl: legacyPod.groundingUrl } : {}),
4237
4259
  },
4238
4260
  harness: {
@@ -4372,6 +4394,8 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
4372
4394
  channels: normalizeTelegramChannels(tg.channels),
4373
4395
  testChannel: normalizeTestChannel(tg.testChannel),
4374
4396
  ...(tg.poller === 'standalone' || tg.poller === 'nexus' ? { poller: tg.poller } : {}),
4397
+ ...(normalizeKindRoles(tg.kindRoles) ? { kindRoles: normalizeKindRoles(tg.kindRoles)! } : {}),
4398
+ ...(str(tg.fieldDefaultEvent) ? { fieldDefaultEvent: str(tg.fieldDefaultEvent)! } : {}),
4375
4399
  },
4376
4400
  discord: {
4377
4401
  enabled: dc.enabled === true,
@@ -5426,6 +5450,11 @@ export function saveUserConfig(
5426
5450
  const rawHarness = rawRest.harness && typeof rawRest.harness === 'object' && !Array.isArray(rawRest.harness)
5427
5451
  ? rawRest.harness as Record<string, unknown> : {};
5428
5452
  delete rawRest.harness;
5453
+ if (cfg.pod?.hostMirror) {
5454
+ const rawPod = rawRest.pod && typeof rawRest.pod === 'object' && !Array.isArray(rawRest.pod)
5455
+ ? rawRest.pod as Record<string, unknown> : {};
5456
+ rawRest.pod = { ...rawPod, hostMirror: cfg.pod.hostMirror };
5457
+ }
5429
5458
 
5430
5459
  const out: Record<string, unknown> = {
5431
5460
  harness: stripUndef({
@@ -5539,6 +5568,7 @@ export function saveUserConfig(
5539
5568
  // ★ 2026-09-25: `poller`(T1 분리 스위치)가 파서엔 있고 이 whitelist 에 없어 `config set telegram.poller` 가
5540
5569
  // 「직렬화 드롭」으로 조용히 사라졌다 — 운영 전환 스위치를 켤 방법이 없었다(07-22 botUsername 과 같은 모양).
5541
5570
  poller: cfg.telegram.poller,
5571
+ fieldDefaultEvent: cfg.telegram.fieldDefaultEvent,
5542
5572
  }),
5543
5573
  discord: stripUndef({
5544
5574
  enabled: cfg.discord.enabled,
@@ -0,0 +1,15 @@
1
+ import { spawnSync, type SpawnSyncOptionsWithStringEncoding } from 'node:child_process';
2
+
3
+ /** Read command text completely or fail closed; never return a partial stdout. */
4
+ export function spawnSyncText(
5
+ cmd: string,
6
+ args: readonly string[],
7
+ opts: SpawnSyncOptionsWithStringEncoding = { encoding: 'utf8' },
8
+ ): string {
9
+ const result = spawnSync(cmd, [...args], { ...opts, maxBuffer: opts.maxBuffer ?? 256 * 1024 * 1024 });
10
+ if (result.error || result.status !== 0) {
11
+ const reason = result.error?.message ?? result.stderr?.trim() ?? '';
12
+ throw new Error(`${cmd} ${args.join(' ')} failed (status=${result.status}${result.error ? `, error=${(result.error as NodeJS.ErrnoException).code ?? result.error.name}` : ''}): ${reason}`);
13
+ }
14
+ return result.stdout;
15
+ }
@@ -54,7 +54,7 @@ function usableInstallMetadataCommit(commit: string): string | undefined {
54
54
  return revision;
55
55
  }
56
56
 
57
- function readInstallMetadataCommit(metadataPath: string): string | undefined {
57
+ export function readInstallMetadataCommit(metadataPath: string): string | undefined {
58
58
  try {
59
59
  const parsed: unknown = JSON.parse(readFileSync(metadataPath, 'utf8'));
60
60
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
@@ -1 +1 @@
1
- {"commit":"0b373c740eb18bdeefe8a8e0710d756b3f36d658"}
1
+ {"commit":"65a4c7230c60a536b0f0f0428322c8ddcbcac752"}