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
@@ -5,6 +5,7 @@
5
5
  // codex·elanous 자격 파일은 이 Job 에 없다. 격리 관문(호스트 넥서스에 닿으면 종료)은 기존 Job 과 같다.
6
6
  // 명령 인자는 셸 문자열로 이어붙이지 않고 `"$@"` 로 따로 인용한다.
7
7
 
8
+ import { randomBytes } from 'node:crypto';
8
9
  import { spawnSync } from 'node:child_process';
9
10
  import { isAbsolute } from 'node:path';
10
11
  import { getUserConfig } from '../../user-config.js';
@@ -66,6 +67,8 @@ if [ -d "$HOME/outbox" ]; then
66
67
  fi`;
67
68
 
68
69
  export interface PodCommandJobInput {
70
+ /** POD1 — this launch's own label; cleanup selects by it, never by name alone. */
71
+ launch?: string;
69
72
  name: string;
70
73
  namespace: string;
71
74
  image: string;
@@ -122,13 +125,13 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
122
125
  return {
123
126
  apiVersion: 'batch/v1',
124
127
  kind: 'Job',
125
- metadata: { name: o.name, namespace: o.namespace, labels: { 'elanous.substrate': 'pod', 'elanous.job': o.name, 'elanous.kind': 'command' } },
128
+ metadata: { name: o.name, namespace: o.namespace, labels: { 'elanous.substrate': 'pod', 'elanous.job': o.name, 'elanous.kind': 'command', ...(o.launch ? { 'elanous.launch': o.launch } : {}) } },
126
129
  spec: {
127
130
  backoffLimit: 0,
128
131
  ttlSecondsAfterFinished: 7200,
129
132
  activeDeadlineSeconds: o.deadlineSeconds,
130
133
  template: {
131
- metadata: { labels: { 'elanous.job': o.name, 'elanous.kind': 'command' } },
134
+ metadata: { labels: { 'elanous.job': o.name, 'elanous.kind': 'command', ...(o.launch ? { 'elanous.launch': o.launch } : {}) } },
132
135
  spec: {
133
136
  restartPolicy: 'Never',
134
137
  securityContext: { runAsUser: 1000, fsGroup: 1000 },
@@ -166,6 +169,7 @@ export function commandJobSecret(o: {
166
169
  skillEnvText: Readonly<Record<string, string>>;
167
170
  grokAuth?: string;
168
171
  ghToken?: string;
172
+ launch?: string;
169
173
  }): Record<string, unknown> | null {
170
174
  const stringData: Record<string, string> = {};
171
175
  if (o.grokAuth !== undefined) stringData['grok-auth.json'] = o.grokAuth;
@@ -177,7 +181,7 @@ export function commandJobSecret(o: {
177
181
  if (Object.keys(stringData).length === 0) return null;
178
182
  return {
179
183
  apiVersion: 'v1', kind: 'Secret', type: 'Opaque',
180
- metadata: { name: `${o.name}-creds`, namespace: o.namespace, labels: { 'elanous.job': o.name } },
184
+ metadata: { name: `${o.name}-creds`, namespace: o.namespace, labels: { 'elanous.job': o.name, ...(o.launch ? { 'elanous.launch': o.launch } : {}) } },
181
185
  stringData,
182
186
  };
183
187
  }
@@ -265,7 +269,11 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
265
269
  const image = options.image ?? 'elanous-harness:local';
266
270
  const repoUrl = options.repoUrl ?? 'https://github.com/ElanvitalAI/elanous';
267
271
  const deadlineSeconds = options.deadlineSeconds ?? POD_COMMAND_DEADLINE_SECONDS;
268
- const name = options.name ?? podJobName(`cmd-${Date.now().toString(36)}`);
272
+ // POD1 (10-01): two launches in the same moment got the same `si-cmd-<time>` and the failing one's cleanup
273
+ // deleted the other's credentials. The name now has a random tail, and cleanup selects this launch's label.
274
+ const launch = randomBytes(6).toString('hex');
275
+ const name = options.name ?? podJobName(`cmd-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`);
276
+ const own = `elanous.job=${name},elanous.launch=${launch}`;
269
277
  const env = options.env ?? process.env;
270
278
  const hostMirror = (options.hostMirror ?? env.ELANOUS_POD_HOST_MIRROR
271
279
  ?? (options.configHostMirror ?? (() => getUserConfig().pod?.hostMirror))())?.trim() || undefined;
@@ -293,7 +301,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
293
301
  const currentContext = member || options.kubectl ? null : baseKubectl(['config', 'current-context']);
294
302
  const context = member?.context ?? (currentContext?.status === 0 ? currentContext.stdout.trim() : '');
295
303
  const kubectl: Kubectl = (args, stdin) => baseKubectl(context ? ['--context', context, ...args] : [...args], stdin);
296
- const cleanupSecret = () => { kubectl(['-n', namespace, 'delete', 'secret', `${name}-creds`, '--ignore-not-found']); };
304
+ const cleanupSecret = () => { kubectl(['-n', namespace, 'delete', 'secret', '-l', own, '--ignore-not-found']); };
297
305
  const artifactsDir = `${options.artifactsRoot ?? `${effectiveInstanceRoot()}/pod-artifacts`}/${name}`;
298
306
  try {
299
307
  if (!context && !options.kubectl) throw new Error('pod command: kubectl context 를 확인할 수 없다');
@@ -328,19 +336,19 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
328
336
  if (synced?.imageRef) member = { ...member, imageRef: synced.imageRef };
329
337
  }
330
338
  const ghToken = options.clone ? (options.ghToken ?? defaultGhToken)() : undefined;
331
- const secret = commandJobSecret({ name, namespace, skills, skillEnvText, ...(grokAuth !== undefined ? { grokAuth } : {}), ...(ghToken !== undefined ? { ghToken } : {}) });
339
+ const secret = commandJobSecret({ name, namespace, launch, skills, skillEnvText, ...(grokAuth !== undefined ? { grokAuth } : {}), ...(ghToken !== undefined ? { ghToken } : {}) });
332
340
  if (secret) {
333
341
  const applied = kubectl(['apply', '-f', '-'], JSON.stringify(secret));
334
342
  if (applied.status !== 0) throw new Error(applied.stderr.trim() || 'pod command: Secret 적용 실패');
335
343
  }
336
344
  const manifest = podCommandJobManifest({
337
- name, namespace, image: member?.imageRef ?? image,
345
+ name, namespace, launch, image: member?.imageRef ?? image,
338
346
  ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}),
339
347
  repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
340
348
  ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(options.memoryLimit ? { memoryLimit: options.memoryLimit } : {}), deadlineSeconds,
341
349
  ...(options.runId ? { runId: options.runId } : {}),
342
350
  });
343
- kubectl(['-n', namespace, 'delete', 'job', name, '--ignore-not-found']);
351
+ kubectl(['-n', namespace, 'delete', 'job', '-l', own, '--ignore-not-found']);
344
352
  const applied = kubectl(['apply', '-f', '-'], JSON.stringify(manifest));
345
353
  if (applied.status !== 0) {
346
354
  cleanupSecret();
@@ -762,6 +762,35 @@ export function defaultTelegramCommands(): TgSlashCommand[] {
762
762
  });
763
763
  },
764
764
  },
765
+ {
766
+ name: 'decisions',
767
+ description: '열린 결정 목록 — 카드를 다시 보내 여기서 바로 정할 수 있습니다',
768
+ handler: async (_args, ctx) => {
769
+ const { telegramDecisionService } = await import('./decisions/telegram-decision-cards.js');
770
+ const service = telegramDecisionService();
771
+ if (!service) return '결정 카드가 이 봇에 연결되어 있지 않습니다.';
772
+ if (!service.isOwner(String(ctx.userId))) return '결정은 소유자만 볼 수 있습니다.';
773
+ // A group would show every member the cards (and route memos from them) — private chat only (TC review #22660).
774
+ if (ctx.chatId !== ctx.userId) return '결정은 개인 대화에서만 볼 수 있습니다.';
775
+ return service.listOpen(String(ctx.chatId));
776
+ },
777
+ },
778
+ ...(['coo', 'cto', 'cmo', 'cxo'] as const).map((command) => ({
779
+ name: command,
780
+ description: `${command.toUpperCase()} 자리에 일을 넘깁니다 — /${command} <할 일> (조율 채널 ⊕ 자리 메시지함)`,
781
+ handler: async (args: string[], ctx: { chatId: number; userId: number }) => {
782
+ const [{ handleCeoSeatCommand }, { telegramDecisionOwner }, { runGh }, { getUserConfig }, { openMsgStore }] = await Promise.all([
783
+ import('./seat-dispatch/ceo-commands.js'), import('./decisions/telegram-decision-cards.js'), import('./decisions/decision-cards.js'),
784
+ import('./user-config.js'), import('./msg/msg-store.js'),
785
+ ]);
786
+ const cfg = getUserConfig();
787
+ const replyTarget = (cfg.raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
788
+ return handleCeoSeatCommand(command, args, ctx, {
789
+ ownerId: telegramDecisionOwner(cfg), replyTarget: typeof replyTarget === 'string' ? replyTarget : null, runGh,
790
+ append: (message) => { const store = openMsgStore(); try { return store.append(message); } finally { store.close(); } },
791
+ });
792
+ },
793
+ })),
765
794
  {
766
795
  name: 'work',
767
796
  description: '글을 알맞은 곳(흡수·태스크·하니스)에 넣고 결과를 여기로 답합니다 — /work [absorb|tasks|graph] <글>',
@@ -11,6 +11,10 @@
11
11
  // - 워크플로 트리거는 core 의 `POST /v1/workflows/telegram-dispatch` 로 넘긴다(`telegram-dispatch-forward.ts`).
12
12
 
13
13
  import { debug } from './debug/log.js';
14
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
15
+ import { randomUUID } from 'node:crypto';
16
+ import { dirname, join } from 'node:path';
17
+ import { effectiveInstanceRoot } from './instance/resolve.js';
14
18
  import { getUserConfig } from './user-config.js';
15
19
  import { resolveChannelBotToken } from './channel-bot-token.js';
16
20
  import { forwardTelegramDispatch } from './telegram-dispatch-forward.js';
@@ -55,10 +59,10 @@ export function setTelegramPollLockForTesting(deps: typeof pollLockForTesting):
55
59
  /** 폴러를 띄우고 핸들을 돌려준다(시험용으로 분리 — 대기는 `runTelegramPoller` 가 한다). */
56
60
  export async function startTelegramPollers(): Promise<TelegramRunResult> {
57
61
  const cfg = getUserConfig();
58
- const mainToken = resolveChannelBotToken('telegram', cfg)?.token;
59
- if (!cfg.telegram.enabled || !mainToken) {
60
- throw new Error('telegram run: telegram.enabled 가 꺼져 있거나 bot token 이 없다');
61
- }
62
+ if (cfg.telegram.enabled === false) throw new Error('telegram run: telegram.enabled 가 꺼져 있다');
63
+ const resolved = resolveChannelBotToken('telegram', cfg);
64
+ if (!resolved.token) throw new Error(`telegram run: bot token 못 풂 — ${resolved.reason}`);
65
+ const mainToken = resolved.token;
62
66
  if (cfg.telegram.poller !== 'standalone') {
63
67
  console.warn('[telegram run] ⚠️ telegram.poller 가 standalone 이 아니다 — 넥서스도 같은 토큰을 폴링하려 한다. 토큰 잠금이 먼저 잡은 쪽만 띄운다.');
64
68
  }
@@ -105,25 +109,91 @@ export async function startTelegramPollers(): Promise<TelegramRunResult> {
105
109
  return { started, refusedBotIds, late };
106
110
  }
107
111
 
108
- /** SIGINT/SIGTERM 까지 돈다. */
109
- export async function runTelegramPoller(): Promise<void> {
112
+ function failurePath(): string { return join(effectiveInstanceRoot(), 'telegram', 'run-failures.json'); }
113
+
114
+ function failureState(): { count: number; alerted: boolean } {
115
+ const path = failurePath();
116
+ if (!existsSync(path)) return { count: 0, alerted: false };
117
+ const data: unknown = JSON.parse(readFileSync(path, 'utf8'));
118
+ if (!data || typeof data !== 'object' || !('count' in data) || !Number.isSafeInteger(data.count)
119
+ || (data.count as number) < 0 || ('alerted' in data && typeof data.alerted !== 'boolean')) {
120
+ throw new Error(`invalid telegram run failures at ${path}`);
121
+ }
122
+ return { count: data.count as number, alerted: 'alerted' in data ? data.alerted as boolean : false };
123
+ }
124
+
125
+ function writeFailureState(count: number, alerted: boolean): void {
126
+ const path = failurePath();
127
+ mkdirSync(dirname(path), { recursive: true });
128
+ const tmp = `${path}.${randomUUID()}.tmp`;
110
129
  try {
111
- const { registerStandaloneLogSink } = await import('./domains/standalone-log-sink.js');
112
- await registerStandaloneLogSink('telegram-run');
130
+ writeFileSync(tmp, JSON.stringify({ count, alerted }), { mode: 0o600, flag: 'wx' });
131
+ renameSync(tmp, path);
132
+ } finally { if (existsSync(tmp)) unlinkSync(tmp); }
133
+ }
134
+
135
+ /** SIGINT/SIGTERM 까지 돈다. Failure exit policy is tested without a live poller. */
136
+ export async function runTelegramPoller(deps: {
137
+ start?: typeof startTelegramPollers;
138
+ alert?: (text: string, kind: string) => boolean;
139
+ wait?: () => Promise<void>;
140
+ registerLogSink?: () => Promise<void>;
141
+ } = {}): Promise<void> {
142
+ try {
143
+ if (deps.registerLogSink) await deps.registerLogSink();
144
+ else {
145
+ const { registerStandaloneLogSink } = await import('./domains/standalone-log-sink.js');
146
+ await registerStandaloneLogSink('telegram-run');
147
+ }
113
148
  } catch (e) {
114
149
  console.error(`[telegram run] ⚠️ 로그 싱크 등록 실패(관측 유실 가능): ${e instanceof Error ? e.message : String(e)}`);
115
150
  }
116
151
  console.log('[telegram run] 토큰 잠금을 잡는 중 (토큰마다 최대 30초)…');
117
- const { started, refusedBotIds, late } = await startTelegramPollers();
152
+ let result: TelegramRunResult;
153
+ try {
154
+ if (!deps.start) {
155
+ const [{ registerAllCloudBackends }, { selectBackendFromConfig }] = await Promise.all([
156
+ import('./nexus/config/secrets/register-cloud.js'), import('./nexus/config/secrets/registry.js'),
157
+ ]);
158
+ registerAllCloudBackends();
159
+ selectBackendFromConfig();
160
+ }
161
+ result = await (deps.start ?? startTelegramPollers)();
162
+ if (result.started.length === 0) throw new Error('telegram run: 띄운 폴러가 없다');
163
+ } catch (error) {
164
+ const reason = error instanceof Error ? error.message : 'unknown start failure';
165
+ const previous = failureState();
166
+ const count = previous.count + 1;
167
+ writeFailureState(count, previous.alerted);
168
+ debug.log('telegram.run', 'start-failed', { count, reason });
169
+ if (count >= 3) {
170
+ if (!previous.alerted) {
171
+ try {
172
+ const alert = deps.alert ?? (await import('./domains/outbound-alert.js')).sendOutbound;
173
+ if (!alert(`텔레그램 폴러가 3번 연속 못 떴다 — ${reason}`, 'alert')) {
174
+ throw new Error('telegram run: outbound alert not delivered');
175
+ }
176
+ } catch (alertError) {
177
+ debug.log('telegram.run', 'alert-failed', { count, reason });
178
+ throw alertError;
179
+ }
180
+ writeFailureState(count, true);
181
+ debug.log('telegram.run', 'alerted', { count, reason });
182
+ }
183
+ return;
184
+ }
185
+ throw error;
186
+ }
187
+ const { started, refusedBotIds, late } = result;
188
+ const path = failurePath();
189
+ if (existsSync(path)) unlinkSync(path);
118
190
  debug.log('telegram.run', 'started', { pid: process.pid, pollers: started.length, refusedBotIds });
119
191
  for (const botId of refusedBotIds) {
120
192
  console.warn(`[telegram run] 봇 ${botId}: 다른 프로세스가 폴링 중 — 15초마다 다시 잡으러 간다 (elanous logs --category telegram.run)`);
121
193
  }
122
- // 하나도 못 잡았으면 끝낸다(exit≠0) — 서비스 관리자(launchd KeepAlive·systemd Restart)가 다시 띄운다.
123
- if (started.length === 0) throw new Error('telegram run: 띄운 폴러가 없다');
124
194
  for (const { channel } of started) console.log(`[telegram run] 채널 '${channel.name}' 폴러 활성 (${channel.botToken.split(':')[0]})`);
125
195
  console.log('[telegram run] Ctrl-C 로 멈춘다.');
126
- await new Promise<void>((resolve) => {
196
+ await (deps.wait?.() ?? new Promise<void>((resolve) => {
127
197
  const stop = (): void => {
128
198
  process.off('SIGINT', stop);
129
199
  process.off('SIGTERM', stop);
@@ -135,5 +205,5 @@ export async function runTelegramPoller(): Promise<void> {
135
205
  };
136
206
  process.on('SIGINT', stop);
137
207
  process.on('SIGTERM', stop);
138
- });
208
+ }));
139
209
  }
package/src/telegram.ts CHANGED
@@ -919,6 +919,24 @@ export class TelegramBot {
919
919
  return undefined;
920
920
  }
921
921
 
922
+ /** DEC-TG — replace a message's text AND its inline keyboard (empty rows remove the buttons). */
923
+ async editMessageWithKeyboard(
924
+ chatId: number,
925
+ messageId: number,
926
+ text: string,
927
+ buttons: Array<Array<{ text: string; data: string }>>,
928
+ ): Promise<void> {
929
+ await this.throttledCall<unknown>(chatId, 'editMessageText', {
930
+ chat_id: chatId,
931
+ message_id: messageId,
932
+ text: text.length > this.maxChars ? `${text.slice(0, this.maxChars - 2)} …` : text,
933
+ reply_markup: { inline_keyboard: buttons.map((row) => row.map((b) => ({ text: b.text, callback_data: b.data.slice(0, 64) }))) },
934
+ });
935
+ }
936
+
937
+ /** True while the long-poll loop runs — only a polling bot should push decision cards (nexus may hold a send-only bot). */
938
+ isPolling(): boolean { return this.running; }
939
+
922
940
  /** T2-P6 — acknowledge a callback_query so Telegram clears the
923
941
  * spinner next to the button the user tapped. Pass a `text` to
924
942
  * flash a tiny toast on the user's device (≤200 chars). Returns
@@ -1156,6 +1174,8 @@ export class TelegramBot {
1156
1174
  if (u.callback_query) {
1157
1175
  const cq = u.callback_query;
1158
1176
  if (!this.isOwnerAllowed(cq.from.id)) {
1177
+ // DEC-TG — a decision-card tap from someone else is the rehearsal's «refused» case; name it there too.
1178
+ if ((cq.data ?? '').startsWith('dec:')) debug.log('decisions.telegram', 'tap-refused', { platform: 'telegram', gate: 'owner-allowlist' }, { level: 'warn' });
1159
1179
  debug.log('telegram.owner-gate', 'refused', {
1160
1180
  kind: 'callback', reason: this.allowedUsers.size === 0 ? 'empty-allowlist' : 'not-allowed', userId: cq.from.id,
1161
1181
  });
@@ -1425,6 +1445,9 @@ export class TelegramBot {
1425
1445
  ...reply, mimeType: 'video/mp4', caption: `현장 영상 · ${event} · ${result.seconds}초`,
1426
1446
  });
1427
1447
  if (!sent) throw new Error('sendDocument failed');
1448
+ if (result.feed?.started) {
1449
+ await this.sendMessage(ctx.chatId, `인스타 피드 초안을 만드는 중 · ${event} — 1분쯤 뒤 앱 «게시 대기»에서 고치고 «최종 게시»를 누르면 됩니다(누르기 전엔 아무 데도 안 올라갑니다).`, reply);
1450
+ }
1428
1451
  } else {
1429
1452
  await this.sendMessage(ctx.chatId, `현장 영상 실패 · ${event} · ${result.error ?? 'render failed'}`, reply);
1430
1453
  }
@@ -2840,6 +2863,17 @@ export function botFromConfig(opts: BotFromConfigOpts): TelegramBot {
2840
2863
  // poll loop requests `callback_query` from the first getUpdates —
2841
2864
  // in-turn approval taps then arrive without a mid-turn delay.
2842
2865
  botRef.ensureSurfaceHitl();
2866
+ // DEC-TG(대표 10-02) — decision cards on the MAIN bot only (other channel pollers carry other tokens). The ticker
2867
+ // itself sends only while this bot polls, so a send-only nexus bot never duplicates the standalone poller's cards.
2868
+ if (opts.userConfig.raw?.decisions === undefined || (opts.userConfig.raw.decisions as { cards?: unknown }).cards !== false) {
2869
+ const mainToken = resolveChannelBotToken('telegram', opts.userConfig)?.token;
2870
+ if (mainToken && botToken === mainToken) {
2871
+ const bot = botRef;
2872
+ void import('./decisions/telegram-decision-cards.js')
2873
+ .then(({ attachTelegramDecisionCards }) => { attachTelegramDecisionCards(bot, opts.userConfig); })
2874
+ .catch((err: unknown) => log(`[telegram] decision cards not attached: ${err instanceof Error ? err.message : String(err)}`));
2875
+ }
2876
+ }
2843
2877
  return botRef;
2844
2878
  }
2845
2879
 
@@ -156,6 +156,14 @@ function renderLine(
156
156
  return `${applyColor(glyph, 'claude')} ${C.muted(message + '…')}${tail}`;
157
157
  }
158
158
 
159
+ function stoppedMessage(message: string, status: ThinkingStatus): string {
160
+ const taskName = message.replace(/^(?:Streaming|Thinking)(?=\b|…)(?:\s*…)?\s*/, '').trim();
161
+ const task = taskName ? `${taskName} ` : '';
162
+ if (status === 'completed') return `${task}완료`;
163
+ if (status === 'interrupted') return `${task}중단됨`;
164
+ return `${task}실패`;
165
+ }
166
+
159
167
  export function startThinking(opts: StartThinkingOpts): ThinkingHandle {
160
168
  let message = opts.message ?? 'Thinking';
161
169
  let messageFactory: ((frame: number) => string) | null = null;
@@ -222,20 +230,19 @@ export function startThinking(opts: StartThinkingOpts): ThinkingHandle {
222
230
  if (metrics.engine) detail.push(metrics.engine);
223
231
  const tail = detail.length ? ' ' + C.muted('(' + detail.join(' · ') + ')') : '';
224
232
  let finalText: string;
225
- if (stopOpts?.finalText) {
233
+ if (stopOpts?.finalText !== undefined) {
226
234
  finalText = stopOpts.finalText;
227
- } else if (status === 'completed') {
228
- // Green tick + verb kept visible (not muted) so it stands out.
229
- const currentMessage = messageFactory ? messageFactory(frame) : message;
230
- finalText = `${applyColor(FIGURES.TICK, 'success')} ${C.success(currentMessage)}${tail}`;
231
- } else if (status === 'interrupted') {
232
- const currentMessage = messageFactory ? messageFactory(frame) : message;
233
- finalText = `${applyColor(FIGURES.CROSS, 'error')} ${C.warning(currentMessage + ' · interrupted')}${tail}`;
234
235
  } else {
235
- // failed
236
- const err = stopOpts?.errorText ? ` — ${stopOpts.errorText}` : '';
237
236
  const currentMessage = messageFactory ? messageFactory(frame) : message;
238
- finalText = `${applyColor(FIGURES.CROSS, 'error')} ${C.error(currentMessage + ' · failed' + err)}${tail}`;
237
+ const label = stoppedMessage(currentMessage, status);
238
+ if (status === 'completed') {
239
+ finalText = `${applyColor(FIGURES.TICK, 'success')} ${C.success(label)}${tail}`;
240
+ } else if (status === 'interrupted') {
241
+ finalText = `${applyColor(FIGURES.CROSS, 'error')} ${C.warning(label)}${tail}`;
242
+ } else {
243
+ const err = stopOpts?.errorText ? ` — ${stopOpts.errorText}` : '';
244
+ finalText = `${applyColor(FIGURES.CROSS, 'error')} ${C.error(label + err)}${tail}`;
245
+ }
239
246
  }
240
247
 
241
248
  // Freeze at tail if sticky='tail' — the indicator was tracking
@@ -310,18 +317,19 @@ export function startPinnedThinking(opts: StartPinnedThinkingOpts): ThinkingHand
310
317
  if (metrics.engine) detail.push(metrics.engine);
311
318
  const tail = detail.length ? ' ' + C.muted('(' + detail.join(' · ') + ')') : '';
312
319
  let finalText: string;
313
- if (stopOpts?.finalText) {
320
+ if (stopOpts?.finalText !== undefined) {
314
321
  finalText = stopOpts.finalText;
315
- } else if (status === 'completed') {
316
- const currentMessage = messageFactory ? messageFactory(frame) : message;
317
- finalText = `${applyColor(FIGURES.TICK, 'success')} ${C.success(currentMessage)}${tail}`;
318
- } else if (status === 'interrupted') {
319
- const currentMessage = messageFactory ? messageFactory(frame) : message;
320
- finalText = `${applyColor(FIGURES.CROSS, 'error')} ${C.warning(currentMessage + ' · interrupted')}${tail}`;
321
322
  } else {
322
- const err = stopOpts?.errorText ? ` — ${stopOpts.errorText}` : '';
323
323
  const currentMessage = messageFactory ? messageFactory(frame) : message;
324
- finalText = `${applyColor(FIGURES.CROSS, 'error')} ${C.error(currentMessage + ' · failed' + err)}${tail}`;
324
+ const label = stoppedMessage(currentMessage, status);
325
+ if (status === 'completed') {
326
+ finalText = `${applyColor(FIGURES.TICK, 'success')} ${C.success(label)}${tail}`;
327
+ } else if (status === 'interrupted') {
328
+ finalText = `${applyColor(FIGURES.CROSS, 'error')} ${C.warning(label)}${tail}`;
329
+ } else {
330
+ const err = stopOpts?.errorText ? ` — ${stopOpts.errorText}` : '';
331
+ finalText = `${applyColor(FIGURES.CROSS, 'error')} ${C.error(label + err)}${tail}`;
332
+ }
325
333
  }
326
334
  opts.target.current = finalText;
327
335
  opts.onFrame();
@@ -1,7 +1,7 @@
1
1
  // ── TUI TRUE-execution badge (self-cognition §1) ──────────────────────
2
2
  //
3
3
  // Resolves the compact engine/model badge for the dashboard chat
4
- // completion line — `✔ Streaming (19s · ↓ 200 tokens · 🧠 terra(high))`.
4
+ // completion line — `✔ 완료 (19s · ↓ 200 tokens · 🧠 terra(high))`.
5
5
  // The value is the model that ACTUALLY ran this turn (from the route
6
6
  // decision recorded in the ACP bridge's getActiveModel), not the config
7
7
  // original — so the "config=opus but codex/terra ran" illusion is
@@ -437,11 +437,28 @@ const LLM_DEFAULTS: LLMConfig = { provider: 'auto' };
437
437
  export type SkillSetName =
438
438
  | 'claudecode' | 'opencode' | 'codex' | 'hermes' | 'openclaw' | 'custom';
439
439
 
440
+ export type SkillSourceKind = 'preset' | 'shared' | 'connected' | 'package';
441
+
442
+ /** One skill root elanous reads. `mode` is always `ref` today (the folder is read where it is). */
443
+ export interface SkillSource {
444
+ id: string;
445
+ path: string;
446
+ kind: SkillSourceKind;
447
+ enabled: boolean;
448
+ }
449
+
440
450
  export interface SkillsConfig {
441
451
  activeSet: SkillSetName;
442
452
  dirs: string[];
443
453
  /** Include the existing shared ~/.agents/skills directory. Default true; false opts out. */
444
454
  includeSharedAgentSkills?: boolean;
455
+ /**
456
+ * EN5 — the one source list. On disk it holds only what `elanous connect` registered (kind `connected`, read in
457
+ * place, never copied); the preset (`activeSet`/`dirs`), the shared `~/.agents/skills` root and Claude package
458
+ * roots are derived at read time by `resolveSkillSources()`. The dev-only `connected: string[]` key (EN9, never
459
+ * released) is read and folded in here; the next save writes `sources` instead.
460
+ */
461
+ sources?: SkillSource[];
445
462
  /** Explicit skill-name allowlist. When set and non-empty, the skill
446
463
  * index filters to this list — all other skills are hidden from
447
464
  * routing, autocompletion, and `/run-skill`. Use when a project
@@ -517,34 +534,84 @@ export function bundledSkillsDir(packageRoot: string = dirname(import.meta.dir))
517
534
  return join(packageRoot, 'skills');
518
535
  }
519
536
 
520
- export function defaultSkillDirs(
521
- cfg?: UserConfig,
522
- opts?: DefaultSkillDirsOptions,
523
- ): string[] {
537
+ function packageOptIn(cfg?: UserConfig): boolean {
538
+ try { return (cfg ?? getUserConfig()).skills.includeClaudePackageSkills === true; } catch { return false; }
539
+ }
540
+
541
+ /** Stable id for a connected root: the agent it belongs to, else the folder (plugin) name. */
542
+ export function skillSourceId(path: string): string {
543
+ if (/[/\\]\.codex[/\\]skills\/?$/.test(path)) return 'codex';
544
+ if (/[/\\]\.claude[/\\]skills\/?$/.test(path)) return 'claude';
545
+ const parts = path.split(/[/\\]/).filter(Boolean);
546
+ const last = parts[parts.length - 1] === 'skills' ? parts[parts.length - 2] : parts[parts.length - 1];
547
+ return `plugin:${last ?? 'skills'}`;
548
+ }
549
+
550
+ /** Parse persisted `skills.sources` ⊕ the dev-only `skills.connected` key into connected sources (deduped by path). */
551
+ export function parsePersistedSkillSources(rawSources: unknown, legacyConnected: unknown): SkillSource[] | undefined {
552
+ const out: SkillSource[] = [];
553
+ const seen = new Set<string>();
554
+ const push = (source: SkillSource) => { if (!seen.has(source.path)) { seen.add(source.path); out.push(source); } };
555
+ if (Array.isArray(rawSources)) {
556
+ for (const row of rawSources) {
557
+ if (!row || typeof row !== 'object') continue;
558
+ const r = row as Record<string, unknown>;
559
+ if (typeof r.path !== 'string' || !r.path.trim()) continue;
560
+ const path = r.path.trim();
561
+ push({ id: typeof r.id === 'string' && r.id.trim() ? r.id.trim() : skillSourceId(path), path, kind: 'connected', enabled: r.enabled !== false });
562
+ }
563
+ }
564
+ if (Array.isArray(legacyConnected)) {
565
+ for (const path of legacyConnected) if (typeof path === 'string' && path.trim()) push({ id: skillSourceId(path.trim()), path: path.trim(), kind: 'connected', enabled: true });
566
+ }
567
+ return out.length ? out : undefined;
568
+ }
569
+
570
+ /**
571
+ * ★ EN5 — the ONE function every reader uses (loader · setup checklist · connect · onboarding).
572
+ * Order is the historical order, so an old config resolves to exactly the old directory list:
573
+ * preset (activeSet/dirs) → shared ~/.agents/skills (only when it exists) → connected → Claude package roots (opt-in).
574
+ * Packaged `skills/` is appended by defaultSkillDirs, not listed here.
575
+ */
576
+ export function resolveSkillSources(cfg?: UserConfig, opts?: DefaultSkillDirsOptions): SkillSource[] {
524
577
  const claudecode = skillSetDir('claudecode')!; // = ~/.claude/skills (LOCAL_SKILLS_DIR)
525
- let base = [claudecode];
578
+ const preset = (paths: string[], id: string): SkillSource[] => paths.map((path, i) => ({ id: paths.length > 1 ? `${id}:${i + 1}` : id, path, kind: 'preset', enabled: true }));
579
+ let sources: SkillSource[] = preset([claudecode], 'claudecode');
526
580
  try {
527
581
  const sk = (cfg ?? getUserConfig()).skills;
528
582
  if (sk.activeSet !== 'custom') {
529
583
  const d = skillSetDir(sk.activeSet);
530
- if (d) base = [d];
531
- else if (sk.dirs.length > 0) base = [...sk.dirs];
584
+ if (d) sources = preset([d], sk.activeSet);
585
+ else if (sk.dirs.length > 0) sources = preset([...sk.dirs], 'custom');
532
586
  } else if (sk.dirs.length > 0) {
533
- base = [...sk.dirs];
587
+ sources = preset([...sk.dirs], 'custom');
534
588
  }
589
+ const has = (path: string) => sources.some((source) => source.path === path);
535
590
  const shared = sharedAgentSkillRoots({
536
591
  home: opts?.sharedAgentHome ?? REMOTE_HOME,
537
592
  includeSharedAgentSkills: sk.includeSharedAgentSkills !== false,
538
593
  });
539
594
  try { debug.log('skills.roots', 'shared-agent', { found: shared.length }); }
540
595
  catch { /* discovery remains fail-soft when observation is unavailable */ }
541
- for (const dir of shared) {
542
- if (!base.includes(dir)) base.push(dir);
596
+ for (const path of shared) if (!has(path)) sources.push({ id: 'agents', path, kind: 'shared', enabled: true });
597
+ for (const source of sk.sources ?? []) if (!has(source.path)) sources.push({ ...source, kind: 'connected' });
598
+ if (sk.includeClaudePackageSkills === true) {
599
+ for (const path of appendClaudePackageSkillDirs([], opts?.pluginsRoot)) {
600
+ if (!has(path)) sources.push({ id: `package:${skillSourceId(path).replace(/^plugin:/, '')}`, path, kind: 'package', enabled: true });
601
+ }
543
602
  }
544
- base = sk.includeClaudePackageSkills === true
545
- ? appendClaudePackageSkillDirs(base, opts?.pluginsRoot)
546
- : base;
547
603
  } catch { /* fail-soft — config 없거나 파싱 실패 시 기본 경로 */ }
604
+ return sources;
605
+ }
606
+
607
+ export function defaultSkillDirs(
608
+ cfg?: UserConfig,
609
+ opts?: DefaultSkillDirsOptions,
610
+ ): string[] {
611
+ const sources = resolveSkillSources(cfg, opts);
612
+ const paths = sources.filter((source) => source.enabled).map((source) => source.path);
613
+ // Historical: the package opt-in path also collapsed duplicate user dirs; the plain path kept them (index dedupes later).
614
+ const base = sources.some((source) => source.kind === 'package') || packageOptIn(cfg) ? uniqueSkillDirsPreserveOrder(paths) : paths;
548
615
  return appendBundledSkillsDir(base, opts?.bundledSkillsRoot);
549
616
  }
550
617
 
@@ -1384,6 +1451,28 @@ export interface ChatCompactConfig {
1384
1451
  archiveRetentionMb: number;
1385
1452
  }
1386
1453
 
1454
+ /** ER1 — 액티브 덤퍼(오류 자동 보고). 기본 켬(첫 실행 안내 한 번 · 대표 10-01) · 신원(email/telegramUser)은 따로 «예»일 때만. */
1455
+ export interface ErrorReportsConfig {
1456
+ /** false = never send. Absent = on (the first-run notice says how to turn it off). */
1457
+ enabled?: boolean;
1458
+ /** true = also send email / telegramUser below with each report. Default false. */
1459
+ identity?: boolean;
1460
+ email?: string;
1461
+ telegramUser?: string;
1462
+ }
1463
+
1464
+ export function parseErrorReportsConfig(raw: unknown): ErrorReportsConfig | undefined {
1465
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
1466
+ const r = raw as Record<string, unknown>;
1467
+ const out: ErrorReportsConfig = {};
1468
+ if (r.enabled === false || r.enabled === 'false') out.enabled = false;
1469
+ else if (r.enabled === true || r.enabled === 'true') out.enabled = true;
1470
+ if (r.identity === true || r.identity === 'true') out.identity = true;
1471
+ if (typeof r.email === 'string' && r.email.trim()) out.email = r.email.trim();
1472
+ if (typeof r.telegramUser === 'string' && r.telegramUser.trim()) out.telegramUser = r.telegramUser.trim();
1473
+ return Object.keys(out).length ? out : undefined;
1474
+ }
1475
+
1387
1476
  export interface ChatConfig {
1388
1477
  /** Short questions may receive a tool-free first reply. Default true. */
1389
1478
  fastPath: boolean;
@@ -3373,6 +3462,8 @@ export interface UserConfig {
3373
3462
  logs: LogsConfig;
3374
3463
  shell: ShellConfig;
3375
3464
  chat: ChatConfig;
3465
+ /** ER1 — optional so older fixtures stay valid; absent = defaults. */
3466
+ errorReports?: ErrorReportsConfig;
3376
3467
  voice: VoiceConfig;
3377
3468
  intake: IntakeConfig;
3378
3469
  dashboard: DashboardConfig;
@@ -4419,6 +4510,7 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
4419
4510
  activeSet: normalizeSkillSet(sk.activeSet),
4420
4511
  dirs: strArray(sk.dirs, skillsDefaults().dirs),
4421
4512
  includeSharedAgentSkills: sk.includeSharedAgentSkills !== false,
4513
+ ...(() => { const sources = parsePersistedSkillSources(sk.sources, sk.connected); return sources ? { sources } : {}; })(),
4422
4514
  allow: strArray(sk.allow, []),
4423
4515
  deny: strArray(sk.deny, []),
4424
4516
  urlRouting: parseUrlRouting(sk.urlRouting),
@@ -4546,6 +4638,7 @@ export function buildUserConfig(path: string = defaultPath()): UserConfig {
4546
4638
  // false required to short-circuit `buildDashboardOptionalToolSpecs`.
4547
4639
  allowDashboardOptionalTools: sh.allowDashboardOptionalTools !== false,
4548
4640
  },
4641
+ ...((): { errorReports?: ErrorReportsConfig } => { const er = parseErrorReportsConfig(rawObj.errorReports); return er ? { errorReports: er } : {}; })(),
4549
4642
  chat: {
4550
4643
  fastPath: chat.fastPath === false || chat.fastPath === 'false' ? false : CHAT_DEFAULTS.fastPath,
4551
4644
  conciseness: {
@@ -5590,6 +5683,10 @@ export function saveUserConfig(
5590
5683
  skills: stripUndef({
5591
5684
  activeSet: cfg.skills.activeSet,
5592
5685
  dirs: [...cfg.skills.dirs],
5686
+ // EN5 — only connected roots are persisted (preset/shared/package are derived). The dev-only `connected` key is not written back.
5687
+ sources: cfg.skills.sources && cfg.skills.sources.some((source) => source.kind === 'connected')
5688
+ ? cfg.skills.sources.filter((source) => source.kind === 'connected').map(({ id, path, enabled }) => ({ id, path, enabled }))
5689
+ : undefined,
5593
5690
  // Omit allow/deny when empty to keep disk JSON minimal for the
5594
5691
  // common "no filter" case.
5595
5692
  allow: cfg.skills.allow && cfg.skills.allow.length > 0 ? [...cfg.skills.allow] : undefined,
@@ -5683,6 +5780,7 @@ export function saveUserConfig(
5683
5780
  debug: cfg.debug,
5684
5781
  logs: cfg.logs,
5685
5782
  shell: cfg.shell,
5783
+ errorReports: cfg.errorReports ? stripUndef({ ...cfg.errorReports }) : undefined,
5686
5784
  chat: stripUndef({
5687
5785
  fastPath: cfg.chat.fastPath,
5688
5786
  conciseness: stripUndef({
@@ -69,7 +69,7 @@ export function simLines(spec: TurnSimSpec): string[] {
69
69
  case 'idle': return [];
70
70
  case 'interrupted':
71
71
  return [...head, ' 일부 답변이 여기까지 나왔습니다.', '',
72
- ' ✘ Streaming · interrupted (24s · ↓ 34 tokens)'];
72
+ ' ✘ 중단됨 (24s · ↓ 34 tokens)'];
73
73
  case 'streaming-with-subagents': {
74
74
  const n = Math.max(1, spec.children ?? 2);
75
75
  return [...head, ...Array.from({ length: n }, (_, i) =>
@@ -1 +1 @@
1
- {"commit":"eadd7c31e07d4414ce452d943984a2e58a01ede4"}
1
+ {"commit":"8f3512a904cff53bb1595fffc091f7443a29c2c2"}