elanous 0.2.7 → 0.2.9

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 (296) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-f972fc1c00a62e88.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-5f30e00693527ebd.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-80497e93a878ddc5.js → page-7fbae173b3c103a7.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-3b4edcce364062ea.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-016e2bda04eecb1f.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-b4d6d9a3b56f2940.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-037f83e2a1edc8e2.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-adfbd2ba9598e59a.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-1a8e4807292f6b17.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-c71c33a573230672.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-0df30e0c7e50148a.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-c1c95739c0bff3b8.js → page-369846d2921af584.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/trace/{page-71d18eec33b9c667.js → page-77b28bfb0996d5ea.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-2c4e59079ee357ac.js → page-90670589b87abea8.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-4498c3853f4f688d.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
  51. package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +1 -0
  52. package/apps/pwa/out/approvals/index.html +1 -1
  53. package/apps/pwa/out/approvals/index.txt +30 -29
  54. package/apps/pwa/out/autopilot/index.html +1 -1
  55. package/apps/pwa/out/autopilot/index.txt +30 -29
  56. package/apps/pwa/out/board/index.html +1 -1
  57. package/apps/pwa/out/board/index.txt +30 -29
  58. package/apps/pwa/out/botlab/index.html +1 -1
  59. package/apps/pwa/out/botlab/index.txt +27 -26
  60. package/apps/pwa/out/bots/index.html +1 -1
  61. package/apps/pwa/out/bots/index.txt +27 -26
  62. package/apps/pwa/out/chat/index.html +1 -1
  63. package/apps/pwa/out/chat/index.txt +27 -26
  64. package/apps/pwa/out/consult/index.html +1 -1
  65. package/apps/pwa/out/consult/index.txt +30 -29
  66. package/apps/pwa/out/control/index.html +1 -1
  67. package/apps/pwa/out/control/index.txt +30 -29
  68. package/apps/pwa/out/design-check/index.html +1 -1
  69. package/apps/pwa/out/design-check/index.txt +30 -29
  70. package/apps/pwa/out/editor/index.html +1 -1
  71. package/apps/pwa/out/editor/index.txt +30 -29
  72. package/apps/pwa/out/exec/index.html +1 -1
  73. package/apps/pwa/out/exec/index.txt +30 -29
  74. package/apps/pwa/out/field/index.html +1 -1
  75. package/apps/pwa/out/field/index.txt +30 -29
  76. package/apps/pwa/out/healthz +1 -1
  77. package/apps/pwa/out/index.html +1 -1
  78. package/apps/pwa/out/index.txt +27 -26
  79. package/apps/pwa/out/intake/index.html +1 -1
  80. package/apps/pwa/out/intake/index.txt +30 -29
  81. package/apps/pwa/out/live/index.html +1 -1
  82. package/apps/pwa/out/live/index.txt +30 -29
  83. package/apps/pwa/out/market/index.html +1 -1
  84. package/apps/pwa/out/market/index.txt +27 -26
  85. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  86. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  87. package/apps/pwa/out/morning/index.html +1 -1
  88. package/apps/pwa/out/morning/index.txt +30 -29
  89. package/apps/pwa/out/observatory/index.html +1 -1
  90. package/apps/pwa/out/observatory/index.txt +30 -29
  91. package/apps/pwa/out/ops/checklist/index.html +1 -0
  92. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  93. package/apps/pwa/out/ops/release/index.html +1 -0
  94. package/apps/pwa/out/ops/release/index.txt +30 -0
  95. package/apps/pwa/out/reflection/index.html +1 -1
  96. package/apps/pwa/out/reflection/index.txt +28 -27
  97. package/apps/pwa/out/scheduler/index.html +1 -1
  98. package/apps/pwa/out/scheduler/index.txt +30 -29
  99. package/apps/pwa/out/sessions/index.html +1 -1
  100. package/apps/pwa/out/sessions/index.txt +27 -26
  101. package/apps/pwa/out/settings/devices/index.html +1 -1
  102. package/apps/pwa/out/settings/devices/index.txt +30 -29
  103. package/apps/pwa/out/settings/index.html +1 -1
  104. package/apps/pwa/out/settings/index.txt +30 -29
  105. package/apps/pwa/out/setup/done/index.html +1 -1
  106. package/apps/pwa/out/setup/done/index.txt +30 -29
  107. package/apps/pwa/out/setup/index.html +1 -1
  108. package/apps/pwa/out/setup/index.txt +30 -29
  109. package/apps/pwa/out/share/index.html +1 -1
  110. package/apps/pwa/out/share/index.txt +30 -29
  111. package/apps/pwa/out/showroom/index.html +1 -1
  112. package/apps/pwa/out/showroom/index.txt +27 -26
  113. package/apps/pwa/out/tasks/index.html +1 -1
  114. package/apps/pwa/out/tasks/index.txt +30 -29
  115. package/apps/pwa/out/term/index.html +1 -1
  116. package/apps/pwa/out/term/index.txt +30 -29
  117. package/apps/pwa/out/trace/index.html +1 -1
  118. package/apps/pwa/out/trace/index.txt +30 -29
  119. package/apps/pwa/out/vault/index.html +1 -1
  120. package/apps/pwa/out/vault/index.txt +30 -29
  121. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  122. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  123. package/apps/pwa/out/workflows/index.html +1 -1
  124. package/apps/pwa/out/workflows/index.txt +30 -29
  125. package/apps/pwa/out/workspace/index.html +1 -1
  126. package/apps/pwa/out/workspace/index.txt +30 -29
  127. package/apps/pwa/out/worktrees/index.html +1 -1
  128. package/apps/pwa/out/worktrees/index.txt +30 -29
  129. package/graphs/release/recipes.yaml +3 -0
  130. package/graphs/release/release-loop.yaml +3 -1
  131. package/graphs/test-diet/nightly-audit.yaml +18 -0
  132. package/graphs/test-diet/recipes.yaml +12 -0
  133. package/graphs/test-diet/test-diet.yaml +22 -0
  134. package/package.json +2 -1
  135. package/persona-presets/_index.yaml +10 -0
  136. package/persona-presets/_schema.json +146 -0
  137. package/persona-presets/dana.yaml +26 -0
  138. package/persona-presets/haru.yaml +27 -0
  139. package/persona-presets/kai.yaml +27 -0
  140. package/persona-presets/leo.yaml +27 -0
  141. package/persona-presets/mira.yaml +30 -0
  142. package/persona-presets/nari.yaml +27 -0
  143. package/persona-presets/sora.yaml +27 -0
  144. package/persona-presets/yuna.yaml +27 -0
  145. package/plugins/doc-draft/graphs/run-step.ts +1 -1
  146. package/plugins/geo-check/graphs/run-step.ts +34 -10
  147. package/scripts/field-feed/deliver-node.ts +8 -3
  148. package/scripts/field-feed/lib.ts +2 -0
  149. package/scripts/install.sh +20 -11
  150. package/scripts/persona-presets/lint.ts +79 -0
  151. package/scripts/release-loop/checklist-gate-node.ts +2 -2
  152. package/scripts/release-loop/cutoff.ts +20 -2
  153. package/scripts/release-loop/export-check-node.ts +85 -0
  154. package/scripts/release-loop/gate-node.ts +45 -7
  155. package/scripts/release-loop/node-verdict.ts +8 -1
  156. package/scripts/release-loop/npm-publish-node.ts +10 -4
  157. package/scripts/release-loop/ops-upgrade-node.ts +11 -3
  158. package/scripts/release-loop/publish-node.ts +23 -1
  159. package/scripts/release-loop/shard-plan.ts +17 -0
  160. package/scripts/storyboard/storyboard.ts +167 -0
  161. package/scripts/test-diet/lib.ts +89 -0
  162. package/scripts/test-diet/node.ts +144 -0
  163. package/src/acp/auth-methods.ts +30 -0
  164. package/src/acp/server.ts +5 -5
  165. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  166. package/src/auth/setup-link-tokens.ts +153 -0
  167. package/src/budget/unified-usage.ts +75 -0
  168. package/src/channel-bot-token.ts +37 -10
  169. package/src/channel-import/detect.ts +191 -0
  170. package/src/channel-import/run.ts +111 -0
  171. package/src/chat/index.ts +9 -9
  172. package/src/cli/config-test-sync.ts +4 -0
  173. package/src/cli/decisions-cli.ts +13 -3
  174. package/src/cli/doctor-bundle.ts +128 -0
  175. package/src/cli/doctor-cli.ts +36 -1
  176. package/src/cli/doctor-local-llm.ts +95 -0
  177. package/src/cli/import-cli.ts +60 -0
  178. package/src/cli/mcp-login.ts +13 -4
  179. package/src/cli/op-proxy-sync.ts +83 -0
  180. package/src/cli/persona-cli.ts +116 -0
  181. package/src/cli/pr-cli.ts +6 -2
  182. package/src/cli/release-cli.ts +49 -8
  183. package/src/cli/skills-cli.ts +173 -0
  184. package/src/cli/start-cli.ts +13 -4
  185. package/src/codex/setup.ts +15 -13
  186. package/src/dashboard/dashboard-status-lines.ts +45 -0
  187. package/src/dashboard/first-screen-band.ts +35 -0
  188. package/src/dashboard/index.ts +87 -14
  189. package/src/dashboard/provider-slash-runtime.ts +28 -11
  190. package/src/dashboard/slash-runtime/dashboard-handlers.ts +12 -2
  191. package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
  192. package/src/dashboard/turn-stream-formatter.ts +1 -1
  193. package/src/decisions/decision-cards.ts +291 -0
  194. package/src/decisions/decision-ledger.ts +5 -2
  195. package/src/decisions/discord-decision-cards.ts +133 -0
  196. package/src/decisions/telegram-decision-cards.ts +100 -0
  197. package/src/directives/version-at.ts +77 -0
  198. package/src/discord.ts +17 -0
  199. package/src/domains/outbound-alert.ts +44 -1
  200. package/src/domains/repeated-failure.ts +1 -1
  201. package/src/error-report/report.ts +200 -0
  202. package/src/exec-requests/planner.ts +8 -2
  203. package/src/exec-requests/runner.ts +64 -24
  204. package/src/exec-requests/store.ts +2 -0
  205. package/src/field/field-feed-auto.ts +48 -0
  206. package/src/field/field-reel.ts +8 -1
  207. package/src/hooks/error-report.ts +210 -0
  208. package/src/hooks/receiver.ts +41 -1
  209. package/src/import/apply.ts +128 -0
  210. package/src/import/plan.ts +197 -0
  211. package/src/index.ts +98 -11
  212. package/src/input-core/text-entry.ts +13 -5
  213. package/src/intake-plane/seat-answer.ts +96 -0
  214. package/src/intake-plane/seat-doc-route.ts +17 -3
  215. package/src/maturity/feature-maturity.ts +2 -0
  216. package/src/nexus/api/consult-requests.ts +19 -8
  217. package/src/nexus/api/error-reports.ts +108 -0
  218. package/src/nexus/api/http-server.ts +41 -1
  219. package/src/nexus/api/meta-api.ts +27 -0
  220. package/src/nexus/api/operator.ts +61 -0
  221. package/src/nexus/api/ops-api.ts +139 -0
  222. package/src/nexus/api/ops-seats-sources.ts +65 -0
  223. package/src/nexus/api/ops-seats.ts +114 -0
  224. package/src/nexus/api/outbound-report.ts +7 -0
  225. package/src/nexus/api/public-routes.ts +14 -1
  226. package/src/nexus/api/running-runs-background.ts +68 -0
  227. package/src/nexus/api/running-runs-child.ts +6 -0
  228. package/src/nexus/api/setup-claim.ts +21 -0
  229. package/src/nexus/api/share-captures.ts +91 -0
  230. package/src/nexus/api/skill-problems.ts +56 -0
  231. package/src/nexus/api/terminals.ts +24 -8
  232. package/src/nexus/config/secrets/file-backend.ts +56 -18
  233. package/src/nexus/index.ts +25 -1
  234. package/src/nexus/setup-status.ts +7 -2
  235. package/src/oauth/browser-availability.ts +14 -0
  236. package/src/oauth/codex-quota-policy.ts +16 -0
  237. package/src/onboarding/continue-here.ts +13 -8
  238. package/src/onboarding/first-run-doctor.ts +54 -0
  239. package/src/onboarding/web-first.ts +8 -1
  240. package/src/onboarding.ts +66 -12
  241. package/src/persona/presets.ts +105 -0
  242. package/src/release-loop/checklist.ts +33 -69
  243. package/src/release-loop/feature-store.ts +327 -0
  244. package/src/release-loop/manifest.ts +59 -5
  245. package/src/seat-dispatch/ceo-commands.ts +65 -0
  246. package/src/self-dev/orchestrate.ts +18 -11
  247. package/src/shell-env-bootstrap.ts +44 -7
  248. package/src/skills/connect-claude-plugin.ts +142 -0
  249. package/src/skills/connect.ts +70 -0
  250. package/src/skills/index.ts +29 -1
  251. package/src/skills/repair.ts +124 -0
  252. package/src/skills/runner.ts +11 -2
  253. package/src/skills/tools/usage-slash.ts +8 -7
  254. package/src/skills/trigger-extract.ts +1 -1
  255. package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
  256. package/src/telegram-commands.ts +29 -0
  257. package/src/telegram-run.ts +83 -13
  258. package/src/telegram.ts +34 -0
  259. package/src/thinking-line.ts +28 -20
  260. package/src/tui/chat/execution-badge.ts +1 -1
  261. package/src/user-config.ts +111 -13
  262. package/src/ux-sim/turn-states.ts +1 -1
  263. package/src/version/packed-revision.json +1 -1
  264. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
  294. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
  295. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +0 -1
  296. /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
@@ -725,7 +725,8 @@ import { SELF_IMPLEMENT_TOOL_NAMES } from '../boot/daemon-tools/self-implement-n
725
725
  import { FOLD_LIMITS, type FoldMode } from '../log-entry.js';
726
726
  import { renderToolCallEvent, renderToolResultVariants } from '../chat/tool-render/index.js';
727
727
  import { FoldStack } from '../fold-stack.js';
728
- import { getSkillIndex, reloadSkillIndex, applySkillFilter } from '../skills/index.js';
728
+ import { getSkillIndex, reloadSkillIndex, applySkillFilter, skillIndexProblems } from '../skills/index.js';
729
+ import { skillRepairNotice } from '../skills/repair.js';
729
730
  import type { DetectResult } from '../skills/router.js';
730
731
  import { detectUrlRoute } from '../skills/url-router.js';
731
732
  import {
@@ -880,6 +881,10 @@ import { getModelFamily, getModelTier } from '../models/prompts.js';
880
881
  import { buildPromptInjection, getPromptBankStore } from '../prompt-bank/index.js';
881
882
  import { getInputHistoryStore, inputHistoryDbPath, inputHistoryJsonPath } from '../input-history.js';
882
883
  import { inspectActiveProvider } from '../provider-summary.js';
884
+ import { activeCodexAccountView } from '../oauth/codex-account-store.js';
885
+ import { buildDashboardStatusLines } from './dashboard-status-lines.js';
886
+ import { buildFirstScreenBand } from './first-screen-band.js';
887
+ import { runNexusShow, type NexusShowResult } from '../cli/nexus-show.js';
883
888
  import {
884
889
  workingDirSegment, sessionCwdSegment, modelSegment, gitSegment,
885
890
  ctxBarSegment, costSegment, speedSegment, elapsedSegment, cacheSegment,
@@ -2099,6 +2104,35 @@ export function createDashboardLogSearchOpener(deps: {
2099
2104
  };
2100
2105
  }
2101
2106
 
2107
+ export function renderDashboardFirstScreenBand(input: {
2108
+ readonly history: ReadonlyArray<{ role: string }>;
2109
+ readonly lines: string[];
2110
+ readonly width: number;
2111
+ readonly draw: () => void;
2112
+ readonly observe: (data: { daemon: boolean; hasPwa: boolean; hasTailnet: boolean }) => void;
2113
+ readonly show?: () => Promise<Pick<NexusShowResult, 'status' | 'urls'>>;
2114
+ }): Promise<void> {
2115
+ if (input.history.some((message) => message.role !== 'system')) return Promise.resolve();
2116
+ const band = buildFirstScreenBand({ width: input.width, daemon: false });
2117
+ input.lines.unshift(...band);
2118
+ const show = input.show ?? (() => runNexusShow({ format: 'json', out: { log: () => {}, error: () => {} } }));
2119
+ const observe = (data: { daemon: boolean; hasPwa: boolean; hasTailnet: boolean }): void => {
2120
+ try { input.observe(data); } catch { /* observation cannot interrupt the transcript */ }
2121
+ };
2122
+ return Promise.resolve().then(show).then((result) => {
2123
+ const daemon = result.status !== 'absent';
2124
+ const pwa = result.urls?.pwa;
2125
+ const hasPwa = !!pwa;
2126
+ const hasTailnet = !!pwa?.tailnet;
2127
+ observe({ daemon, hasPwa, hasTailnet });
2128
+ if (input.lines[1] !== band[1]) return;
2129
+ input.lines[1] = buildFirstScreenBand({ width: input.width, daemon, pwa })[1]!;
2130
+ try { input.draw(); } catch { /* screen may have closed */ }
2131
+ }).catch(() => {
2132
+ observe({ daemon: false, hasPwa: false, hasTailnet: false });
2133
+ });
2134
+ }
2135
+
2102
2136
  export async function showDashboard(opts: ShowDashboardOptions = {}): Promise<DashboardAction> {
2103
2137
  const dashboardQuitConfirmationState = createQuitConfirmationState();
2104
2138
  const signalQuitConfirmationState = createQuitConfirmationState();
@@ -6329,19 +6363,34 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
6329
6363
  draw,
6330
6364
  });
6331
6365
 
6332
- const getDashboardStatusLines = () => [
6333
- 'Dashboard status',
6334
- ...buildSessionDashboardStatusLines(resolveSessionSurfaceStatus({ chatModeState })).map(
6335
- line => ` ${line}`,
6336
- ),
6337
- ` view: ${workingDir.view}`,
6338
- ` focus: ${workingDir.focus}`,
6339
- ` cwd: ${workingDir.cwd}`,
6340
- ` preview: ${dockedPreview.sourceMode}`,
6341
- ` starterClosed: ${closedStarterPanes().length ? closedStarterPanes().map((pane) => paneLabel(pane)).join(', ') : 'none'}`,
6342
- ` chatOnly: ${effectiveChatOnlyMode() ? 'on' : 'off'}${chatOnlyMode ? ' (manual)' : ''}`,
6343
- ` acp: ${dashboardAcpChat.isBusy() ? 'busy' : 'idle'}`,
6344
- ];
6366
+ let daemonAttached = false;
6367
+ const getDashboardStatusLines = () => {
6368
+ const cfg = getUserConfig();
6369
+ const active = inspectActiveProvider(cfg);
6370
+ const reasoningProvider = (['openai-codex', 'openai', 'anthropic', 'gemini', 'grok', 'local', 'openrouter'] as const)
6371
+ .find(provider => active.provider === provider || active.provider === `auto:${provider}`);
6372
+ return buildDashboardStatusLines({
6373
+ provider: active.provider,
6374
+ model: active.model,
6375
+ reasoning: reasoningProvider
6376
+ ? modelSupportsReasoning(reasoningProvider, active.model)
6377
+ ? reasoningLevelLabel(effectiveReasoningLevel(cfg.llm, reasoningProvider, active.model))
6378
+ : '미지원'
6379
+ : undefined,
6380
+ daemonAddress: opts.remote?.url,
6381
+ daemonConnected: daemonAttached,
6382
+ accountName: active.provider === 'openai-codex' || active.provider === 'auto:openai-codex'
6383
+ ? activeCodexAccountView().name : undefined,
6384
+ sessionSurfaceLines: buildSessionDashboardStatusLines(resolveSessionSurfaceStatus({ chatModeState })),
6385
+ view: String(workingDir.view),
6386
+ focus: workingDir.focus,
6387
+ cwd: workingDir.cwd,
6388
+ preview: dockedPreview.sourceMode,
6389
+ starterClosed: closedStarterPanes().length ? closedStarterPanes().map((pane) => paneLabel(pane)).join(', ') : 'none',
6390
+ chatOnly: `${effectiveChatOnlyMode() ? 'on' : 'off'}${chatOnlyMode ? ' (manual)' : ''}`,
6391
+ acp: dashboardAcpChat.isBusy() ? 'busy' : 'idle',
6392
+ });
6393
+ };
6345
6394
 
6346
6395
  bootDashboardSlashExecutor({
6347
6396
  initDashboardSlashExecutor,
@@ -7190,6 +7239,21 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
7190
7239
  // and after each user-initiated skill cursor change.
7191
7240
  const skillView: SkillViewState = createSkillViewState(LOCAL_SKILLS_DIR);
7192
7241
  try { refreshSkills(skillView); refreshSkillFiles(skillView); } catch { /* missing root fine */ }
7242
+ // SK2 — a skill the index could not read is a one-line notice with the repair command, never a crash.
7243
+ try {
7244
+ getSkillIndex();
7245
+ const problems = skillIndexProblems();
7246
+ const notice = skillRepairNotice(problems);
7247
+ if (notice) chatLines.push(C.info(notice));
7248
+ // ER1 — tell operations too (consented · scrubbed · async); a kept unsent report is retried once a day.
7249
+ void import('../error-report/report.js').then(async ({ reportError, retryPendingErrorReport }) => {
7250
+ await retryPendingErrorReport();
7251
+ for (const p of problems.slice(0, 3)) {
7252
+ const outcome = await reportError({ code: 'skill-index', message: `${p.name}: ${p.code ?? ''} ${p.error}`.trim(), surface: 'tui' });
7253
+ if (outcome.sent && outcome.reportId) { chatLines.push(C.muted(` 운영팀에 알렸습니다 (보고 번호 ${outcome.reportId})`)); }
7254
+ }
7255
+ }).catch(() => { /* reporting never breaks the dashboard */ });
7256
+ } catch { /* the notice is advisory */ }
7193
7257
 
7194
7258
  // Scratch visibility (Phase S5). When closed, the workingDir
7195
7259
  // layouts drop the scratch cell so the remaining panes expand
@@ -15381,6 +15445,7 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
15381
15445
  ...(opts.remote ? {
15382
15446
  remote: opts.remote,
15383
15447
  onRemoteDisconnect: () => {
15448
+ daemonAttached = false;
15384
15449
  pushChatLine(C.warning(' ⚠ 데몬 연결이 끊겼다 · 입력 중인 글은 그대로다 · 다시 붙으려면 TUI 를 다시 띄워라'));
15385
15450
  draw();
15386
15451
  },
@@ -15594,6 +15659,7 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
15594
15659
  },
15595
15660
  });
15596
15661
  const dashboardAcpSession: DashboardSession = dashboardAcpBootResult.session;
15662
+ daemonAttached = !!opts.remote && dashboardAcpBootResult.mode !== 'in-process';
15597
15663
  // ⭐ `B3` — 턴 중 발화를 «도는 턴»에 넣을 때 쓰는 키.
15598
15664
  try { acpSessionIdRef.current = dashboardAcpSession.currentSessionId ?? null; } catch { /* 없으면 넣지 않는다 */ }
15599
15665
  pushDebugLine(C.muted('[acp-boot] dashboard ACP session ready'));
@@ -15686,6 +15752,13 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
15686
15752
  }
15687
15753
  }
15688
15754
  chatScrollOffset = -1;
15755
+ void renderDashboardFirstScreenBand({
15756
+ history: chat.history,
15757
+ lines: chatLines,
15758
+ width: Math.max(0, termSize().cols - 4),
15759
+ draw: () => draw(),
15760
+ observe: (data) => { try { debug.log('dashboard.first-screen-band', 'resolved', data); } catch { /* observation is best-effort */ } },
15761
+ });
15689
15762
 
15690
15763
  while (true) {
15691
15764
  draw();
@@ -8,7 +8,7 @@ export interface DashboardProviderRotationEntryView {
8
8
  export interface DashboardProviderAvailabilityView {
9
9
  available: boolean;
10
10
  name: string;
11
- model: string;
11
+ model: string | undefined;
12
12
  }
13
13
 
14
14
  export interface DashboardProviderSlashRuntimeDeps {
@@ -31,6 +31,7 @@ export interface DashboardProviderSlashRuntime {
31
31
  overviewLines(
32
32
  rotationEntries: readonly DashboardProviderRotationEntryView[],
33
33
  providers: readonly DashboardProviderAvailabilityView[],
34
+ catalogModelIds?: ReadonlySet<string>,
34
35
  ): string[];
35
36
  }
36
37
 
@@ -48,25 +49,41 @@ export function createDashboardProviderSlashRuntime(
48
49
  switchedLine: (label, provider, model) => deps.success(` ✓ switched → ${label} (${providerDetail(provider, model)})`),
49
50
  resetEmptyLine: () => deps.warning(' rotation empty — nothing to reset'),
50
51
  resetLine: (label) => deps.success(` ✓ reset → ${label}`),
51
- overviewLines: (rotationEntries, providers) => {
52
+ overviewLines: (rotationEntries, providers, catalogModelIds) => {
52
53
  const lines = ['', deps.accent('\u276f /provider')];
53
54
  if (rotationEntries.length > 0) {
54
- lines.push(deps.muted(' Rotation (cycle with `/provider next` or `/provider use <label>`):'));
55
+ lines.push(deps.muted(' 회전(바꾸기: /provider next · /provider use <이름>)'));
55
56
  for (const entry of rotationEntries) {
56
57
  const marker = entry.current ? deps.success('▸') : ' ';
57
- lines.push(` ${marker} ${entry.label.padEnd(14)} ${deps.subtext(entry.provider)} ${deps.muted(entry.model)}`);
58
+ const missing = catalogModelIds && entry.model !== '(provider default)' && !catalogModelIds.has(entry.model)
59
+ ? deps.muted(' · 목록에 없는 모델 — /provider use 로 바꾸기')
60
+ : '';
61
+ lines.push(` ${marker} ${entry.label.padEnd(14)} ${deps.subtext(entry.provider)} ${deps.muted(entry.model)}${missing}`);
58
62
  }
59
63
  lines.push('');
60
64
  }
61
- lines.push(deps.muted(' Available providers (env-detected):'));
65
+ lines.push(deps.muted(' 쓸 수 있는 공급자'));
66
+ // PROVIDERS has anthropic, not claude; collapse the legacy claude display alias.
67
+ const canonicalName = (name: string): string => {
68
+ const lower = name.toLowerCase();
69
+ return lower === 'claude' ? 'anthropic' : lower;
70
+ };
71
+ const unique = new Map<string, DashboardProviderAvailabilityView>();
62
72
  for (const provider of providers) {
63
- const mark = provider.available ? deps.success('\u2713') : deps.muted('\u00B7');
64
- const label = provider.available ? deps.text(provider.name) : deps.muted(provider.name);
65
- const hint = !provider.available
66
- ? deps.muted(` (set ${provider.name.toUpperCase()}_API_KEY${provider.name === 'local' ? ' or LOCAL_LLM_URL' : ''})`)
67
- : '';
68
- lines.push(` ${mark} ${label.padEnd(20)} ${deps.subtext(provider.model)}${hint}`);
73
+ const key = canonicalName(provider.name);
74
+ const previous = unique.get(key);
75
+ if (!previous || (!previous.available && provider.available) || (key === provider.name && previous.name !== key && previous.available === provider.available)) {
76
+ unique.set(key, provider);
77
+ }
78
+ }
79
+ let unavailable = 0;
80
+ for (const provider of unique.values()) {
81
+ if (!provider.available) { unavailable++; continue; }
82
+ lines.push(` ${deps.success('\u2713')} ${deps.text(provider.name).padEnd(20)} ${deps.subtext(provider.model ?? '')}`);
69
83
  }
84
+ if (unavailable > 0) lines.push(deps.muted(` 그 밖 ${unavailable}개는 키·로그인 필요 — elanous setup llm`));
85
+ // Keep the old English search terms as a subordinate hint, not section headings.
86
+ lines.push(deps.muted(' · Rotation (cycle with /provider next) · Available providers'));
70
87
  return lines;
71
88
  },
72
89
  };
@@ -301,6 +301,7 @@ import {
301
301
  import type { LLMProviderName } from '../../user-config.js';
302
302
  import { lookupLlmTierSpec } from '../../model-tier/index.js';
303
303
  import { listProviders } from '../../llm.js';
304
+ import { enabledModels, loadCatalog } from '../../intelligence-map/model-catalog.js';
304
305
  import { selfOrchestrateRuntime } from '../../self-dev/self-orchestrate-runtime.js';
305
306
  import type { ToolRuntime } from '../../tool-runtime/types.js';
306
307
  import { getSessionCwd } from '../../session/working-dir.js';
@@ -742,6 +743,7 @@ export interface DashboardSlashContext {
742
743
  overviewLines(
743
744
  rotation: readonly { label: string; provider: string; model: string; current: boolean }[],
744
745
  providers: readonly { available: boolean; name: string; model: string | undefined }[],
746
+ catalogModelIds?: ReadonlySet<string>,
745
747
  ): readonly string[];
746
748
  };
747
749
  /** 2026-05-05 — open the visual model picker programmatically.
@@ -1370,8 +1372,8 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
1370
1372
  ctx.attachmentRowMap.clear();
1371
1373
  ctx.clearLogSearch();
1372
1374
  ctx.clearLogFilter();
1373
- ctx.pushDebugLine(ctx.muted('Status cleared'));
1374
- ctx.chatLines.push(ctx.muted('Status cleared'));
1375
+ ctx.pushDebugLine(ctx.muted('Chat log cleared'));
1376
+ ctx.chatLines.push(ctx.muted('Chat log cleared'));
1375
1377
  ctx.setChatScrollOffset(-1);
1376
1378
  });
1377
1379
 
@@ -5281,6 +5283,13 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
5281
5283
  const cfgNow = getUserConfig();
5282
5284
  const rot = cfgNow.llm.rotation;
5283
5285
  const curIdx = rot && rot.length > 0 ? currentRotationIndex(cfgNow) : -1;
5286
+ let catalogModelIds: Set<string> | undefined;
5287
+ try {
5288
+ const loaded = loadCatalog();
5289
+ if (loaded.source !== 'fallback') {
5290
+ catalogModelIds = new Set(enabledModels(loaded.catalog).map((model) => model.id));
5291
+ }
5292
+ } catch { /* catalog unreadable: no stale-model verdict */ }
5284
5293
  ctx.chatLines.push(...ctx.provider.slashRuntime.overviewLines(
5285
5294
  (rot ?? []).map((entry, index) => ({
5286
5295
  label: rotationEntryLabel(entry),
@@ -5293,6 +5302,7 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
5293
5302
  name: provider.name,
5294
5303
  model: provider.model,
5295
5304
  })),
5305
+ catalogModelIds,
5296
5306
  ));
5297
5307
  const currentRoute = currentRouteDecision('dashboard');
5298
5308
  if (currentRoute) ctx.chatLines.push(ctx.muted(` current route: ${formatRouteDecisionSummary(currentRoute)}`));
@@ -3,7 +3,9 @@ import { visibleWidth } from '../../tui.js';
3
3
 
4
4
  const ESSENTIAL_KEYS = [
5
5
  ['Enter', 'Send message'],
6
- ['Esc', 'Clear input / close help modal'],
6
+ // U1 · 2026-10-02 — stopping and queueing while an answer streams were missing from help (same line count:
7
+ // the modal has a row budget and falls back to the chat log when it is exceeded).
8
+ ['Esc', 'Stop the answer (Esc again = rewind) · clear input / close help'],
7
9
  ['Ctrl+U', 'Clear text before cursor'],
8
10
  ['Ctrl+W', 'Delete previous word'],
9
11
  ['Ctrl+← / →', 'Move by word'],
@@ -11,7 +13,7 @@ const ESSENTIAL_KEYS = [
11
13
  ['Home / End', 'Move to start / end of line'],
12
14
  ['Ctrl+A / E', 'Move to start / end of line'],
13
15
  ['PgUp / PgDn', 'Scroll chat log'],
14
- ['Ctrl+↑', 'Recall sent line while streaming'],
16
+ ['Ctrl+↑', 'Recall a queued line (Enter while answering queues it)'],
15
17
  ] as const;
16
18
 
17
19
  function fitLine(text: string, width: number): string {
@@ -236,7 +236,7 @@ export function createTurnStreamFormatter(
236
236
  },
237
237
  onToolCall(call: TurnStreamCall): void {
238
238
  toolCallCount += 1;
239
- deps.thinking.update(`Streaming ${call.name} (${toolCallCount} tools)`);
239
+ deps.thinking.update(`Streaming ${call.name} (${toolCallCount} ${toolCallCount === 1 ? 'tool' : 'tools'})`);
240
240
  const ptyLine = deps.ptyCallLine(call.name, call.args);
241
241
  if (ptyLine) {
242
242
  nonGenericCallIds.add(call.id);
@@ -0,0 +1,291 @@
1
+ // DEC-TG(대표 10-02 07:1x) — «밖에 있어도 주요 결정을 텔레그램·디스코드로 다 끝낸다».
2
+ // `elanous decisions raise` 로 올라온 결정 → 대표 DM 카드(SCQA 요약 ⊕ 선택지 버튼 ⊕ 메모 달기) → 누르면 CLI `decide` 와 같은
3
+ // 원장 메서드로 기록 → 올린 자리에 회신 → 카드는 «결정됨: A · 시각»으로 바뀐다.
4
+ // ⛔ 보안: 버튼은 대표 id 만 받는다(그 밖은 무시 ⊕ 관측). 되돌릴 수 없는 범주(결제·외부 게시·보안·비가역)는 «되돌릴 수 없음» ⊕ 두 번 확인.
5
+ // 플랫폼(텔레그램·디스코드)은 `CardTransport` 하나만 구현한다 — 판단은 전부 여기 있다.
6
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { debug } from '../debug/log.js';
9
+ import { DecisionLedger, type DecisionEntry } from './decision-ledger.js';
10
+
11
+ export type CardPlatform = 'telegram' | 'discord';
12
+ export interface CardButton { label: string; data: string }
13
+ export interface CardView { text: string; buttons: CardButton[][] }
14
+ export interface CardRef { chat: string; message: string }
15
+
16
+ /** What a platform must provide. Every method is best-effort; failures are observed, never thrown to the poller. */
17
+ export interface CardTransport {
18
+ platform: CardPlatform;
19
+ /** Owner DM destinations (Telegram chat id = user id for a DM; Discord DM channel id). */
20
+ ownerChats(): Promise<string[]>;
21
+ send(chat: string, view: CardView): Promise<CardRef | null>;
22
+ edit(ref: CardRef, view: CardView): Promise<void>;
23
+ /** Plain text (reminders, list replies). */
24
+ notify(chat: string, text: string): Promise<void>;
25
+ }
26
+
27
+ const IRREVERSIBLE = new Set(['money', 'publish', 'security', 'irreversible', 'secret']);
28
+ export const DECISION_DATA_PREFIX = 'dec:';
29
+ const REMIND_BEFORE_MS = 2 * 3600_000;
30
+
31
+ export function isIrreversible(entry: Pick<DecisionEntry, 'category'>): boolean {
32
+ return IRREVERSIBLE.has(entry.category);
33
+ }
34
+
35
+ const kst = (at: string) => new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(at));
36
+
37
+ /** The card body: title, SCQA in four short lines, options with consequences, recommendation, deadline. */
38
+ export function renderCardText(e: DecisionEntry, extra: { note?: string; confirm?: string } = {}): string {
39
+ const lines = [
40
+ `🗳 결정 요청 ${e.id}${isIrreversible(e) ? ' · ⚠️ 되돌릴 수 없음' : ''}`,
41
+ `«${e.title}» · 올린 자리: ${e.raisedBy.agent}${e.raisedBy.track ? `(${e.raisedBy.track})` : ''}`,
42
+ '',
43
+ `상황: ${e.scqa.s}`,
44
+ `문제: ${e.scqa.c}`,
45
+ ...(e.scqa.q ? [`질문: ${e.scqa.q}`] : []),
46
+ ...(e.scqa.a ? [`제안: ${e.scqa.a}`] : []),
47
+ '',
48
+ ...e.options.map((o) => `${o.key.toUpperCase()}) ${o.label} — ${o.consequence}`),
49
+ 'skipped' in e.recommendation ? `권고 없음: ${e.recommendation.reason}` : `권고: ${e.recommendation.option.toUpperCase()} — ${e.recommendation.why}`,
50
+ ...(e.dueAt ? [`기한: ${kst(e.dueAt)}`] : []),
51
+ ...(extra.note ? ['', `📝 메모: ${extra.note}`] : []),
52
+ ];
53
+ if (extra.confirm) {
54
+ const option = e.options.find((o) => o.key === extra.confirm);
55
+ lines.push('', `⚠️ ${extra.confirm.toUpperCase()}) ${option?.label ?? ''} — 되돌릴 수 없습니다. 정말 이것으로 정할까요?`);
56
+ }
57
+ return lines.join('\n');
58
+ }
59
+
60
+ export function renderCard(e: DecisionEntry, extra: { note?: string; confirm?: string } = {}): CardView {
61
+ const text = renderCardText(e, extra);
62
+ if (e.status === 'decided') {
63
+ const option = e.options.find((o) => o.key === e.choice);
64
+ return { text: `${text}\n\n✅ 결정됨: ${e.choice?.toUpperCase() ?? '?'}) ${option?.label ?? ''} · ${e.decidedAt ? kst(e.decidedAt) : ''}${e.note ? `\n📝 ${e.note}` : ''}`, buttons: [] };
65
+ }
66
+ if (e.status === 'withdrawn') return { text: `${text}\n\n↩️ 철회됨${e.withdrawReason ? `: ${e.withdrawReason}` : ''}`, buttons: [] };
67
+ if (extra.confirm) {
68
+ return { text, buttons: [[
69
+ { label: `✅ 예, ${extra.confirm.toUpperCase()} 로 정한다`, data: `${DECISION_DATA_PREFIX}${e.id}:${extra.confirm}:ok` },
70
+ { label: '취소', data: `${DECISION_DATA_PREFIX}${e.id}:-` },
71
+ ]] };
72
+ }
73
+ const choices = e.options.map((o) => ({ label: Array.from(`${o.key.toUpperCase()}) ${o.label}`).slice(0, 60).join(''), data: `${DECISION_DATA_PREFIX}${e.id}:${o.key}` }));
74
+ const rows: CardButton[][] = [];
75
+ for (let i = 0; i < choices.length; i += 2) rows.push(choices.slice(i, i + 2));
76
+ rows.push([{ label: '📝 메모 달기', data: `${DECISION_DATA_PREFIX}${e.id}:memo` }]);
77
+ return { text, buttons: rows };
78
+ }
79
+
80
+ export type ParsedTap = { id: string; action: 'choose'; key: string; confirmed: boolean } | { id: string; action: 'memo' } | { id: string; action: 'cancel' };
81
+
82
+ export function parseTap(data: string): ParsedTap | null {
83
+ const m = /^dec:(D-\d{8}-\d+):([a-z]|memo|-)(:ok)?$/.exec(data);
84
+ if (!m) return null;
85
+ if (m[2] === 'memo') return { id: m[1]!, action: 'memo' };
86
+ if (m[2] === '-') return { id: m[1]!, action: 'cancel' };
87
+ return { id: m[1]!, action: 'choose', key: m[2]!, confirmed: Boolean(m[3]) };
88
+ }
89
+
90
+ interface CardState {
91
+ /** First start of this platform's service — decisions raised before it are not pushed (no burst of old items at
92
+ * deploy); they stay reachable through `/decisions`. */
93
+ since?: string;
94
+ cards: Record<string, { refs: CardRef[]; closed?: boolean; remindedAt?: string; note?: string }>;
95
+ }
96
+
97
+ export interface DecisionCardServiceOptions {
98
+ transport: CardTransport;
99
+ /** Owner user ids for this platform — taps from anyone else are refused and observed. */
100
+ ownerIds: readonly string[];
101
+ ledger?: DecisionLedger;
102
+ /** Where this platform keeps which card it sent (one file per platform — the two bots may run in different processes). */
103
+ statePath?: string;
104
+ now?: () => Date;
105
+ /** Tell the seat that raised it. Default: none (observed only). */
106
+ replyToRaiser?: (entry: DecisionEntry, via: CardPlatform) => Promise<void>;
107
+ }
108
+
109
+ export type TapOutcome =
110
+ | { kind: 'refused' } | { kind: 'ignored' } | { kind: 'unknown' }
111
+ | { kind: 'confirm'; view: CardView; toast: string }
112
+ | { kind: 'decided'; view: CardView; toast: string }
113
+ | { kind: 'cancelled'; view: CardView; toast: string }
114
+ | { kind: 'memo-requested'; toast: string }
115
+ | { kind: 'closed'; view: CardView; toast: string };
116
+
117
+ /** One per platform bot. `tick()` sends new cards, closes decided ones, sends reminders; `tap()` handles a button. */
118
+ export class DecisionCardService {
119
+ private readonly ledger: DecisionLedger;
120
+ private readonly statePath: string;
121
+ private readonly now: () => Date;
122
+ constructor(private readonly opts: DecisionCardServiceOptions) {
123
+ this.ledger = opts.ledger ?? new DecisionLedger();
124
+ this.statePath = opts.statePath ?? join(this.ledger.path, '..', `cards-${opts.transport.platform}.json`);
125
+ this.now = opts.now ?? (() => new Date());
126
+ }
127
+
128
+ private load(): CardState {
129
+ try { return JSON.parse(readFileSync(this.statePath, 'utf8')) as CardState; } catch { return { cards: {} }; }
130
+ }
131
+ private save(state: CardState): void {
132
+ mkdirSync(join(this.statePath, '..'), { recursive: true });
133
+ const tmp = `${this.statePath}.${process.pid}.tmp`;
134
+ writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
135
+ renameSync(tmp, this.statePath);
136
+ }
137
+
138
+ isOwner(userId: string): boolean { return this.opts.ownerIds.includes(userId); }
139
+
140
+ /** Send cards for new open decisions · close cards decided elsewhere · remind two hours before a deadline. */
141
+ async tick(): Promise<{ sent: number; closed: number; reminded: number }> {
142
+ const platform = this.opts.transport.platform;
143
+ const state = this.load();
144
+ let sent = 0; let closed = 0; let reminded = 0;
145
+ let all: DecisionEntry[];
146
+ try { all = this.ledger.list({ status: 'all' }); } catch (error) {
147
+ debug.log('decisions.telegram', 'ledger-unreadable', { platform, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
148
+ return { sent, closed, reminded };
149
+ }
150
+ let dirty = false;
151
+ if (!state.since) { state.since = this.now().toISOString(); dirty = true; debug.log('decisions.telegram', 'baseline', { platform, since: state.since }); }
152
+ const chats = await this.opts.transport.ownerChats().catch(() => [] as string[]);
153
+ for (const e of all) {
154
+ const card = state.cards[e.id];
155
+ if (e.status === 'open' && !card && e.options.length >= 2 && e.raisedAt !== undefined && e.raisedAt >= state.since) {
156
+ const refs: CardRef[] = [];
157
+ for (const chat of chats) {
158
+ const ref = await this.opts.transport.send(chat, renderCard(e)).catch(() => null);
159
+ if (ref) refs.push(ref);
160
+ }
161
+ if (refs.length) { state.cards[e.id] = { refs }; sent++; }
162
+ debug.log('decisions.telegram', 'card-sent', { platform, id: e.id, category: e.category, chats: chats.length, delivered: refs.length });
163
+ continue;
164
+ }
165
+ if (card && !card.closed && e.status !== 'open') {
166
+ for (const ref of card.refs) await this.opts.transport.edit(ref, renderCard(e)).catch(() => undefined);
167
+ card.closed = true; closed++;
168
+ debug.log('decisions.telegram', 'card-closed', { platform, id: e.id, status: e.status });
169
+ continue;
170
+ }
171
+ if (e.status === 'open' && card && e.dueAt && !card.remindedAt && this.now().getTime() >= Date.parse(e.dueAt) - REMIND_BEFORE_MS) {
172
+ for (const chat of chats) await this.opts.transport.notify(chat, `⏰ 기한 2시간 전 — ${e.id} «${e.title}» (기한 ${kst(e.dueAt)}) · 카드에서 정하거나 /decisions`).catch(() => undefined);
173
+ card.remindedAt = this.now().toISOString(); reminded++;
174
+ debug.log('decisions.telegram', 'reminded', { platform, id: e.id });
175
+ }
176
+ }
177
+ if (dirty || sent || closed || reminded) this.save(state);
178
+ return { sent, closed, reminded };
179
+ }
180
+
181
+ /** A button tap. Never throws. */
182
+ async tap(userId: string, data: string): Promise<TapOutcome> {
183
+ const platform = this.opts.transport.platform;
184
+ const parsed = parseTap(data);
185
+ if (!parsed) return { kind: 'ignored' };
186
+ if (!this.isOwner(userId)) {
187
+ debug.log('decisions.telegram', 'tap-refused', { platform, id: parsed.id }, { level: 'warn' });
188
+ return { kind: 'refused' };
189
+ }
190
+ let entry: DecisionEntry;
191
+ try { entry = this.ledger.show(parsed.id); } catch { return { kind: 'unknown' }; }
192
+ const state = this.load();
193
+ const card = state.cards[entry.id];
194
+ if (entry.status !== 'open') return { kind: 'closed', view: renderCard(entry), toast: '이미 정해진 결정입니다' };
195
+ if (parsed.action === 'memo') {
196
+ debug.log('decisions.telegram', 'memo-requested', { platform, id: entry.id });
197
+ return { kind: 'memo-requested', toast: '메모를 보내 주세요 — 다음 메시지가 메모가 됩니다' };
198
+ }
199
+ if (parsed.action === 'cancel') return { kind: 'cancelled', view: renderCard(entry, card?.note ? { note: card.note } : {}), toast: '취소했습니다' };
200
+ if (!entry.options.some((o) => o.key === parsed.key)) return { kind: 'unknown' };
201
+ if (isIrreversible(entry) && !parsed.confirmed) {
202
+ debug.log('decisions.telegram', 'confirm-asked', { platform, id: entry.id, key: parsed.key });
203
+ return { kind: 'confirm', view: renderCard(entry, { confirm: parsed.key, ...(card?.note ? { note: card.note } : {}) }), toast: '되돌릴 수 없는 결정 — 한 번 더 확인해 주세요' };
204
+ }
205
+ let decided: DecisionEntry;
206
+ try {
207
+ // Same ledger method the CLI `decide` uses — one record path.
208
+ decided = this.ledger.decide(entry.id, parsed.key, { kind: 'human' }, card?.note);
209
+ } catch (error) {
210
+ debug.log('decisions.telegram', 'decide-failed', { platform, id: entry.id, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
211
+ try { const now = this.ledger.show(entry.id); if (now.status !== 'open') return { kind: 'closed', view: renderCard(now), toast: '이미 정해진 결정입니다' }; } catch { /* fall through */ }
212
+ return { kind: 'unknown' };
213
+ }
214
+ const view = renderCard(decided);
215
+ if (card) {
216
+ for (const ref of card.refs) await this.opts.transport.edit(ref, view).catch(() => undefined);
217
+ card.closed = true;
218
+ this.save(state);
219
+ }
220
+ debug.log('decisions.telegram', 'decided', { platform, id: decided.id, choice: decided.choice, category: decided.category, noted: Boolean(decided.note) });
221
+ await this.opts.replyToRaiser?.(decided, platform).catch((error: unknown) => {
222
+ debug.log('decisions.telegram', 'reply-failed', { platform, id: decided.id, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
223
+ });
224
+ return { kind: 'decided', view, toast: `결정했습니다: ${decided.choice?.toUpperCase()}` };
225
+ }
226
+
227
+ /** Store a memo typed after «메모 달기»; the card shows it and the eventual decision carries it. */
228
+ async setNote(userId: string, id: string, note: string): Promise<CardView | null> {
229
+ if (!this.isOwner(userId)) return null;
230
+ let entry: DecisionEntry;
231
+ try { entry = this.ledger.show(id); } catch { return null; }
232
+ const clean = note.replace(/\s+/g, ' ').trim().slice(0, 300);
233
+ if (!clean || entry.status !== 'open') return null;
234
+ const state = this.load();
235
+ const card = state.cards[id] ?? { refs: [] };
236
+ card.note = clean;
237
+ state.cards[id] = card;
238
+ this.save(state);
239
+ const view = renderCard(entry, { note: clean });
240
+ for (const ref of card.refs) await this.opts.transport.edit(ref, view).catch(() => undefined);
241
+ debug.log('decisions.telegram', 'memo-set', { platform: this.opts.transport.platform, id, chars: clean.length });
242
+ return view;
243
+ }
244
+
245
+ /** `/decisions` — open decisions as one list; each card is sent again so the buttons are at hand. */
246
+ async listOpen(chat: string): Promise<string> {
247
+ let open: DecisionEntry[];
248
+ try { open = this.ledger.list({ status: 'open' }).filter((e) => e.options.length >= 2); } catch { return '결정 원장을 읽지 못했습니다.'; }
249
+ if (!open.length) return '열린 결정이 없습니다.';
250
+ const state = this.load();
251
+ for (const e of open) {
252
+ const ref = await this.opts.transport.send(chat, renderCard(e, state.cards[e.id]?.note ? { note: state.cards[e.id]!.note } : {})).catch(() => null);
253
+ if (ref) { const card = state.cards[e.id] ?? { refs: [] }; card.refs.push(ref); state.cards[e.id] = card; }
254
+ }
255
+ this.save(state);
256
+ debug.log('decisions.telegram', 'listed', { platform: this.opts.transport.platform, open: open.length });
257
+ return `열린 결정 ${open.length}건 — ${open.map((e) => `${e.id}${e.dueAt ? `(기한 ${kst(e.dueAt)})` : ''}`).join(' · ')}`;
258
+ }
259
+ }
260
+
261
+ /** Reply line for the seat that raised it. Posted to the coordination PR when `decisions.replyGhPr` = "owner/repo#N". */
262
+ export function raiserReplyText(e: DecisionEntry, via: CardPlatform): string {
263
+ const option = e.options.find((o) => o.key === e.choice);
264
+ const at = e.decidedAt ? new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul', dateStyle: 'short', timeStyle: 'short' }).format(new Date(e.decidedAt)) : '';
265
+ return `**[대표]** ${at} KST → ${e.raisedBy.agent} · 결정 ${e.id} «${e.title}» = ${e.choice?.toUpperCase()}) ${option?.label ?? ''}${e.note ? ` · 메모: ${e.note}` : ''} (${via === 'telegram' ? '텔레그램' : '디스코드'})`;
266
+ }
267
+
268
+ export function ghPrReplier(target: string, run: (args: string[], stdin: string) => Promise<number> = runGh): (e: DecisionEntry, via: CardPlatform) => Promise<void> {
269
+ const m = /^([\w.-]+\/[\w.-]+)#(\d+)$/.exec(target.trim());
270
+ return async (e, via) => {
271
+ if (!m) throw new Error('decisions.replyGhPr must be owner/repo#N');
272
+ const code = await run(['pr', 'comment', m[2]!, '--repo', m[1]!, '--body-file', '-'], raiserReplyText(e, via));
273
+ debug.log('decisions.telegram', 'replied', { id: e.id, via, target: 'gh-pr', ok: code === 0 });
274
+ if (code !== 0) throw new Error(`gh exit ${code}`);
275
+ };
276
+ }
277
+
278
+ /** Post as the automation App (not the machine's personal gh login — that would make 대표 a participant and mail them),
279
+ * with proxy variables removed (they break gh on the ops host). TC review #22660. */
280
+ export async function runGh(args: string[], stdin: string): Promise<number> {
281
+ const env: Record<string, string> = {};
282
+ for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(https?|all|no)_proxy$/i.test(k)) env[k] = v;
283
+ try {
284
+ const { githubAutomationToken } = await import('../auth/github-app-token.js');
285
+ const token = githubAutomationToken();
286
+ if (token) env.GH_TOKEN = token;
287
+ debug.log('decisions.telegram', 'reply-identity', { app: Boolean(token) });
288
+ } catch { /* falls back to the machine login */ }
289
+ const proc = Bun.spawn(['gh', ...args], { stdin: new Blob([stdin]), stdout: 'ignore', stderr: 'ignore', env });
290
+ return proc.exited;
291
+ }
@@ -15,11 +15,13 @@ export interface DecisionEntry {
15
15
  options: DecisionOption[]; recommendation: Recommendation; raisedAt?: string; importedAt?: string;
16
16
  raisedBy: { agent: string; track?: DecisionTrack; session?: string }; version?: Versions;
17
17
  status: 'open' | 'decided' | 'withdrawn'; refs?: string[];
18
+ /** DEC-TG — optional deadline (UTC). Cards remind the owner two hours before it. */
19
+ dueAt?: string;
18
20
  decidedAt?: string; decidedBy?: DecisionActor; choice?: string; note?: string; versionAtDecision?: Versions;
19
21
  withdrawnAt?: string; withdrawReason?: string;
20
22
  history: Array<{ type: 'raised' | 'options-added' | 'decided' | 'withdrawn'; at?: string; by: string; version?: Versions; choice?: string; reason?: string }>;
21
23
  }
22
- export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs'> & { raisedAt?: string };
24
+ export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt'> & { raisedAt?: string };
23
25
  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
26
  export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions }
25
27
  const CATEGORIES: readonly string[] = ['secret', 'publish', 'money', 'security', 'scope', 'irreversible', 'other'];
@@ -73,7 +75,8 @@ function validate(input: RaiseInput, historical = false): RaiseInput {
73
75
  if ('option' in recommendation && !options.some(o => o.key === recommendation.option)) throw new Error('recommended option not found');
74
76
  return { ...input, title, scqa: { s, c, ...(q ? { q } : {}), ...(a ? { a } : {}) }, options, recommendation,
75
77
  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'))) } : {}) };
78
+ ...(input.refs ? { refs: input.refs.map(r => safe(single(r, 'ref'))) } : {}),
79
+ ...(input.dueAt ? { dueAt: utc(input.dueAt) } : {}) };
77
80
  }
78
81
 
79
82
  export class DecisionLedger {