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,404 @@
1
+ import { closeSync, existsSync, fstatSync, ftruncateSync, mkdirSync, openSync, readFileSync, realpathSync, statSync, unlinkSync, writeSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
4
+ import { debug, redactSecretText } from '../debug/log.js';
5
+ import { createVersionResolver, type VersionOptions, type Versions } from '../directives/version-at.js';
6
+ import { getUserConfig } from '../user-config.js';
7
+
8
+ export type DecisionCategory = 'secret' | 'publish' | 'money' | 'security' | 'scope' | 'irreversible' | 'other';
9
+ export type DecisionTrack = 'S' | 'T' | 'F' | 'O';
10
+ export interface DecisionOption { key: string; label: string; consequence: string }
11
+ export type Recommendation = { option: string; why: string } | { skipped: true; reason: string };
12
+ export type DecisionActor = { kind: 'human' } | { kind: 'auto'; agent: string; track?: DecisionTrack; delegation: string };
13
+ export interface DecisionEntry {
14
+ id: string; title: string; category: DecisionCategory; scqa: { s: string; c: string; q?: string; a?: string };
15
+ options: DecisionOption[]; recommendation: Recommendation; raisedAt?: string; importedAt?: string;
16
+ raisedBy: { agent: string; track?: DecisionTrack; session?: string }; version?: Versions;
17
+ status: 'open' | 'decided' | 'withdrawn'; refs?: string[];
18
+ decidedAt?: string; decidedBy?: DecisionActor; choice?: string; note?: string; versionAtDecision?: Versions;
19
+ withdrawnAt?: string; withdrawReason?: string;
20
+ history: Array<{ type: 'raised' | 'options-added' | 'decided' | 'withdrawn'; at?: string; by: string; version?: Versions; choice?: string; reason?: string }>;
21
+ }
22
+ export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs'> & { raisedAt?: string };
23
+ type Event = { type: 'raised'; entry: DecisionEntry } | { type: 'options-added'; id: string; at: string; options: DecisionOption[]; by: string } | { type: 'decided'; id: string; at?: string; by: DecisionActor; choice?: string; version?: Versions; note?: string } | { type: 'withdrawn'; id: string; at: string; reason: string; version: Versions };
24
+ export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions }
25
+ const CATEGORIES: readonly string[] = ['secret', 'publish', 'money', 'security', 'scope', 'irreversible', 'other'];
26
+ const TRACKS: readonly string[] = ['S', 'T', 'F', 'O'];
27
+
28
+ function required(value: string, name: string): string {
29
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} is required`);
30
+ return value.trim();
31
+ }
32
+ function single(value: string, name: string): string {
33
+ const text = required(value, name);
34
+ if (/[\r\n]/.test(text)) throw new Error(`${name} must be one line`);
35
+ return text;
36
+ }
37
+ function safe(value: string): string {
38
+ return redactSecretText(value)
39
+ .replace(/\bsk-[a-z0-9-]{16,}\b/gi, '[REDACTED]')
40
+ .replace(/\b(?:gh[pousr]_|github_pat_|xox[baprs]-)[A-Za-z0-9_-]{12,}\b/g, '[REDACTED]')
41
+ .replace(/\b(?:api[_-]?key|token|secret|password)\s*[:=]\s*[^\s,;]+/gi, '[REDACTED]')
42
+ .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED]')
43
+ .replace(/\b[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{8,}\b/g, '[REDACTED]');
44
+ }
45
+ function utc(value: string): string {
46
+ if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?Z$/.test(value)) throw new Error(`UTC timestamp required: ${value}`);
47
+ const parsed = new Date(value);
48
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 19) !== value.slice(0, 19)) throw new Error(`UTC timestamp required: ${value}`);
49
+ return parsed.toISOString();
50
+ }
51
+ function scqaField(value: string, name: string): string {
52
+ const original = required(value, `SCQA ${name}`);
53
+ const sentences = original.split(/[.!?。!?]+(?:\s+|$)/).filter(part => part.trim());
54
+ if (Array.from(original).length > 240 || sentences.length > 2) throw new Error(`SCQA ${name} exceeds 2 sentences or 240 characters`);
55
+ return safe(original);
56
+ }
57
+ function validate(input: RaiseInput, historical = false): RaiseInput {
58
+ const title = safe(single(input.title, 'title'));
59
+ if (!CATEGORIES.includes(input.category)) throw new Error('invalid category');
60
+ if (!input.raisedBy?.agent) throw new Error('raisedBy.agent is required');
61
+ if (input.raisedBy.track && !TRACKS.includes(input.raisedBy.track)) throw new Error('invalid track');
62
+ const s = scqaField(input.scqa.s, 's');
63
+ const c = scqaField(input.scqa.c, 'c');
64
+ const q = input.scqa.q?.trim() ? scqaField(input.scqa.q, 'q') : undefined;
65
+ const a = input.scqa.a?.trim() ? scqaField(input.scqa.a, 'a') : undefined;
66
+ if (q && q === c) throw new Error('SCQA Q repeats C — leave Q empty');
67
+ if (!Array.isArray(input.options) || (input.options.length < 2 && !(historical && input.options.length === 0))) throw new Error('at least two options required');
68
+ const options = input.options.map(o => ({ key: single(o.key, 'option key'), label: safe(single(o.label, 'option label')), consequence: safe(single(o.consequence, 'option consequence')) }));
69
+ if (options.some(o => !/^[a-z]$/.test(o.key)) || new Set(options.map(o => o.key)).size !== options.length) throw new Error('option keys must be unique lowercase letters');
70
+ const recommendation = 'skipped' in input.recommendation
71
+ ? { skipped: true as const, reason: safe(single(input.recommendation.reason, 'skip reason')) }
72
+ : { option: input.recommendation.option, why: safe(single(input.recommendation.why, 'recommendation why')) };
73
+ if ('option' in recommendation && !options.some(o => o.key === recommendation.option)) throw new Error('recommended option not found');
74
+ return { ...input, title, scqa: { s, c, ...(q ? { q } : {}), ...(a ? { a } : {}) }, options, recommendation,
75
+ raisedBy: { agent: safe(single(input.raisedBy.agent, 'agent')), ...(input.raisedBy.track ? { track: input.raisedBy.track } : {}), ...(input.raisedBy.session ? { session: safe(single(input.raisedBy.session, 'session')) } : {}) },
76
+ ...(input.refs ? { refs: input.refs.map(r => safe(single(r, 'ref'))) } : {}) };
77
+ }
78
+
79
+ export class DecisionLedger {
80
+ readonly path: string;
81
+ private readonly lockPath: string;
82
+ private readonly now: () => Date;
83
+ private versionResolver?: (at: string) => Versions;
84
+ private readonly versionOptions: DecisionLedgerOptions;
85
+ constructor(options: DecisionLedgerOptions = {}) {
86
+ this.path = join(options.stateDir ?? elanousStateRoot(), 'decisions', 'decisions.jsonl');
87
+ this.lockPath = `${this.path}.lock`;
88
+ this.now = options.now ?? (() => new Date());
89
+ this.versionOptions = options;
90
+ this.versionResolver = options.resolveVersion;
91
+ }
92
+ private version(at: string): Versions {
93
+ if (!this.versionResolver) {
94
+ const release = getUserConfig().raw.release as { codenames?: Record<string, string> } | undefined;
95
+ this.versionResolver = createVersionResolver({ releaseRoot: this.versionOptions.releaseRoot, repoRoot: this.versionOptions.repoRoot,
96
+ codenames: this.versionOptions.codenames ?? release?.codenames });
97
+ }
98
+ return this.versionResolver(at);
99
+ }
100
+ private locked<T>(fn: () => T): T {
101
+ mkdirSync(join(this.path, '..'), { recursive: true });
102
+ const deadline = Date.now() + 10000;
103
+ let fd: number;
104
+ for (;;) {
105
+ try { fd = openSync(this.lockPath, 'wx', 0o600); break; }
106
+ catch (error) {
107
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
108
+ // Only a dead, old owner can be reclaimed; never steal a live process's lock.
109
+ try {
110
+ const pid = Number(readFileSync(this.lockPath, 'utf8'));
111
+ if (Number.isSafeInteger(pid) && pid > 0 && Date.now() - statSync(this.lockPath).mtimeMs > 30000) {
112
+ try { process.kill(pid, 0); } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ESRCH') { unlinkSync(this.lockPath); continue; } }
113
+ }
114
+ } catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') continue; }
115
+ if (Date.now() >= deadline) throw new Error('decision ledger lock timeout');
116
+ Bun.sleepSync(20);
117
+ }
118
+ }
119
+ try { writeSync(fd, `${process.pid}`); return fn(); }
120
+ finally { closeSync(fd); unlinkSync(this.lockPath); }
121
+ }
122
+ private events(): Event[] {
123
+ if (!existsSync(this.path)) return [];
124
+ const text = readFileSync(this.path, 'utf8');
125
+ if (text && !text.endsWith('\n')) throw new Error('incomplete decision ledger line');
126
+ return text.split('\n').filter(Boolean).map((line, i) => {
127
+ try { return JSON.parse(line) as Event; }
128
+ catch { throw new Error(`invalid decision ledger event on line ${i + 1}`); }
129
+ });
130
+ }
131
+ private append(event: Event): void { this.appendBatch([event]); }
132
+ private appendBatch(events: Event[]): void {
133
+ const fd = openSync(this.path, 'a', 0o600);
134
+ try {
135
+ const bytes = Buffer.from(events.map(event => JSON.stringify(event)).join('\n') + '\n', 'utf8');
136
+ const size = fstatSync(fd).size;
137
+ try {
138
+ if (writeSync(fd, bytes) !== bytes.length) throw new Error('short decision ledger write');
139
+ } catch (error) {
140
+ try { ftruncateSync(fd, size); } catch { /* Keep the original write failure. */ }
141
+ throw error;
142
+ }
143
+ } finally { closeSync(fd); }
144
+ }
145
+ list(filters: { status?: 'open' | 'decided' | 'all'; since?: string; version?: string; decidedBy?: 'human' | 'auto'; category?: DecisionCategory } = {}): DecisionEntry[] {
146
+ if (filters.since && !Number.isFinite(Date.parse(filters.since))) throw new Error('invalid since date');
147
+ if (filters.status && !['open', 'decided', 'all'].includes(filters.status)) throw new Error('invalid status');
148
+ if (filters.decidedBy && !['human', 'auto'].includes(filters.decidedBy)) throw new Error('invalid decided-by');
149
+ if (filters.category && !CATEGORIES.includes(filters.category)) throw new Error('invalid category');
150
+ const map = new Map<string, DecisionEntry>();
151
+ for (const event of this.events()) {
152
+ if (event.type === 'raised') {
153
+ if (map.has(event.entry.id)) throw new Error(`duplicate decision: ${event.entry.id}`);
154
+ map.set(event.entry.id, event.entry);
155
+ } else {
156
+ const old = map.get(event.id);
157
+ if (!old || old.status !== 'open') throw new Error(`invalid decision transition: ${event.id}`);
158
+ if (event.type === 'options-added') {
159
+ if (old.options.length || event.options.length < 2) throw new Error(`invalid options transition: ${event.id}`);
160
+ map.set(event.id, { ...old, options: event.options, history: [...old.history, { type: 'options-added', at: event.at, by: event.by }] });
161
+ continue;
162
+ }
163
+ if (event.type === 'decided' && (event.choice ? !old.options.some(option => option.key === event.choice) : old.options.length !== 0 || !old.refs?.length)) throw new Error(`invalid decision option: ${event.id}`);
164
+ if (event.type === 'decided') map.set(event.id, { ...old, status: 'decided', ...(event.at ? { decidedAt: event.at } : {}), decidedBy: event.by, choice: event.choice,
165
+ ...(event.version ? { versionAtDecision: event.version } : {}), ...(event.note ? { note: event.note } : {}),
166
+ history: [...old.history, { type: 'decided', ...(event.at ? { at: event.at } : {}), by: event.by.kind, choice: event.choice, ...(event.version ? { version: event.version } : {}) }] });
167
+ else map.set(event.id, { ...old, status: 'withdrawn', withdrawnAt: event.at, withdrawReason: event.reason,
168
+ history: [...old.history, { type: 'withdrawn', at: event.at, by: 'agent', reason: event.reason, version: event.version }] });
169
+ }
170
+ }
171
+ return [...map.values()].filter(e => (filters.status === 'all' || e.status === (filters.status ?? 'open'))
172
+ && (!filters.since || (e.raisedAt !== undefined && e.raisedAt >= new Date(filters.since).toISOString()))
173
+ && (!filters.version || [e.version?.released, e.version?.dev, e.version?.codename, e.versionAtDecision?.released, e.versionAtDecision?.dev, e.versionAtDecision?.codename].some(v => v === filters.version || v?.startsWith(`${filters.version}-`)))
174
+ && (!filters.decidedBy || e.decidedBy?.kind === filters.decidedBy)
175
+ && (!filters.category || e.category === filters.category)).sort((a, b) => (b.raisedAt ?? b.importedAt ?? '').localeCompare(a.raisedAt ?? a.importedAt ?? '') || a.id.localeCompare(b.id));
176
+ }
177
+ show(id: string): DecisionEntry { const entry = this.list({ status: 'all' }).find(e => e.id === id); if (!entry) throw new Error(`decision not found: ${id}`); return entry; }
178
+ raise(input: RaiseInput): DecisionEntry {
179
+ const clean = validate(input);
180
+ return this.locked(() => {
181
+ const at = utc(clean.raisedAt ?? this.now().toISOString());
182
+ const day = at.slice(0, 10).replaceAll('-', '');
183
+ const prefix = `D-${day}-`;
184
+ let next = 1;
185
+ for (const entry of this.list({ status: 'all' })) {
186
+ if (entry.id.startsWith(prefix)) next = Math.max(next, Number(entry.id.slice(prefix.length)) + 1);
187
+ }
188
+ const id = `${prefix}${String(next).padStart(2, '0')}`;
189
+ const version = this.version(at);
190
+ const entry: DecisionEntry = { ...clean, id, raisedAt: at, version, status: 'open', history: [{ type: 'raised', at, by: clean.raisedBy.agent, version }] };
191
+ this.append({ type: 'raised', entry });
192
+ debug.log('decisions', 'raised', { id, category: entry.category, by: entry.raisedBy.agent });
193
+ return entry;
194
+ });
195
+ }
196
+ decide(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): DecisionEntry {
197
+ if (!by || (by.kind !== 'human' && by.kind !== 'auto')) throw new Error('decidedBy is required');
198
+ if (by.kind === 'auto') {
199
+ single(by.delegation, 'delegation'); single(by.agent, 'agent');
200
+ if (by.track && !TRACKS.includes(by.track)) throw new Error('invalid track');
201
+ by = { ...by, agent: safe(single(by.agent, 'agent')), delegation: safe(single(by.delegation, 'delegation')) };
202
+ }
203
+ return this.locked(() => {
204
+ const old = this.show(id);
205
+ if (old.status !== 'open') throw new Error(`decision already closed: ${id}`);
206
+ if (!old.options.some(o => o.key === choice)) throw new Error(`option not found: ${choice}`);
207
+ const at = utc(decidedAt ?? this.now().toISOString());
208
+ if (old.raisedAt && at < old.raisedAt) throw new Error('decision before raise');
209
+ this.append({ type: 'decided', id, at, by, choice, version: this.version(at), ...(note ? { note: safe(single(note, 'note')) } : {}) });
210
+ debug.log('decisions', 'decided', { id, category: old.category, by: by.kind });
211
+ return this.show(id);
212
+ });
213
+ }
214
+ /** Import an open historical request with known alternatives and an unknown original time. */
215
+ importOpen(input: RaiseInput): DecisionEntry {
216
+ const clean = validate(input);
217
+ if (!clean.refs?.length) throw new Error('historical source reference required');
218
+ return this.locked(() => {
219
+ const existing = this.list({ status: 'all' });
220
+ if (clean.refs?.some(ref => existing.some(e => e.refs?.includes(ref)))) throw new Error('source already imported');
221
+ const importedAt = this.now().toISOString();
222
+ const at = clean.raisedAt ? utc(clean.raisedAt) : undefined;
223
+ const prefix = `D-${(at ?? importedAt).slice(0, 10).replaceAll('-', '')}-`;
224
+ const next = existing.reduce((n, e) => e.id.startsWith(prefix) ? Math.max(n, Number(e.id.slice(prefix.length)) + 1) : n, 1);
225
+ const id = `${prefix}${String(next).padStart(2, '0')}`;
226
+ const version = at ? this.version(at) : undefined;
227
+ const entry: DecisionEntry = { ...clean, id, ...(at ? { raisedAt: at, version } : {}), importedAt, status: 'open',
228
+ history: [{ type: 'raised', ...(at ? { at, version } : {}), by: clean.raisedBy.agent }] };
229
+ this.append({ type: 'raised', entry });
230
+ debug.log('decisions', 'raised', { id, category: entry.category, by: entry.raisedBy.agent });
231
+ return entry;
232
+ });
233
+ }
234
+ /** Import an open historical request whose source contains no alternatives yet. */
235
+ importPending(input: RaiseInput): DecisionEntry {
236
+ if (!input.refs?.length || input.options.length !== 0) throw new Error('historical source without alternatives required');
237
+ const clean = validate(input, true);
238
+ return this.locked(() => {
239
+ const existing = this.list({ status: 'all' });
240
+ if (clean.refs?.some(ref => existing.some(e => e.refs?.includes(ref)))) throw new Error('source already imported');
241
+ const importedAt = this.now().toISOString();
242
+ const at = clean.raisedAt ? utc(clean.raisedAt) : undefined;
243
+ const prefix = `D-${(at ?? importedAt).slice(0, 10).replaceAll('-', '')}-`;
244
+ const next = existing.reduce((n, e) => e.id.startsWith(prefix) ? Math.max(n, Number(e.id.slice(prefix.length)) + 1) : n, 1);
245
+ const id = `${prefix}${String(next).padStart(2, '0')}`;
246
+ const version = at ? this.version(at) : undefined;
247
+ const entry: DecisionEntry = { ...clean, id, ...(at ? { raisedAt: at, version } : {}), importedAt, status: 'open', history: [{ type: 'raised', ...(at ? { at, version } : {}), by: clean.raisedBy.agent }] };
248
+ this.append({ type: 'raised', entry });
249
+ debug.log('decisions', 'raised', { id, category: entry.category, by: entry.raisedBy.agent });
250
+ return entry;
251
+ });
252
+ }
253
+ addOptions(id: string, options: DecisionOption[], agent: string): DecisionEntry {
254
+ if (!Array.isArray(options) || options.length < 2) throw new Error('at least two options required');
255
+ const clean = options.map(o => ({ key: single(o.key, 'option key'), label: safe(single(o.label, 'option label')), consequence: safe(single(o.consequence, 'option consequence')) }));
256
+ if (clean.some(o => !/^[a-z]$/.test(o.key)) || new Set(clean.map(o => o.key)).size !== clean.length) throw new Error('option keys must be unique lowercase letters');
257
+ return this.locked(() => {
258
+ const old = this.show(id);
259
+ if (old.status !== 'open' || old.options.length) throw new Error('options already present or decision closed');
260
+ this.append({ type: 'options-added', id, at: this.now().toISOString(), options: clean, by: safe(single(agent, 'agent')) });
261
+ return this.show(id);
262
+ });
263
+ }
264
+ /** Record a documented decision with unknown time; a failed import leaves no half-raised entry. */
265
+ importRecorded(input: RaiseInput, choice: string | undefined, by: DecisionActor, note: string): DecisionEntry {
266
+ if (!input.refs?.length) throw new Error('historical source reference required');
267
+ const clean = validate(input, true);
268
+ if (choice ? !clean.options.some(o => o.key === choice) : clean.options.length !== 0) throw new Error(`option not found: ${choice}`);
269
+ if (by.kind === 'auto') {
270
+ single(by.agent, 'agent'); single(by.delegation, 'delegation');
271
+ if (by.track && !TRACKS.includes(by.track)) throw new Error('invalid track');
272
+ by = { ...by, agent: safe(single(by.agent, 'agent')), delegation: safe(single(by.delegation, 'delegation')) };
273
+ }
274
+ const cleanNote = safe(single(note, 'note'));
275
+ return this.locked(() => {
276
+ const existing = this.list({ status: 'all' });
277
+ if (clean.refs?.some(ref => existing.some(e => e.refs?.includes(ref)))) throw new Error('source already imported');
278
+ const importedAt = this.now().toISOString();
279
+ const at = clean.raisedAt ? utc(clean.raisedAt) : undefined;
280
+ const prefix = `D-${(at ?? importedAt).slice(0, 10).replaceAll('-', '')}-`;
281
+ const next = existing.reduce((n, e) => e.id.startsWith(prefix) ? Math.max(n, Number(e.id.slice(prefix.length)) + 1) : n, 1);
282
+ const id = `${prefix}${String(next).padStart(2, '0')}`;
283
+ const version = at ? this.version(at) : undefined;
284
+ const entry: DecisionEntry = { ...clean, id, ...(at ? { raisedAt: at, version } : {}), importedAt, status: 'open',
285
+ history: [{ type: 'raised', ...(at ? { at, version } : {}), by: clean.raisedBy.agent }] };
286
+ this.appendBatch([{ type: 'raised', entry }, { type: 'decided', id, by, ...(choice ? { choice } : {}), note: cleanNote }]);
287
+ debug.log('decisions', 'raised', { id, category: entry.category, by: entry.raisedBy.agent });
288
+ debug.log('decisions', 'decided', { id, category: entry.category, by: by.kind });
289
+ return this.show(id);
290
+ });
291
+ }
292
+ /** Resume a partially imported historical item only if its recorded alternatives still match the source. */
293
+ completeRecorded(id: string, choice: string | undefined, by: DecisionActor, note: string, sourceOptions: DecisionOption[]): DecisionEntry {
294
+ if (by.kind === 'auto') {
295
+ single(by.agent, 'agent'); single(by.delegation, 'delegation');
296
+ if (by.track && !TRACKS.includes(by.track)) throw new Error('invalid track');
297
+ by = { ...by, agent: safe(single(by.agent, 'agent')), delegation: safe(single(by.delegation, 'delegation')) };
298
+ }
299
+ const cleanNote = safe(single(note, 'note'));
300
+ return this.locked(() => {
301
+ const entry = this.show(id);
302
+ if (entry.status !== 'open') throw new Error(`decision already closed: ${id}`);
303
+ if (JSON.stringify(entry.options) !== JSON.stringify(sourceOptions)) throw new Error('previously imported options differ from source');
304
+ if (choice ? !entry.options.some(o => o.key === choice) : entry.options.length !== 0) throw new Error(`option not found: ${choice}`);
305
+ this.append({ type: 'decided', id, by, ...(choice ? { choice } : {}), note: cleanNote });
306
+ debug.log('decisions', 'decided', { id, category: entry.category, by: by.kind });
307
+ return this.show(id);
308
+ });
309
+ }
310
+ withdraw(id: string, reason: string): DecisionEntry {
311
+ const clean = safe(single(reason, 'reason'));
312
+ return this.locked(() => {
313
+ const old = this.show(id);
314
+ if (old.status !== 'open') throw new Error(`decision already closed: ${id}`);
315
+ const at = this.now().toISOString();
316
+ this.append({ type: 'withdrawn', id, at, reason: clean, version: this.version(at) });
317
+ debug.log('decisions', 'withdrawn', { id, category: old.category, by: 'agent' });
318
+ return this.show(id);
319
+ });
320
+ }
321
+ }
322
+
323
+ export interface MarkdownImportResult { imported: string[]; existing: string[]; unread: string[]; incomplete: string[] }
324
+ // The document is a fixed historical seed, not a general Markdown grammar. Preserve each source reference and report every unparseable numbered row.
325
+ export function importDecisionMarkdown(ledger: DecisionLedger, path: string): MarkdownImportResult {
326
+ const source = readFileSync(path, 'utf8');
327
+ // Identify the source by its real file path so relative and absolute spellings of the same document share refs.
328
+ const sourceId = realpathSync(resolve(path));
329
+ const lines = source.split('\n');
330
+ const imported: string[] = [], existing: string[] = [], unread: string[] = [], incomplete: string[] = [];
331
+ const headings = lines.map((line, i) => ({ line, i })).filter(x => /^### (?:\(옛\) )?[ABC]\d+\.|^\| [D][1-5] \||^- \*\*E[1-4] /.test(x.line));
332
+ const seen = new Set<string>();
333
+ for (const { line, i } of headings) {
334
+ const match = /(?:^### (?:\(옛\) )?|^\| |^- \*\*)([A-E]\d+)/.exec(line);
335
+ if (!match) continue;
336
+ const code = match[1]!;
337
+ if (seen.has(code)) continue;
338
+ seen.add(code);
339
+ const ref = `${sourceId}#${code}`;
340
+ const previous = ledger.list({ status: 'all' }).find(e => e.refs?.includes(ref));
341
+ if (previous && (previous.status !== 'open' || !['C4', 'D1', 'D2', 'D3', 'D4', 'D5', 'E1', 'E2', 'E3', 'E4'].includes(code))) {
342
+ existing.push(code);
343
+ if (!previous.options.length) incomplete.push(`${code}: 원문에 선택지 없음; add-options 로 보완 가능`);
344
+ continue;
345
+ }
346
+ const end = lines.findIndex((l, n) => n > i && (/^### |^## |^---$/.test(l) || (code.startsWith('D') && /^\| D\d+ \|/.test(l)) || (code.startsWith('E') && /^- \*\*E\d+ /.test(l))));
347
+ const block = lines.slice(i, end < 0 ? undefined : end).join('\n');
348
+ const oldHeading = !['C4', 'C5'].includes(code) || line.startsWith('### (옛)') ? -1 : lines.findIndex(l => l.startsWith(`### (옛) ${code}.`));
349
+ const oldEnd = oldHeading < 0 ? -1 : lines.findIndex((l, n) => n > oldHeading && (/^### |^## |^---$/.test(l)));
350
+ const optionBlock = oldHeading < 0 ? block : `${block}\n${lines.slice(oldHeading, oldEnd < 0 ? undefined : oldEnd).join('\n')}`;
351
+ try {
352
+ const cells = code.startsWith('D') ? line.split('|').map(s => s.trim()) : [];
353
+ const text = (code.startsWith('D') ? cells[2] : line.replace(/^### (?:\(옛\) )?[ABC]\d+\.\s*|^- \*\*E\d+\s*/, '')).replace(/\*\*|`/g, '').replace(/\s+/g, ' ').trim();
354
+ const title = text.split(/[.!?。!?](?:\s|$)/)[0]!.slice(0, 120);
355
+ const context = (code.startsWith('D') ? cells[3] : block.split('\n').slice(1).filter(l => l.startsWith('- ')).map(l => l.replace(/^- \*\*[^*]+\*\*:\s*/, '')).find(Boolean)) ?? text;
356
+ const shortContext = context.replace(/`[^`]+`/g, '관련 경로').split(/[.!?。!?]/)[0]!.slice(0, 220);
357
+ const category: DecisionCategory = /키|토큰|로그인/.test(title) ? 'secret' : /게시|공개|릴리스|티저|npm/.test(title) ? 'publish' : /구매|유료|결제|크레딧/.test(title) ? 'money' : /인가|인증|샌드박스/.test(title) ? 'security' : /범위|0\.2\.7|루프/.test(title) ? 'scope' : 'other';
358
+ const explicitOptions = /\*\*선택지\*\*: ([^\n]+)/.exec(optionBlock)?.[1];
359
+ const choices = explicitOptions ? [...explicitOptions.matchAll(/([ⓐⓑⓒ])\s*([^ⓐⓑⓒ]+)/g)].map((m) => {
360
+ const source = m[2]!.trim();
361
+ const [label, consequence] = source.split(/\s*[—–]\s*/, 2);
362
+ return { key: ({ 'ⓐ': 'a', 'ⓑ': 'b', 'ⓒ': 'c' } as Record<string, string>)[m[1]!]!, label: label!.slice(0, 120), consequence: (consequence ?? '원문에 결과 미기재').slice(0, 240) };
363
+ }) : [];
364
+ if (choices.length === 1) throw new Error('source specifies only one option');
365
+ const options = choices;
366
+ const rec = /\*\*🅢 권고\*\*: ([^\n]+)/.exec(optionBlock)?.[1];
367
+ const recommendedKey = rec && options.find(o => rec.includes(o.key === 'a' ? 'ⓐ' : o.key === 'b' ? 'ⓑ' : 'ⓒ'))?.key;
368
+ const recommendation: Recommendation = rec && recommendedKey
369
+ ? { option: recommendedKey, why: rec.slice(0, 240) }
370
+ : { skipped: true, reason: rec ? `원문 권고(선택지 키 미상): ${rec.slice(0, 180)}` : '원문에 권고가 없음 (가져오면서 생성하지 않음)' };
371
+ const historical = ['C4', ...Array.from({ length: 5 }, (_, n) => `D${n + 1}`), ...Array.from({ length: 4 }, (_, n) => `E${n + 1}`)].includes(code);
372
+ // C and A come from the source text only; a field the source does not state stays «미상» rather than describing the import itself.
373
+ const complicationLine = code.startsWith('D') ? cells[3] : block.split('\n').find(l => /^- \*\*(왜|무엇이 막혀 있나|지금)\*\*/.test(l))?.replace(/^- \*\*[^*]+\*\*:\s*/, '');
374
+ const firstSentence = (value: string) => value.replace(/\*\*|`/g, '').replace(/\s+/g, ' ').trim().split(/[.!?。!?](?:\s|$)/)[0]!.slice(0, 220);
375
+ const complication = complicationLine ? firstSentence(complicationLine) : '미상(원문에 따로 적힌 문제 없음)';
376
+ const answer = historical ? firstSentence(text) || '미상(원문에서 결정 문장을 읽지 못함)' : undefined;
377
+ const input: RaiseInput = { title, category, scqa: { s: shortContext, c: complication, ...(answer ? { a: answer } : {}) }, options, recommendation, raisedBy: { agent: 'unknown', track: 'S' }, refs: [ref] };
378
+ if (historical) {
379
+ // Only C4 states both alternatives and a chosen key; D/E record decisions without fabricating options or timestamps.
380
+ const choice = code === 'C4' && /L16\s*→\s*\*\*0\.2\.7 N1\*\*/.test(line) && options.some(o => o.key === 'b' && /0\.2\.7/.test(o.label)) ? 'b' : undefined;
381
+ if (code === 'C4' && !choice) throw new Error('source does not identify a documented option and chosen key');
382
+ const by: DecisionActor = code.startsWith('D') ? { kind: 'auto', agent: 'unknown', track: 'S', delegation: '대표 09-30 01:4x 결정사항 S 에게' } : { kind: 'human' };
383
+ if (previous) { ledger.completeRecorded(previous.id, choice, by, `문서 ${code}: ${text}`, options); existing.push(code); }
384
+ else { ledger.importRecorded(input, choice, by, `문서 ${code}: ${text}`); imported.push(code); }
385
+ } else if (!options.length) { ledger.importPending(input); imported.push(code); }
386
+ else { ledger.importOpen(input); imported.push(code); }
387
+ if (!options.length) incomplete.push(`${code}: 원문에 선택지 없음; ${historical ? '선택 키 미상' : 'add-options 로 보완 후 결정 가능'}`);
388
+ } catch (error) {
389
+ if (error instanceof Error && (error.message === 'source already imported' || (error.message.startsWith('decision already closed:') && previous?.status === 'open'))) {
390
+ const concurrent = ledger.list({ status: 'all' }).find(e => e.refs?.includes(ref));
391
+ if (concurrent && (error.message === 'source already imported' || concurrent.status === 'decided')) {
392
+ existing.push(code);
393
+ if (!concurrent.options.length) incomplete.push(`${code}: 원문에 선택지 없음; add-options 로 보완 가능`);
394
+ continue;
395
+ }
396
+ }
397
+ unread.push(`${code}: ${error instanceof Error ? error.message : String(error)}`);
398
+ }
399
+ }
400
+ for (const code of ['A1','A2', ...Array.from({ length: 5 }, (_, n) => `B${n+1}`), ...Array.from({ length: 7 }, (_, n) => `C${n+1}`), ...Array.from({ length: 5 }, (_, n) => `D${n+1}`), ...Array.from({ length: 4 }, (_, n) => `E${n+1}`)]) {
401
+ if (!seen.has(code)) unread.push(`${code}: source item not found`);
402
+ }
403
+ return { imported, existing, unread, incomplete };
404
+ }
@@ -14,6 +14,7 @@ import { openSurfaceEventsDb, recordEvent } from './surface-events.js';
14
14
  import { latestUserIntentTs } from '../user-intent/index.js';
15
15
  import { getUserConfig } from '../user-config.js';
16
16
  import { resolveChannelBotToken } from '../channel-bot-token.js';
17
+ import { kindRouteTarget } from './telegram-kind-route.js';
17
18
  import { debug } from '../debug/log.js';
18
19
  import { resolveDaemonEndpoint } from '../nexus/daemon-endpoint.js';
19
20
  // ★ origin 되돌림(대표 2026-07-12) — 미션 알림을 발신 채널(메인 Q&A 봇)로 되돌린다. type-only
@@ -115,10 +116,10 @@ function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null
115
116
  mkdirSync(dirname(path), { recursive: true });
116
117
  const rec = { ts: new Date().toISOString(), kind, text, ...(origin ? { origin } : {}) };
117
118
  appendFileSync(path, JSON.stringify(rec) + '\n');
118
- console.log(`[outbound] 야간 무음(00:00~06:30 KST) — 보류 적재 (${kind})`);
119
+ console.error(`[outbound] 야간 무음(00:00~06:30 KST) — 보류 적재 (${kind})`);
119
120
  logSend('deferred', kind, { reason: 'quiet-hours' }); // 밀림 적재 관측
120
121
  } catch (e) {
121
- console.log(`[outbound] 보류 적재 실패 — 콘솔 출력\n${text}`, e instanceof Error ? e.message : '');
122
+ console.error(`[outbound] 보류 적재 실패 — 콘솔 출력\n${text}`, e instanceof Error ? e.message : '');
122
123
  }
123
124
  }
124
125
 
@@ -237,7 +238,7 @@ export function sendOutbound(text: string, kind = 'alert', origin?: MissionOrigi
237
238
  return true; // 보류 = 수락 (호출측 재시도/에러 루프 방지)
238
239
  }
239
240
  const bypass = inQuietHours();
240
- if (bypass) console.log('[outbound] 야간 무음 우회 — 최근 사용자 활동(깨어있음) → 즉시 발송 + 보류분 flush');
241
+ if (bypass) console.error('[outbound] 야간 무음 우회 — 최근 사용자 활동(깨어있음) → 즉시 발송 + 보류분 flush');
241
242
  try { flushDeferred(); } catch { /* fail-soft */ }
242
243
  // ★ 발송 관측(대표 지시) — 발송 시각·mode·밀림(burst) 판정을 logs.db 에. burst=최근 2분 5건+
243
244
  // (몰려 나가는 중 = 밀림 의심). `elanous logs --category outbound.send` 로 실시간/밀림 구분.
@@ -274,7 +275,7 @@ export function classifyDaemonResponse(j: unknown): DaemonPathClass {
274
275
  function logDaemonPath(classification: DaemonPathClass, kind: string, extra: Record<string, unknown> = {}): void {
275
276
  try { debug.log('outbound.send', 'daemon-path', { classification, kind, ...extra }); } catch { /* fail-soft */ }
276
277
  if (classification === 'ok') return;
277
- try { console.log(`[outbound] daemon-path ${classification}`); } catch { /* fail-soft */ }
278
+ try { console.error(`[outbound] daemon-path ${classification}`); } catch { /* fail-soft */ }
278
279
  }
279
280
 
280
281
  /** elanous `/v1/outbound` 우선 → 실패 시 텔레그램 직접. 성공 경로 반환(원장 중복방지용). */
@@ -295,7 +296,7 @@ export function deliver(text: string, kind = 'alert'): 'daemon' | 'direct' | fal
295
296
  logDaemonPath(classification, kind, extra);
296
297
  }
297
298
  // 2) fallback: 텔레그램 sendMessage 직접(3900자 분할) — 데몬 미경유라 클라가 원장 기록.
298
- return sendTelegramDirect(text) ? 'direct' : false;
299
+ return sendTelegramDirect(text, kind) ? 'direct' : false;
299
300
  }
300
301
 
301
302
  /** 텔레그램 raw 발송(토큰·chatId 명시) — spill + 3900자 분할(줄 경계). thread 지원. */
@@ -320,7 +321,12 @@ function sendTelegramRaw(token: string, chatId: string | number, text: string, t
320
321
  }
321
322
 
322
323
  /** 텔레그램 직접 발송(TELEGRAM_BOT_TOKEN/CHAT_ID — env 우선, 없으면 CONATUS/.env). report 폴백. */
323
- function sendTelegramDirect(text: string): boolean {
324
+ function sendTelegramDirect(text: string, kind?: string): boolean {
325
+ // A purpose kind with a channel role (intake · ops-*) goes to that channel even when the daemon is down.
326
+ try {
327
+ const target = kindRouteTarget(getUserConfig(), kind);
328
+ if (target) return sendTelegramRaw(target.botToken, String(target.chatId), text);
329
+ } catch { /* fall through to the legacy direct path */ }
324
330
  let tok = process.env.TELEGRAM_BOT_TOKEN || '';
325
331
  let chat = process.env.TELEGRAM_CHAT_ID || '';
326
332
  if (!tok || !chat) {
@@ -328,6 +334,6 @@ function sendTelegramDirect(text: string): boolean {
328
334
  tok = tok || env.TELEGRAM_BOT_TOKEN || '';
329
335
  chat = chat || env.TELEGRAM_CHAT_ID || '';
330
336
  }
331
- if (!tok || !chat) { console.log('[outbound] 토큰/chat 미설정 — 콘솔 출력\n' + text); return false; }
337
+ if (!tok || !chat) { console.error('[outbound] 토큰/chat 미설정 — 콘솔 출력\n' + text); return false; }
332
338
  return sendTelegramRaw(tok, chat, text);
333
339
  }
@@ -0,0 +1,21 @@
1
+ // 발송 목적(kind) → 텔레그램 채널 역할 · 대상. 가벼운 모듈(telegram.ts 를 import 하지 않는다 — outbound-alert 순환 방지).
2
+ // 표에 없는 kind 는 undefined → 부르는 쪽이 옛 reportChannel 로 간다(투자 알림 등 기존 동작 그대로).
3
+ import type { UserConfig } from '../user-config.js';
4
+ import { channelForRole, resolveTelegramChannels } from './telegram-channels.js';
5
+
6
+ export const DEFAULT_KIND_ROLES: Readonly<Record<string, string>> = {
7
+ intake: 'system', 'ops-report': 'system', 'ops-alert': 'system', 'ops-health': 'system',
8
+ };
9
+
10
+ export function roleForKind(cfg: UserConfig, kind?: string): string | undefined {
11
+ if (!kind) return undefined;
12
+ return cfg.telegram.kindRoles?.[kind] ?? DEFAULT_KIND_ROLES[kind];
13
+ }
14
+
15
+ /** The channel for a purpose kind — only with explicit `telegram.channels` and a mapped role. */
16
+ export function kindRouteTarget(cfg: UserConfig, kind?: string): { botToken: string; chatId: number } | null {
17
+ const role = roleForKind(cfg, kind);
18
+ if (!role || !cfg.telegram.channels?.length) return null;
19
+ const ch = channelForRole(resolveTelegramChannels(cfg.telegram), role);
20
+ return ch?.botToken && Number.isFinite(ch.chatId) ? { botToken: ch.botToken, chatId: ch.chatId } : null;
21
+ }
@@ -0,0 +1,139 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { debug } from '../debug/log.js';
3
+ import { emitDecision, type DecisionEvent } from '../live/detail-switch.js';
4
+ import { CardStore, redactSecrets } from '../task-cards/card-store.js';
5
+ import type { DevPipelineSpec } from '../self-dev/dev-pipeline.js';
6
+ import {
7
+ budgetGate, placementGate, relationGate, memoryGate,
8
+ type BudgetGateDeps, type GateResult, type MemoryDecision, type MemoryGateDeps, type PlacementGateDeps,
9
+ type PlacementGateInput, type RelationGateDeps,
10
+ } from './gates.js';
11
+ import { summarizeGateDecision, type GateName } from './gate-decision-summary.js';
12
+
13
+ export interface DispatchTaskInput {
14
+ goalId: string;
15
+ title: string;
16
+ goalText: string;
17
+ targetPaths: readonly string[];
18
+ spec: DevPipelineSpec;
19
+ runKey?: string;
20
+ }
21
+
22
+ export type ObservedGateResult = GateResult<unknown>;
23
+ export interface DispatchTaskResult {
24
+ cardId: string;
25
+ decisions: Record<GateName, ObservedGateResult>;
26
+ mode: 'observe';
27
+ }
28
+
29
+ export interface DispatchTaskDeps {
30
+ createStore?: () => CardStore;
31
+ budgetGate?: typeof budgetGate;
32
+ placementGate?: typeof placementGate;
33
+ relationGate?: typeof relationGate;
34
+ memoryGate?: (card: Parameters<typeof memoryGate>[0], deps?: MemoryGateDeps) => Promise<GateResult<MemoryDecision | string>>;
35
+ budgetDeps?: BudgetGateDeps;
36
+ placementDeps?: PlacementGateDeps;
37
+ relationDeps?: RelationGateDeps;
38
+ memoryDeps?: MemoryGateDeps;
39
+ emitDecision?: (event: DecisionEvent) => unknown;
40
+ /** Called as soon as the card exists, before any gate can reach its deadline. */
41
+ onCardCreated?: (cardId: string) => void;
42
+ log?: (gate: GateName, data: { cardId: string; verdict: string; ms: number }) => void;
43
+ /** Cancels pending observations and settles dispatch before any further card writes. */
44
+ signal?: AbortSignal;
45
+ /** Optional shorter deadline for tests; never extends the five-second cap. */
46
+ timeoutMs?: number;
47
+ }
48
+
49
+ const UNAVAILABLE = '측정 불가';
50
+
51
+ function verdictFor(gate: GateName, result: ObservedGateResult): string {
52
+ if (result.decision === UNAVAILABLE) return UNAVAILABLE;
53
+ const decision = result.decision as Record<string, unknown> | null;
54
+ const key = gate === 'budget' ? 'action' : gate === 'placement' ? 'substrate' : 'action';
55
+ return typeof decision?.[key] === 'string' ? String(decision[key]) : 'unknown';
56
+ }
57
+
58
+ /** A goal id is an identifier, not a secret: secret-shape redaction turned every long hex id
59
+ * (e.g. `request-<sha256>`) into the same `<redacted>` and merged all goals into one card.
60
+ * Keep ids that use the identifier alphabet; anything else becomes a stable hash. */
61
+ export function cardGoalId(goalId: string): string {
62
+ return /^[A-Za-z0-9._:-]{1,128}$/.test(goalId) ? goalId : `goal-${createHash('sha256').update(goalId).digest('hex').slice(0, 32)}`;
63
+ }
64
+
65
+ /** Observation only: neither the decision nor a failure can select the launch, substrate or model. */
66
+ export async function dispatchTask(input: DispatchTaskInput, deps: DispatchTaskDeps = {}): Promise<DispatchTaskResult> {
67
+ const timeoutMs = Math.min(5_000, Math.max(0, deps.timeoutMs ?? 5_000));
68
+ const deadline = Date.now() + timeoutMs;
69
+ if (deps.signal?.aborted) throw new Error('dispatch observation aborted');
70
+ const store = (deps.createStore ?? (() => new CardStore()))();
71
+ try {
72
+ if (deps.signal?.aborted) throw new Error('dispatch observation aborted');
73
+ const card = store.createCard({ goalId: cardGoalId(input.goalId), title: redactSecrets(input.title) });
74
+ if (card.status !== 'open') throw new Error(`Card is closed: ${card.id}`);
75
+ try { deps.onCardCreated?.(card.id); } catch { /* Ledger observation cannot stop dispatch. */ }
76
+ const runKey = redactSecrets(input.runKey ?? randomUUID());
77
+ const relationDeps: RelationGateDeps = deps.relationDeps ?? {
78
+ openCards: () => store.listCards({ open: true }),
79
+ priorTermination: () => undefined,
80
+ // No active-run source is wired here yet: keep that one field unknown and still measure the rest.
81
+ activeRuns: () => 'unknown',
82
+ };
83
+ const placement: PlacementGateInput = { goal: input.goalText };
84
+ const gates: Record<GateName, () => Promise<ObservedGateResult> | ObservedGateResult> = {
85
+ budget: () => (deps.budgetGate ?? budgetGate)(deps.budgetDeps),
86
+ placement: () => (deps.placementGate ?? placementGate)(placement, deps.placementDeps),
87
+ relation: () => (deps.relationGate ?? relationGate)(card, input.targetPaths, relationDeps),
88
+ memory: () => (deps.memoryGate ?? memoryGate)(card, deps.memoryDeps),
89
+ };
90
+ const results = await Promise.all((Object.keys(gates) as GateName[]).map(async (gate) => {
91
+ const started = Date.now();
92
+ const cap = Math.max(0, deadline - started);
93
+ let timer: ReturnType<typeof setTimeout> | undefined;
94
+ let onAbort: (() => void) | undefined;
95
+ let result: ObservedGateResult;
96
+ try {
97
+ result = await Promise.race([
98
+ Promise.resolve().then(() => {
99
+ if (deps.signal?.aborted) throw new Error('dispatch observation aborted');
100
+ return gates[gate]();
101
+ }),
102
+ new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), cap); }),
103
+ new Promise<never>((_, reject) => {
104
+ onAbort = () => reject(new Error('dispatch observation aborted'));
105
+ deps.signal?.addEventListener('abort', onAbort, { once: true });
106
+ if (deps.signal?.aborted) onAbort();
107
+ }),
108
+ ]);
109
+ } catch (error) {
110
+ result = { decision: UNAVAILABLE, explanation: `${UNAVAILABLE}: ${error instanceof Error ? error.message : String(error)}` };
111
+ } finally {
112
+ if (timer) clearTimeout(timer);
113
+ if (onAbort) deps.signal?.removeEventListener('abort', onAbort);
114
+ }
115
+ if (deps.signal?.aborted) return [gate, result] as const;
116
+ const ms = Date.now() - started;
117
+ const verdict = verdictFor(gate, result);
118
+ try { (deps.log ?? ((name, data) => debug.log('execution-loop.gate', name, data)))(gate, { cardId: card.id, verdict, ms }); }
119
+ catch { /* Observability must not change the launch. */ }
120
+ try {
121
+ (deps.emitDecision ?? emitDecision)({
122
+ kind: gate === 'placement' ? 'ROUTE' : 'PLAN', what: `execution-loop ${gate} gate`,
123
+ reason: summarizeGateDecision(gate, result), purpose: 'observe dispatch only', target: card.id,
124
+ ...(input.spec.runId ? { runId: input.spec.runId } : {}), phase: 'dispatch',
125
+ });
126
+ } catch { /* Live-detail logging is optional. */ }
127
+ return [gate, result] as const;
128
+ }));
129
+ if (deps.signal?.aborted) throw new Error('dispatch observation aborted');
130
+ const decisions = Object.fromEntries(results) as Record<GateName, ObservedGateResult>;
131
+ const content = (value: unknown) => redactSecrets(JSON.stringify(value));
132
+ store.appendSection(card.id, { key: `gates:${runKey}`, owner: 'execution-loop', content: content({ budget: decisions.budget, placement: decisions.placement }) });
133
+ store.appendSection(card.id, { key: `relations:${runKey}`, owner: 'execution-loop', content: content(decisions.relation) });
134
+ store.appendSection(card.id, { key: `memory:${runKey}`, owner: 'execution-loop', content: content(decisions.memory) });
135
+ return { cardId: card.id, decisions, mode: 'observe' };
136
+ } finally {
137
+ store.close();
138
+ }
139
+ }