elanous 0.2.9 → 0.2.10

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 (361) 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/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
  14. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-f972fc1c00a62e88.js → page-54b32bd27726588d.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/editor/{page-973996d584f008c1.js → page-7ed95f296f5ee8e9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/market/{page-8b0402a46a749a3d.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-037f83e2a1edc8e2.js → page-6cb0e1bbefe5fb3d.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0df30e0c7e50148a.js → page-e168fa1030af0548.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-90670589b87abea8.js → page-26102fd7350f171e.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-35bedde6d885f08f.js → page-139ba5c367bbf60a.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
  61. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
  62. package/apps/pwa/out/approvals/index.html +1 -1
  63. package/apps/pwa/out/approvals/index.txt +14 -14
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +14 -14
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +14 -14
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +14 -14
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +14 -14
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +14 -14
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +14 -14
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +14 -14
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +14 -14
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +14 -14
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +14 -14
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +14 -14
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +14 -14
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +14 -14
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +14 -14
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +14 -14
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +14 -14
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +14 -14
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +14 -14
  105. package/apps/pwa/out/ops/seats/index.html +1 -0
  106. package/apps/pwa/out/ops/seats/index.txt +30 -0
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -0
  130. package/apps/pwa/out/today/index.txt +33 -0
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/package.json +1 -1
  146. package/plugins/card-followup/graphs/run-step.ts +48 -13
  147. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  148. package/plugins/doc-draft/graphs/run-step.ts +13 -6
  149. package/scripts/ci-pwa-build-gate.ts +160 -0
  150. package/scripts/coord-channel-watch.sh +42 -8
  151. package/scripts/coord-digest.ts +199 -0
  152. package/scripts/coord-post.sh +60 -3
  153. package/scripts/install.ps1 +68 -9
  154. package/scripts/install.sh +39 -12
  155. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  156. package/scripts/lib/tui-regress-checks.ts +146 -0
  157. package/scripts/memory-lifecycle-cycle.ts +2 -2
  158. package/scripts/ops-now-probe.ts +327 -0
  159. package/scripts/publish-internal-dist.ts +11 -1
  160. package/scripts/release-loop/cutoff.ts +19 -8
  161. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  162. package/scripts/release-loop/release-readiness.ts +9 -0
  163. package/scripts/release-loop/version-node.ts +58 -10
  164. package/scripts/repo-watch-cycle.ts +1 -1
  165. package/scripts/retro-cycle.ts +2 -2
  166. package/scripts/schedule-health-report.ts +1 -1
  167. package/scripts/storyboard/storyboard.ts +5 -161
  168. package/scripts/test-diet/lib.ts +30 -1
  169. package/scripts/test-diet/node.ts +82 -7
  170. package/scripts/test-timeout-audit.ts +114 -0
  171. package/scripts/tree-sync-alert.ts +1 -1
  172. package/scripts/tui-regress.ts +71 -0
  173. package/scripts/waitlist-daily-digest.ts +1 -1
  174. package/skills/explainer-video/SKILL.md +4 -0
  175. package/skills/explainer-video/engine/reel.mjs +18 -13
  176. package/skills/explainer-video/engine/reel.sh +24 -5
  177. package/skills/explainer-video/engine/stitch.mjs +145 -0
  178. package/skills/pitch-deck/SKILL.md +36 -0
  179. package/skills/pitch-deck/template.html +80 -0
  180. package/src/acp/codex-approval-adapter.ts +9 -3
  181. package/src/acp/core-turn-bridge.ts +117 -18
  182. package/src/acp/fact-question.ts +11 -0
  183. package/src/acp/server.ts +48 -7
  184. package/src/acp/tool-approval.ts +126 -0
  185. package/src/acp/turn-runner.ts +36 -0
  186. package/src/agent/monad-agent-turn.ts +8 -0
  187. package/src/ask-user-question/pending-questions.ts +25 -10
  188. package/src/ask-user-question/types.ts +9 -0
  189. package/src/auth/setup-link-tokens.ts +16 -5
  190. package/src/autopilot/build/integrity-gate.ts +12 -0
  191. package/src/boot/daemon-prompt-turn.ts +3 -0
  192. package/src/boot/daemon-tools/index.ts +8 -0
  193. package/src/boot/daemon-tools/types.ts +2 -0
  194. package/src/boot/daemon-tools/web-search.ts +1 -0
  195. package/src/chat/ctrl-c-in-input.ts +14 -0
  196. package/src/chat/index.ts +52 -0
  197. package/src/chat/pickers/state.ts +6 -2
  198. package/src/cli/agent-cli.ts +7 -0
  199. package/src/cli/decisions-cli.ts +25 -3
  200. package/src/cli/doctor-bundle-upload.ts +68 -0
  201. package/src/cli/doctor-cli.ts +38 -4
  202. package/src/cli/ops-cli.ts +20 -0
  203. package/src/cli/pod-cli.ts +49 -0
  204. package/src/cli/pr-cli.ts +31 -2
  205. package/src/cli/pwa-stop.ts +163 -13
  206. package/src/cli/pwa-test.ts +3 -0
  207. package/src/cli/release-cli.ts +63 -20
  208. package/src/cli/self-update.ts +14 -0
  209. package/src/cli/storyboard-cli.ts +37 -0
  210. package/src/cli/test-state-dir-flag.ts +11 -4
  211. package/src/connectors/linear.ts +146 -0
  212. package/src/context-bus/coord-events.ts +115 -0
  213. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  214. package/src/dashboard/index.ts +58 -21
  215. package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  217. package/src/decisions/decision-cards.ts +49 -1
  218. package/src/decisions/decision-ledger.ts +75 -4
  219. package/src/decisions/decision-linear-projection.ts +157 -0
  220. package/src/decisions/web-push-decision-cards.ts +50 -0
  221. package/src/directive/interpretation-card.ts +36 -0
  222. package/src/discord.ts +2 -1
  223. package/src/domains/coo-admin-tool.ts +62 -0
  224. package/src/domains/core-tools.ts +11 -3
  225. package/src/domains/elanous-card.ts +29 -0
  226. package/src/domains/ops-facts-tool.ts +76 -0
  227. package/src/domains/outbound-alert.ts +41 -13
  228. package/src/domains/release-tool.ts +168 -0
  229. package/src/domains/telegram-kind-route.ts +19 -1
  230. package/src/exec-requests/planner.ts +98 -28
  231. package/src/exec-requests/runner.ts +107 -10
  232. package/src/exec-requests/store.ts +8 -2
  233. package/src/field/field-media.ts +10 -7
  234. package/src/field/field-reel.ts +14 -4
  235. package/src/git-fs/worktree.ts +45 -6
  236. package/src/graph-runner/graph-approval-card.ts +113 -0
  237. package/src/graph-runner/graph-cli.ts +129 -4
  238. package/src/graph-runner/graph-notify.ts +25 -3
  239. package/src/graph-runner/runner.ts +142 -16
  240. package/src/harness/browser-verify.ts +25 -2
  241. package/src/harness/harness-cli-command.ts +231 -35
  242. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  243. package/src/harness/harness-incidents.ts +129 -0
  244. package/src/harness/harness-stop.ts +53 -3
  245. package/src/hitl/clarification-policy.ts +3 -1
  246. package/src/index.ts +156 -37
  247. package/src/intake-plane/author-goals.ts +37 -4
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/discord-seat-work.ts +47 -2
  250. package/src/intake-plane/seat-doc-route.ts +170 -6
  251. package/src/intake-plane/telegram-seat-work.ts +44 -2
  252. package/src/intake-plane/telegram-work.ts +1 -0
  253. package/src/maturity/cli-maturity.ts +34 -0
  254. package/src/maturity/feature-maturity.ts +98 -1
  255. package/src/maturity/tui-slash-maturity.ts +31 -0
  256. package/src/memory.ts +32 -4
  257. package/src/nexus/api/exec-requests.ts +33 -3
  258. package/src/nexus/api/http-server.ts +53 -1
  259. package/src/nexus/api/meta-api.ts +11 -0
  260. package/src/nexus/api/ops-api.ts +10 -1
  261. package/src/nexus/api/ops-seats-sources.ts +37 -7
  262. package/src/nexus/api/ops-seats.ts +16 -6
  263. package/src/nexus/api/seat-requests.ts +88 -8
  264. package/src/nexus/api/sessions-store.ts +63 -0
  265. package/src/nexus/api/setup-codex-login.ts +160 -0
  266. package/src/nexus/api/usage-api.ts +102 -0
  267. package/src/nexus/index.ts +45 -4
  268. package/src/onboarding/first-run.ts +89 -0
  269. package/src/onboarding/lan-address.ts +45 -0
  270. package/src/onboarding/web-first.ts +119 -24
  271. package/src/onboarding.ts +4 -0
  272. package/src/ops-now/ops-now-refresh.ts +100 -0
  273. package/src/project/project-store.ts +115 -0
  274. package/src/release-loop/checklist.ts +78 -4
  275. package/src/release-loop/feature-store.ts +154 -21
  276. package/src/release-loop/release-schedule.ts +53 -0
  277. package/src/repl/index.ts +7 -0
  278. package/src/seat-dispatch/ceo-commands.ts +41 -8
  279. package/src/seat-dispatch/ceo-intent.ts +38 -0
  280. package/src/seat-loop/seat-loop.ts +231 -0
  281. package/src/seat-loop/seat-report.ts +64 -0
  282. package/src/self-dev/decompose.ts +66 -11
  283. package/src/self-dev/run-store.ts +10 -1
  284. package/src/self-implement/execution-clarification.ts +115 -0
  285. package/src/self-implement/gate-cli.ts +6 -3
  286. package/src/self-implement/orchestrator.ts +37 -3
  287. package/src/self-implement/pipeline-shape.ts +1 -1
  288. package/src/self-implement/run-status-mapping.ts +5 -3
  289. package/src/session/chat.ts +2 -0
  290. package/src/session/index.ts +9 -0
  291. package/src/shell-env-bootstrap.ts +17 -0
  292. package/src/steward/directive.ts +51 -2
  293. package/src/steward/launch.ts +250 -0
  294. package/src/steward/outcome-notify.ts +115 -0
  295. package/src/steward/steward-cards.ts +8 -2
  296. package/src/steward/triage-plan.ts +32 -4
  297. package/src/steward/triage.ts +93 -9
  298. package/src/steward/working-backwards.ts +64 -0
  299. package/src/storyboard/storyboard.ts +141 -0
  300. package/src/task-cards/card-store.ts +2 -2
  301. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  302. package/src/telegram-card-followup.ts +233 -0
  303. package/src/telegram-report.ts +15 -9
  304. package/src/telegram-run.ts +7 -0
  305. package/src/telegram.ts +29 -8
  306. package/src/tool-runtime/types.ts +4 -0
  307. package/src/tui-client/dashboard-session.ts +2 -0
  308. package/src/user-config.ts +118 -11
  309. package/src/version/packed-revision.json +1 -1
  310. package/src/web-push/notify-exec-request.ts +32 -0
  311. package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/bots/page-5f30e00693527ebd.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-7fbae173b3c103a7.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b4edcce364062ea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/exec/page-016e2bda04eecb1f.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/field/page-b4d6d9a3b56f2940.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/morning/page-adfbd2ba9598e59a.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/page-1a8e4807292f6b17.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/sessions/page-c71c33a573230672.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/tasks/page-369846d2921af584.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/trace/page-77b28bfb0996d5ea.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/workspace/page-4498c3853f4f688d.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  359. package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +0 -1
  360. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  361. /package/apps/pwa/out/_next/static/{bFI5EEwg7T6aG88ibX4Zq → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -13,9 +13,9 @@ import { conatusEnv } from './conatus-env.js';
13
13
  import { spillLongContent } from '../storage/content-spill.js';
14
14
  import { openSurfaceEventsDb, recordEvent } from './surface-events.js';
15
15
  import { latestUserIntentTs } from '../user-intent/index.js';
16
- import { getUserConfig } from '../user-config.js';
16
+ import { getUserConfig, type UserConfig } from '../user-config.js';
17
17
  import { resolveChannelBotToken } from '../channel-bot-token.js';
18
- import { kindRouteTarget } from './telegram-kind-route.js';
18
+ import { isOperationalKind, kindRouteTarget, logKindRouteFallback, mainHomeTarget } from './telegram-kind-route.js';
19
19
  import { debug } from '../debug/log.js';
20
20
  import { resolveDaemonEndpoint } from '../nexus/daemon-endpoint.js';
21
21
  import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
@@ -210,9 +210,14 @@ export function flushDeferred(path = DEFERRED_PATH): number {
210
210
  ].join('\n');
211
211
  observeFlush(items.length, lagMin, [...new Set(items.map(i => i.kind))]);
212
212
  let delivered = 0;
213
- // origin 없는 것 = report 묶음(기존 동작·finance/크론 알림).
213
+ // origin 없는 finance 는 기존 report 묶음. 운영 보류분은 별도로 묶어 운영 봇 경계를 유지한다.
214
214
  const noOrigin = items.filter(i => !i.origin);
215
- if (noOrigin.length && deliver(fmt(noOrigin), 'report')) delivered += noOrigin.length;
215
+ const tradingBatch = noOrigin.filter(i => !isOperationalKind(i.kind));
216
+ if (tradingBatch.length && deliver(fmt(tradingBatch), 'report')) delivered += tradingBatch.length;
217
+ for (const kind of new Set(noOrigin.filter(i => isOperationalKind(i.kind)).map(i => i.kind))) {
218
+ const batch = noOrigin.filter(i => i.kind === kind);
219
+ if (deliver(fmt(batch), kind)) delivered += batch.length;
220
+ }
216
221
  // origin 있는 것 = 발신 채널·스레드별로 묶어 서로 다른 수신자에게 섞이지 않게 발송.
217
222
  const groups = new Map<string, { origin: MissionOrigin; list: Item[] }>();
218
223
  for (const it of items) {
@@ -402,20 +407,43 @@ function sendTelegramRaw(token: string, chatId: string | number, text: string, t
402
407
  return ok;
403
408
  }
404
409
 
405
- /** 텔레그램 직접 발송(TELEGRAM_BOT_TOKEN/CHAT_ID — env 우선, 없으면 CONATUS/.env). report 폴백. */
406
- function sendTelegramDirect(text: string, kind?: string): boolean {
407
- // A purpose kind with a channel role (intake · ops-*) goes to that channel even when the daemon is down.
408
- try {
409
- const target = kindRouteTarget(getUserConfig(), kind);
410
- if (target) return sendTelegramRaw(target.botToken, String(target.chatId), text);
411
- } catch { /* fall through to the legacy direct path */ }
410
+ /** 텔레그램 직접 발송. 운영 kind 는 메인 봇/홈 밖의 env 로 내려가지 않는다. */
411
+ export function sendTelegramDirect(
412
+ text: string, kind?: string,
413
+ deps: { config?: UserConfig; sendRaw?: typeof sendTelegramRaw; legacyEnv?: typeof conatusEnv } = {},
414
+ ): boolean {
415
+ const sendRaw = deps.sendRaw ?? sendTelegramRaw;
416
+ let cfg: UserConfig | undefined;
417
+ try { cfg = deps.config ?? getUserConfig(); } catch { /* unavailable config: operational delivery still fails closed */ }
418
+ if (cfg) {
419
+ try {
420
+ const target = kindRouteTarget(cfg, kind);
421
+ if (target) return sendRaw(target.botToken, String(target.chatId), text);
422
+ } catch { /* unavailable channel routing: fall through */ }
423
+ }
424
+ if (isOperationalKind(kind)) {
425
+ let home: ReturnType<typeof mainHomeTarget> = null;
426
+ let mainToken = '';
427
+ try {
428
+ if (cfg) {
429
+ mainToken = resolveChannelBotToken('telegram', cfg).token;
430
+ home = mainHomeTarget(cfg);
431
+ }
432
+ } catch { /* unavailable main bot */ }
433
+ const rcToken = cfg?.telegram.reportChannel?.botToken;
434
+ logKindRouteFallback(kind, home ? 'main-home' : 'none', !!mainToken && !!cfg?.telegram.reportChannel && (!rcToken || rcToken === mainToken));
435
+ if (home) return sendRaw(home.botToken, String(home.chatId), text);
436
+ console.error('[outbound] 운영 kind 메인 봇/홈 미설정 — 콘솔 출력\n' + text);
437
+ return false;
438
+ }
412
439
  let tok = process.env.TELEGRAM_BOT_TOKEN || '';
413
440
  let chat = process.env.TELEGRAM_CHAT_ID || '';
414
441
  if (!tok || !chat) {
415
- const env = conatusEnv();
442
+ const env = (deps.legacyEnv ?? conatusEnv)();
416
443
  tok = tok || env.TELEGRAM_BOT_TOKEN || '';
417
444
  chat = chat || env.TELEGRAM_CHAT_ID || '';
418
445
  }
446
+ logKindRouteFallback(kind, tok && chat ? 'conatus-env' : 'none', false);
419
447
  if (!tok || !chat) { console.error('[outbound] 토큰/chat 미설정 — 콘솔 출력\n' + text); return false; }
420
- return sendTelegramRaw(tok, chat, text);
448
+ return sendRaw(tok, chat, text);
421
449
  }
@@ -0,0 +1,168 @@
1
+ import type { LLMToolSpec } from '../llm.js';
2
+ import type { ToolRuntimeContext } from '../tool-runtime/types.js';
3
+ import { debug } from '../debug/log.js';
4
+ import { requestConfirmation, type ConfirmOpts, type ConfirmResult } from '../hitl/confirm.js';
5
+ import { devVersion, listChecklist, summarizeChecklist, type Checklist } from '../release-loop/checklist.js';
6
+ import { add, move, validateVersion } from '../release-loop/feature-store.js';
7
+ import { formatKst, getSchedule, setSchedule, type ReleaseSchedule } from '../release-loop/release-schedule.js';
8
+ import { latestGraphRun, type GraphRunState } from '../graph-runner/runner.js';
9
+ import { formatElanousCard, type ElanousCard } from './elanous-card.js';
10
+
11
+ export const RELEASE_STATUS_SPEC: LLMToolSpec = {
12
+ name: 'release_status',
13
+ description: '판 어디까지 · 컷 언제 · 릴리스 상황 · 판올림 · 체크리스트 · 남은 칸을 읽는다. 판 일정, 빨강 칸, 담당별 노랑, 최근 발행 런을 조회하는 읽기 전용 도구. 행정 업무는 coo_admin.',
14
+ parameters: { type: 'object', properties: { version: { type: 'string', description: '조회할 판. 생략하면 개발 판과 다음 판.' } }, required: [] },
15
+ };
16
+
17
+ export const RELEASE_CHANGE_SPEC: LLMToolSpec = {
18
+ name: 'release_change',
19
+ description: '오너의 판올림 변경 요청 — 체크리스트 칸 추가, 칸을 다른 판으로 이동, 판 컷·착지 마감 수정. 실행 전에 사람에게 한 번 확인받는다. 읽기 요청에는 release_status 사용.',
20
+ parameters: { type: 'object', properties: {
21
+ action: { type: 'string', enum: ['add-item', 'move-item', 'set-schedule'] },
22
+ version: { type: 'string', description: 'add-item 또는 set-schedule 대상 판' },
23
+ id: { type: 'string', description: '칸 id (add-item 또는 move-item)' },
24
+ title: { type: 'string', description: '추가할 칸 제목' },
25
+ owner: { type: 'string', description: '추가할 칸 담당(선택)' },
26
+ from: { type: 'string', description: '이동 전 판' },
27
+ to: { type: 'string', description: '이동 후 판' },
28
+ cutAt: { type: 'string', description: 'ISO 오프셋 포함 컷 시각(선택)' },
29
+ landBy: { type: 'string', description: 'ISO 오프셋 포함 착지 마감(선택)' },
30
+ }, required: ['action'] },
31
+ };
32
+
33
+ export interface ReleaseToolDeps {
34
+ now?: Date;
35
+ devVersion?: () => string;
36
+ checklist?: (version: string) => Checklist;
37
+ schedule?: (version: string) => ReleaseSchedule | null;
38
+ latestRun?: (graphId: string) => GraphRunState | null;
39
+ confirm?: (options: ConfirmOpts) => Promise<ConfirmResult>;
40
+ add?: typeof add;
41
+ move?: typeof move;
42
+ setSchedule?: typeof setSchedule;
43
+ }
44
+
45
+ function nextVersion(version: string): string {
46
+ const [major, minor, patch] = version.split('.').map(Number);
47
+ return `${major}.${minor}.${patch! + 1}`;
48
+ }
49
+
50
+ function kstDate(iso: string): string {
51
+ return new Date(Date.parse(iso) + 9 * 60 * 60_000).toISOString().slice(0, 10);
52
+ }
53
+
54
+ function daysLeft(iso: string, now: Date): number {
55
+ const due = Date.parse(`${kstDate(iso)}T00:00:00Z`);
56
+ const today = Date.parse(`${kstDate(now.toISOString())}T00:00:00Z`);
57
+ return (due - today) / 86_400_000;
58
+ }
59
+
60
+ function dDay(days: number): string {
61
+ return days < 0 ? `D+${-days}` : days === 0 ? 'D-day' : `D-${days}`;
62
+ }
63
+
64
+ export function dispatchReleaseStatus(args: Record<string, unknown>, deps: ReleaseToolDeps = {}): { text: string; structured: Record<string, unknown> } {
65
+ const requested = args.version;
66
+ if (requested !== undefined && (typeof requested !== 'string' || !requested.trim())) throw new Error('판 이름이 필요합니다');
67
+ const version = requested ? null : (deps.devVersion?.() ?? devVersion());
68
+ const versions = requested ? [requested as string] : [version!, nextVersion(version!)];
69
+ const now = deps.now ?? new Date();
70
+ const run = (deps.latestRun ?? latestGraphRun)('release-loop');
71
+ const latest = run ? { status: run.status, lastNode: run.path.at(-1) ?? null, startedAt: run.startedAt ?? null } : null;
72
+ const entries = versions.map(v => {
73
+ validateVersion(v);
74
+ const schedule = (deps.schedule ?? getSchedule)(v);
75
+ const data = (deps.checklist ?? listChecklist)(v);
76
+ const summary = summarizeChecklist(data);
77
+ const red = data.items.filter(item => item.status === 'red').map(({ id, title, owner }) => ({ id, title, owner: owner ?? '미배정' }));
78
+ const yellowByOwner: Record<string, number> = {};
79
+ for (const item of data.items.filter(item => item.status === 'yellow')) {
80
+ const owner = item.owner ?? '미배정';
81
+ yellowByOwner[owner] = (yellowByOwner[owner] ?? 0) + 1;
82
+ }
83
+ const cutDaysLeft = schedule ? daysLeft(schedule.cutAt, now) : null;
84
+ const landDaysLeft = schedule?.landBy ? daysLeft(schedule.landBy, now) : null;
85
+ const cutHoursLeft = schedule ? Math.ceil((Date.parse(schedule.cutAt) - now.getTime()) / 3_600_000) : null;
86
+ const landHoursLeft = schedule?.landBy ? Math.ceil((Date.parse(schedule.landBy) - now.getTime()) / 3_600_000) : null;
87
+ return { version: v, schedule: schedule ? { cutAt: schedule.cutAt, landBy: schedule.landBy, cutDaysLeft, landDaysLeft, cutHoursLeft, landHoursLeft } : null,
88
+ counts: { green: summary.green, yellow: summary.yellow, red: summary.red, done: summary.done }, red, yellowByOwner };
89
+ });
90
+ const lines = entries.map(entry => {
91
+ const schedule = entry.schedule;
92
+ return `${entry.version} · 컷 ${schedule ? formatKst(schedule.cutAt) : '미정'}${schedule?.landBy ? ` · 착지 마감 ${formatKst(schedule.landBy)}` : ' · 착지 마감 미정'}${schedule ? ` · 컷 ${dDay(schedule.cutDaysLeft!)} (${schedule.cutHoursLeft}시간)${schedule.landDaysLeft !== null ? ` · 착지 ${dDay(schedule.landDaysLeft)} (${schedule.landHoursLeft}시간)` : ''}` : ''}\n` +
93
+ `초록 ${entry.counts.green} · 노랑 ${entry.counts.yellow} · 빨강 ${entry.counts.red} · 끝 ${entry.counts.done}\n` +
94
+ `빨강 칸: ${entry.red.length ? entry.red.map(item => `${item.id} ${item.title} (${item.owner})`).join(', ') : '없음'}\n` +
95
+ `담당별 노랑: ${Object.keys(entry.yellowByOwner).length ? Object.entries(entry.yellowByOwner).map(([owner, count]) => `${owner} ${count}`).join(', ') : '없음'}`;
96
+ });
97
+ lines.push(latest ? `최근 발행 런: ${latest.status} · 끝 노드 ${latest.lastNode ?? '없음'} · 시작 ${latest.startedAt ?? '미상'}` : '최근 발행 런: 없음');
98
+ const cards: ElanousCard[] = [
99
+ { kind: 'release-schedule', items: entries.map(entry => ({ title: `${entry.version} 컷`, due: entry.schedule ? kstDate(entry.schedule.cutAt) : null, daysLeft: entry.schedule?.cutDaysLeft ?? null, state: entry.schedule ? 'scheduled' : 'undecided', owner: 'release' })), meta: { versions: entries.map(entry => entry.version) } },
100
+ { kind: 'release-checklist', items: entries.flatMap(entry => entry.red.map(item => ({ title: `${item.id} ${item.title}`, due: null, daysLeft: null, state: 'red', owner: item.owner }))), meta: { counts: entries.map(entry => ({ version: entry.version, ...entry.counts })) } },
101
+ ];
102
+ const structured = { releases: entries, latestRun: latest };
103
+ debug.log('release.tool', 'status', { action: 'status', version: versions.join(',') });
104
+ return { text: `${lines.join('\n\n')}\n${cards.map(formatElanousCard).join('\n')}`, structured };
105
+ }
106
+
107
+ function requiredString(args: Record<string, unknown>, key: string): string {
108
+ const value = args[key];
109
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${key} 값이 필요합니다`);
110
+ return value.trim();
111
+ }
112
+
113
+ /** Never accepts a user-supplied owner claim: only a human, server-assigned owner context can authorize a mutation. */
114
+ export async function dispatchReleaseChange(args: Record<string, unknown>, context?: ToolRuntimeContext, deps: ReleaseToolDeps = {}): Promise<string> {
115
+ const action = typeof args.action === 'string' ? args.action : '';
116
+ const version = typeof args.version === 'string' ? args.version : typeof args.from === 'string' ? args.from : '';
117
+ const ownerId = context?.verifiedOwner?.id;
118
+ if (context?.requestOrigin === 'external-agent' || !context?.sessionId || !ownerId?.trim()) {
119
+ debug.log('release.tool', 'change-refused', { action, version });
120
+ return '서버에서 검증한 오너의 사람 채팅 문맥에서만 판올림을 바꿀 수 있습니다.';
121
+ }
122
+ const channels = context.confirmChannels;
123
+ if (!channels?.length) {
124
+ debug.log('release.tool', 'change-refused', { action, version });
125
+ return '원래 채팅의 확인 채널이 없어 판올림을 바꾸지 않았습니다.';
126
+ }
127
+ let prompt: string;
128
+ let apply: () => void;
129
+ if (action === 'add-item') {
130
+ const v = requiredString(args, 'version');
131
+ const id = requiredString(args, 'id');
132
+ const title = requiredString(args, 'title');
133
+ validateVersion(v);
134
+ const owner = args.owner === undefined ? undefined : requiredString(args, 'owner');
135
+ prompt = `${v} 에 칸 ${id} «${title}» 추가 — 진행할까요?`;
136
+ apply = () => { (deps.add ?? add)(v, { id, title, status: 'yellow', ...(owner ? { owner } : {}), updatedAt: new Date().toISOString(), updatedBy: ownerId }); };
137
+ } else if (action === 'move-item') {
138
+ const id = requiredString(args, 'id');
139
+ const from = requiredString(args, 'from');
140
+ const to = requiredString(args, 'to');
141
+ validateVersion(from); validateVersion(to);
142
+ if (from === to) throw new Error('같은 판으로 옮길 수 없습니다');
143
+ prompt = `${id} 칸을 ${from} 에서 ${to} 로 이동 — 진행할까요?`;
144
+ apply = () => { (deps.move ?? move)(id, from, to, ownerId); };
145
+ } else if (action === 'set-schedule') {
146
+ const v = requiredString(args, 'version');
147
+ validateVersion(v);
148
+ const cutAt = args.cutAt === undefined ? undefined : requiredString(args, 'cutAt');
149
+ const landBy = args.landBy === undefined ? undefined : requiredString(args, 'landBy');
150
+ if (!cutAt && !landBy) throw new Error('cutAt 또는 landBy 가 필요합니다');
151
+ // Validate offsets before asking for approval, not after.
152
+ for (const iso of [cutAt, landBy].filter((value): value is string => !!value)) {
153
+ if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d{1,3})?)?(?:Z|[+-]\d\d:\d\d)$/.test(iso) || !Number.isFinite(Date.parse(iso))) throw new Error(`잘못된 ISO 시각: ${iso}`);
154
+ }
155
+ prompt = `${v} 판 일정 ${cutAt ? `컷 ${formatKst(cutAt)}` : ''}${landBy ? ` 착지 마감 ${formatKst(landBy)}` : ''} 변경 — 진행할까요?`;
156
+ apply = () => { (deps.setSchedule ?? setSchedule)(v, { ...(cutAt ? { cutAt } : {}), ...(landBy ? { landBy } : {}) }, ownerId); };
157
+ } else throw new Error('action 은 add-item, move-item, set-schedule 중 하나여야 합니다');
158
+ debug.log('release.tool', 'change-requested', { action, version });
159
+ const result = await (deps.confirm ?? requestConfirmation)({ prompt, onTimeout: () => false, failOpen: false,
160
+ channels });
161
+ if (!result.answer || !channels.some(channel => channel.name === result.channel)) {
162
+ debug.log('release.tool', 'change-refused', { action, version });
163
+ return `${prompt} — 승인되지 않아 변경하지 않았습니다.`;
164
+ }
165
+ apply();
166
+ debug.log('release.tool', 'change-confirmed', { action, version });
167
+ return `${prompt} — 변경했습니다.`;
168
+ }
@@ -1,12 +1,30 @@
1
1
  // 발송 목적(kind) → 텔레그램 채널 역할 · 대상. 가벼운 모듈(telegram.ts 를 import 하지 않는다 — outbound-alert 순환 방지).
2
- // 표에 없는 kind 는 undefined → 부르는 쪽이 옛 reportChannel 로 간다(투자 알림 등 기존 동작 그대로).
2
+ // 표에 없는 kind 는 undefined → 부르는 쪽이 폴백을 판정한다(투자 알림은 옛 reportChannel 유지).
3
3
  import type { UserConfig } from '../user-config.js';
4
+ import { resolveChannelBotToken } from '../channel-bot-token.js';
5
+ import { debug } from '../debug/log.js';
4
6
  import { channelForRole, resolveTelegramChannels } from './telegram-channels.js';
5
7
 
6
8
  export const DEFAULT_KIND_ROLES: Readonly<Record<string, string>> = {
7
9
  intake: 'system', 'ops-report': 'system', 'ops-alert': 'system', 'ops-health': 'system',
10
+ regression: 'system', 'agent-mission': 'system',
8
11
  };
9
12
 
13
+ /** No channel mapping for an operational kind may cross into a different bot. */
14
+ export function isOperationalKind(kind?: string): boolean {
15
+ return kind?.startsWith('ops-') ?? false;
16
+ }
17
+
18
+ export function mainHomeTarget(cfg: UserConfig): { botToken: string; chatId: number } | null {
19
+ const botToken = resolveChannelBotToken('telegram', cfg).token;
20
+ const chatId = cfg.telegram.homeChannel ?? cfg.telegram.allowedUsers[0];
21
+ return botToken && chatId != null && Number.isFinite(chatId) ? { botToken, chatId } : null;
22
+ }
23
+
24
+ export function logKindRouteFallback(kind: string | undefined, to: 'report-channel' | 'main-home' | 'conatus-env' | 'none', sameBot: boolean): void {
25
+ try { debug.log('telegram.kind-route', 'fallback', { kind, to, sameBot }); } catch { /* logging must not prevent delivery */ }
26
+ }
27
+
10
28
  export function roleForKind(cfg: UserConfig, kind?: string): string | undefined {
11
29
  if (!kind) return undefined;
12
30
  return cfg.telegram.kindRoles?.[kind] ?? DEFAULT_KIND_ROLES[kind];
@@ -1,10 +1,11 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { existsSync, readFileSync, readdirSync, statSync, type Dirent } from 'node:fs';
2
+ import { dirname, join, resolve, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { parse as parseYaml } from 'yaml';
5
5
  import { elanousStateRoot } from '../autopilot/state-paths.js';
6
6
  import { defaultGraphsDir } from '../self-implement/graph-templates.js';
7
- import { handleGraphsGet } from '../nexus/api/graphs-api.js';
7
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
8
+ import type { ExecAttachment } from './store.js';
8
9
  import { debug } from '../debug/log.js';
9
10
 
10
11
  export interface InstalledGraph {
@@ -12,7 +13,7 @@ export interface InstalledGraph {
12
13
  title: string;
13
14
  description: string;
14
15
  path: string;
15
- /** 플러그인 그래프가 받는 입력 키(`examples/*.json` 표본에서) — COO 가 inputs 를 채울 수 있게. */
16
+ /** 코어 loop.inputs 또는 플러그인 examples/*.json 표본의 입력 키. */
16
17
  inputKeys?: string[];
17
18
  }
18
19
  export interface ExecPlanItem {
@@ -83,24 +84,41 @@ export async function installedGraphs(coreDir = defaultGraphsDir(), mineDir = jo
83
84
  }
84
85
 
85
86
  async function coreAndMineGraphs(coreDir: string, mineDir: string): Promise<InstalledGraph[]> {
86
- const catalog = handleGraphsGet('/v1/graphs', coreDir, { coreDir, mineDir });
87
- if (!catalog.ok) throw new Error('installed graph catalog unavailable');
88
- const { graphs } = await catalog.json() as { graphs: Array<{ id: string; source: 'core' | 'mine' }> };
89
- return graphs.flatMap(({ id, source }) => {
90
- const dir = source === 'core' ? coreDir : mineDir;
91
- if (!/^[a-z0-9-]+$/.test(id) || !existsSync(join(dir, 'recipes.yaml'))) return [];
92
- let names: string[];
93
- try { names = readdirSync(dir).filter(name => /\.ya?ml$/.test(name)); }
94
- catch { return []; }
95
- for (const name of names) {
96
- const path = join(dir, name);
97
- try {
98
- const doc = parseYaml(readFileSync(path, 'utf8')) as { graph_id?: string; loop?: { title?: string; description?: string } };
99
- if (doc?.graph_id === id) return [{ id, title: doc.loop?.title ?? id, description: doc.loop?.description ?? '', path }];
100
- } catch { /* A malformed file is not an executable graph. */ }
87
+ const graphs: InstalledGraph[] = [];
88
+ const seen = new Set<string>();
89
+ const scan = (dir: string, core: boolean): void => {
90
+ let entries: Dirent[];
91
+ try { entries = readdirSync(dir, { withFileTypes: true }); }
92
+ catch { return; }
93
+ if (!core || dir === coreDir || entries.some(entry => entry.isFile() && entry.name === 'recipes.yaml')) {
94
+ for (const entry of entries) {
95
+ if (!entry.isFile() || !/\.ya?ml$/.test(entry.name) || entry.name === 'recipes.yaml') continue;
96
+ const path = join(dir, entry.name);
97
+ try {
98
+ const doc = parseYaml(readFileSync(path, 'utf8')) as {
99
+ graph_id?: unknown;
100
+ loop?: { title?: string; description?: string; exec_request?: unknown; inputs?: unknown };
101
+ } | null;
102
+ const id = doc?.graph_id;
103
+ if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id) || seen.has(id)) continue;
104
+ if (core && doc?.loop?.exec_request !== true) continue;
105
+ const inputs = doc?.loop?.inputs;
106
+ const inputKeys = core && Array.isArray(inputs)
107
+ ? inputs.filter((key): key is string => typeof key === 'string')
108
+ : core && inputs && typeof inputs === 'object' ? Object.keys(inputs) : undefined;
109
+ graphs.push({ id, title: doc?.loop?.title ?? id, description: doc?.loop?.description ?? '', path,
110
+ ...(inputKeys ? { inputKeys } : {}) });
111
+ seen.add(id);
112
+ } catch { /* A malformed file is not an executable graph. */ }
113
+ }
101
114
  }
102
- return [];
103
- });
115
+ if (core) for (const entry of entries) {
116
+ if (entry.isDirectory() && !entry.name.startsWith('.')) scan(join(dir, entry.name), true);
117
+ }
118
+ };
119
+ scan(coreDir, true);
120
+ scan(mineDir, false);
121
+ return graphs;
104
122
  }
105
123
 
106
124
  export function seatTitles(path = fileURLToPath(new URL('../../scripts/coord-tracks.json', import.meta.url))): string[] {
@@ -108,8 +126,32 @@ export function seatTitles(path = fileURLToPath(new URL('../../scripts/coord-tra
108
126
  return data.tracks.flatMap(track => track.title ? [track.title] : []);
109
127
  }
110
128
 
111
- export async function judgeExecPlan(text: string, graphs: readonly InstalledGraph[], seats: readonly string[]): Promise<unknown> {
112
- const prompt = `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{},"after":[]}]. 앞 항목의 결과가 있어야 할 수 있는 일(예: 점검·조사 «결과로» 쓰는 한 장 · 보고)은 after 에 그 앞 항목 번호(0부터)를 적는다 — 그 항목은 앞 항목이 끝난 뒤 그 결과를 받아 시작한다. 서로 기다릴 필요가 없으면 after 는 빈 배열. 자리 이름은 주어진 목록에서만, 그래프 ID는 설치된 목록에서만 고른다. 맞는 그래프가 없으면 graphId=""로 표시한다. 그래프에 inputKeys 가 있으면 inputs 를 그 키로만 채우되 한 줄에서 알 수 있는 값만 넣는다(모르는 값은 넣지 않는다). 그 그래프가 꼭 받아야 할 값(예: 브랜드 이름 · 사진 파일)을 한 줄에서 얻을 수 없으면 graphId=""로 두고 title 끝에 « — <무엇> 필요»를 적는다. 없는 그래프나 산출을 지어내지 마라. 바깥 게시·발행·광고·결제는 승인 노드를 거치기 전 실행하면 안 된다.\n자리: ${JSON.stringify(seats)}\n실행 가능한 그래프: ${JSON.stringify(graphs.map(({ id, title, description, inputKeys }) => ({ id, title, description, ...(inputKeys ? { inputKeys } : {}) })))}\n한 줄: ${JSON.stringify(text)}`;
129
+ export function latestFieldFolder(configRoot = getElanousConfigDir()): string | null {
130
+ const root = join(configRoot, 'field');
131
+ let latest: { path: string; time: number } | null = null;
132
+ try {
133
+ for (const event of readdirSync(root, { withFileTypes: true })) {
134
+ if (!event.isDirectory() || event.name.startsWith('.')) continue;
135
+ const path = join(root, event.name);
136
+ try {
137
+ const photos = readdirSync(path, { withFileTypes: true }).filter(file => file.isFile() && /\.(?:png|jpe?g|webp|heic)$/i.test(file.name));
138
+ for (const photo of photos) {
139
+ const time = statSync(join(path, photo.name)).mtimeMs;
140
+ if (!latest || time > latest.time || (time === latest.time && path.localeCompare(latest.path) > 0)) latest = { path, time };
141
+ }
142
+ } catch { /* Unreadable event does not supply photos. */ }
143
+ }
144
+ } catch { /* No field directory. */ }
145
+ return latest?.path ?? null;
146
+ }
147
+
148
+ export function execPlanPrompt(text: string, graphs: readonly InstalledGraph[], seats: readonly string[], attachments: readonly ExecAttachment[] = [], fieldFolder: string | null = latestFieldFolder()): string {
149
+ const supplied = attachments.map(({ name, path }) => `첨부: ${name} (${/\.(?:png|jpe?g|webp|gif|heic)$/i.test(name) ? '이미지' : '파일'}) — ${path}`).join('\n');
150
+ return `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{},"after":[]}]. 앞 항목의 결과가 있어야 할 수 있는 일(예: 점검·조사 «결과로» 쓰는 한 장 · 보고)은 after 에 그 앞 항목 번호(0부터)를 적는다 — 그 항목은 앞 항목이 끝난 뒤 그 결과를 받아 시작한다. 서로 기다릴 필요가 없으면 after 는 빈 배열. 자리 이름은 주어진 목록에서만, 그래프 ID는 설치된 목록에서만 고른다. 맞는 그래프가 없으면 graphId=""로 표시한다. 그래프에 inputKeys 가 있으면 inputs 를 그 키로만 채우되 한 줄과 아래 알려진 값에서 얻을 수 있는 값만 넣는다(모르는 값은 넣지 않는다). image/photo/file 입력에는 알맞은 첨부 경로를 쓸 수 있다. folder 입력에는 첨부가 없거나 첨부 사진이 그 현장 폴더 안에 있을 때만 오늘 현장 폴더를 쓸 수 있다. 다른 현장 사진이나 출처를 모르는 업로드 사진이 있으면 폴더를 추측하지 마라. 그 그래프가 꼭 받아야 할 값(예: 브랜드 이름 · 사진 파일)을 알 수 없으면 graphId=""로 두고 title 끝에 « — <무엇> 필요»를 적는다. 단순히 묻는 말에는 없는 그래프를 지어내지 말고 graphId=""로 표시한다. 없는 그래프나 산출을 지어내지 마라. 바깥 게시·발행·광고·결제는 승인 노드를 거치기 전 실행하면 안 된다.\n자리: ${JSON.stringify(seats)}\n실행 가능한 그래프: ${JSON.stringify(graphs.map(({ id, title, description, inputKeys }) => ({ id, title, description, ...(inputKeys ? { inputKeys } : {}) })))}\n${fieldFolder ? `알려진 값: 오늘 현장 폴더 = ${fieldFolder}\n` : ''}${supplied ? `${supplied}\n` : ''}한 줄: ${JSON.stringify(text)}`;
151
+ }
152
+
153
+ export async function judgeExecPlan(text: string, graphs: readonly InstalledGraph[], seats: readonly string[], attachments: readonly ExecAttachment[] = [], fieldFolder: string | null = latestFieldFolder()): Promise<unknown> {
154
+ const prompt = execPlanPrompt(text, graphs, seats, attachments, fieldFolder);
113
155
  const { streamLLM } = await import('../llm.js');
114
156
  const { tierModel } = await import('../llm/model-defaults.js');
115
157
  const raw = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('best') });
@@ -120,13 +162,17 @@ export async function planExecRequest(text: string, deps: {
120
162
  graphs?: () => Promise<InstalledGraph[]>;
121
163
  seats?: () => string[];
122
164
  judge?: typeof judgeExecPlan;
165
+ attachments?: readonly ExecAttachment[];
166
+ fieldFolder?: () => string | null;
123
167
  } = {}): Promise<ExecPlanItem[]> {
124
168
  const graphs = await (deps.graphs ?? installedGraphs)();
125
169
  const seats = (deps.seats ?? seatTitles)();
126
- const raw = await (deps.judge ?? judgeExecPlan)(text, graphs, seats);
170
+ const attachments = deps.attachments ?? [];
171
+ const fieldFolder = (deps.fieldFolder ?? latestFieldFolder)();
172
+ const raw = await (deps.judge ?? judgeExecPlan)(text, graphs, seats, attachments, fieldFolder);
127
173
  if (!Array.isArray(raw) || raw.length === 0) throw new Error('COO 계획이 비었거나 배열이 아닙니다');
128
174
  const known = new Map(graphs.map(graph => [graph.id, graph]));
129
- return raw.map((item: unknown, index: number) => {
175
+ const plans: ExecPlanItem[] = raw.map((item: unknown, index: number) => {
130
176
  if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error('COO 계획 항목 형식 오류');
131
177
  const row = item as Record<string, unknown>;
132
178
  if (typeof row.seat !== 'string' || !seats.includes(row.seat) || typeof row.title !== 'string' || !row.title.trim()
@@ -136,11 +182,35 @@ export async function planExecRequest(text: string, deps: {
136
182
  const after = Array.isArray(row.after)
137
183
  ? [...new Set(row.after.filter((n): n is number => Number.isInteger(n) && (n as number) >= 0 && (n as number) < index))]
138
184
  : [];
185
+ const graph = known.get(row.graphId);
186
+ const inputs = { ...row.inputs as Record<string, unknown> };
187
+ let folderReason: string | undefined;
188
+ if (graph) {
189
+ for (const key of graph.inputKeys ?? []) {
190
+ if (key === 'folder') {
191
+ // A folder input must contain every attached photo — never guess it from «the latest event» (review r1).
192
+ const photos = attachments.filter(a => /\.(?:png|jpe?g|webp|gif|heic)$/i.test(a.name)).map(a => resolve(a.path));
193
+ const holds = (folder: string) => photos.every(photo => photo.startsWith(`${resolve(folder)}${sep}`));
194
+ const given = typeof inputs.folder === 'string' ? inputs.folder : undefined;
195
+ if (given !== undefined) {
196
+ if (!holds(given)) { delete inputs.folder; folderReason = `${row.seat}: folder 필요 — 첨부 사진이 그 현장 폴더 안에 없습니다`; }
197
+ } else if (fieldFolder && holds(fieldFolder)) inputs.folder = fieldFolder;
198
+ else if (photos.length) folderReason = `${row.seat}: folder 필요 — 첨부 사진의 현장 폴더를 알 수 없습니다`;
199
+ continue;
200
+ }
201
+ if (inputs[key] !== undefined) continue;
202
+ if (['image', 'photo', 'file'].includes(key)) {
203
+ const attachment = attachments.find(a => key === 'file' || /\.(?:png|jpe?g|webp|gif|heic)$/i.test(a.name));
204
+ if (attachment) inputs[key] = attachment.path;
205
+ }
206
+ }
207
+ }
139
208
  return {
140
- seat: row.seat, title: row.title.trim(), graphId: row.graphId,
141
- inputs: row.inputs as Record<string, unknown>,
209
+ seat: row.seat, title: row.title.trim(), graphId: row.graphId, inputs,
142
210
  ...(after.length ? { after } : {}),
143
- ...(!known.has(row.graphId) ? { reason: `${row.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}),
211
+ ...(!graph ? { reason: `${row.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : folderReason ? { reason: folderReason } : {}),
144
212
  };
145
213
  });
214
+ for (const plan of plans) debug.log('exec.planner', 'select', { seat: plan.seat, chosen: plan.graphId, candidates: graphs.slice(0, 10).map(graph => graph.id) });
215
+ return plans;
146
216
  }