elanous 0.2.8 → 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 (385) 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-d4b6df905f185055.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-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
  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-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
  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-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.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-7464c1987dd76e6d.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-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
  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-26102fd7350f171e.js +1 -0
  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-139ba5c367bbf60a.js +1 -0
  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 +30 -29
  64. package/apps/pwa/out/autopilot/index.html +1 -1
  65. package/apps/pwa/out/autopilot/index.txt +30 -29
  66. package/apps/pwa/out/board/index.html +1 -1
  67. package/apps/pwa/out/board/index.txt +30 -29
  68. package/apps/pwa/out/botlab/index.html +1 -1
  69. package/apps/pwa/out/botlab/index.txt +27 -26
  70. package/apps/pwa/out/bots/index.html +1 -1
  71. package/apps/pwa/out/bots/index.txt +27 -26
  72. package/apps/pwa/out/chat/index.html +1 -1
  73. package/apps/pwa/out/chat/index.txt +27 -26
  74. package/apps/pwa/out/consult/index.html +1 -1
  75. package/apps/pwa/out/consult/index.txt +30 -29
  76. package/apps/pwa/out/control/index.html +1 -1
  77. package/apps/pwa/out/control/index.txt +30 -29
  78. package/apps/pwa/out/design-check/index.html +1 -1
  79. package/apps/pwa/out/design-check/index.txt +30 -29
  80. package/apps/pwa/out/editor/index.html +1 -1
  81. package/apps/pwa/out/editor/index.txt +30 -29
  82. package/apps/pwa/out/exec/index.html +1 -1
  83. package/apps/pwa/out/exec/index.txt +30 -29
  84. package/apps/pwa/out/field/index.html +1 -1
  85. package/apps/pwa/out/field/index.txt +30 -29
  86. package/apps/pwa/out/healthz +1 -1
  87. package/apps/pwa/out/index.html +1 -1
  88. package/apps/pwa/out/index.txt +27 -26
  89. package/apps/pwa/out/intake/index.html +1 -1
  90. package/apps/pwa/out/intake/index.txt +30 -29
  91. package/apps/pwa/out/live/index.html +1 -1
  92. package/apps/pwa/out/live/index.txt +30 -29
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +27 -26
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +27 -26
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +30 -29
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +30 -29
  101. package/apps/pwa/out/ops/checklist/index.html +1 -0
  102. package/apps/pwa/out/ops/checklist/index.txt +30 -0
  103. package/apps/pwa/out/ops/release/index.html +1 -0
  104. package/apps/pwa/out/ops/release/index.txt +30 -0
  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 +28 -27
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +30 -29
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +27 -26
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +30 -29
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +30 -29
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +30 -29
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +30 -29
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +30 -29
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +27 -26
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +30 -29
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +30 -29
  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 +30 -29
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +30 -29
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +30 -29
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +30 -29
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +30 -29
  143. package/graphs/field/field-feed.yaml +2 -0
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +3 -1
  147. package/graphs/test-diet/nightly-audit.yaml +18 -0
  148. package/graphs/test-diet/recipes.yaml +3 -0
  149. package/package.json +1 -1
  150. package/plugins/card-followup/graphs/run-step.ts +48 -13
  151. package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
  152. package/plugins/doc-draft/graphs/run-step.ts +14 -7
  153. package/scripts/ci-pwa-build-gate.ts +160 -0
  154. package/scripts/coord-channel-watch.sh +42 -8
  155. package/scripts/coord-digest.ts +199 -0
  156. package/scripts/coord-post.sh +60 -3
  157. package/scripts/install.ps1 +68 -9
  158. package/scripts/install.sh +39 -12
  159. package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
  160. package/scripts/lib/tui-regress-checks.ts +146 -0
  161. package/scripts/memory-lifecycle-cycle.ts +2 -2
  162. package/scripts/ops-now-probe.ts +327 -0
  163. package/scripts/publish-internal-dist.ts +11 -1
  164. package/scripts/release-loop/cutoff.ts +31 -2
  165. package/scripts/release-loop/export-check-node.ts +85 -0
  166. package/scripts/release-loop/gate-node.ts +18 -4
  167. package/scripts/release-loop/node-verdict.ts +8 -1
  168. package/scripts/release-loop/ops-upgrade-node.ts +72 -16
  169. package/scripts/release-loop/release-readiness.ts +9 -0
  170. package/scripts/release-loop/version-node.ts +58 -10
  171. package/scripts/repo-watch-cycle.ts +1 -1
  172. package/scripts/retro-cycle.ts +2 -2
  173. package/scripts/schedule-health-report.ts +1 -1
  174. package/scripts/storyboard/storyboard.ts +5 -161
  175. package/scripts/test-diet/lib.ts +31 -1
  176. package/scripts/test-diet/node.ts +134 -17
  177. package/scripts/test-timeout-audit.ts +114 -0
  178. package/scripts/tree-sync-alert.ts +1 -1
  179. package/scripts/tui-regress.ts +71 -0
  180. package/scripts/waitlist-daily-digest.ts +1 -1
  181. package/skills/explainer-video/SKILL.md +4 -0
  182. package/skills/explainer-video/engine/reel.mjs +18 -13
  183. package/skills/explainer-video/engine/reel.sh +24 -5
  184. package/skills/explainer-video/engine/stitch.mjs +145 -0
  185. package/skills/pitch-deck/SKILL.md +36 -0
  186. package/skills/pitch-deck/template.html +80 -0
  187. package/src/acp/codex-approval-adapter.ts +9 -3
  188. package/src/acp/core-turn-bridge.ts +117 -18
  189. package/src/acp/fact-question.ts +11 -0
  190. package/src/acp/server.ts +48 -7
  191. package/src/acp/tool-approval.ts +126 -0
  192. package/src/acp/turn-runner.ts +36 -0
  193. package/src/agent/monad-agent-turn.ts +8 -0
  194. package/src/ask-user-question/pending-questions.ts +25 -10
  195. package/src/ask-user-question/types.ts +9 -0
  196. package/src/auth/setup-link-tokens.fixture.ts +19 -0
  197. package/src/auth/setup-link-tokens.ts +164 -0
  198. package/src/autopilot/build/integrity-gate.ts +12 -0
  199. package/src/boot/daemon-prompt-turn.ts +3 -0
  200. package/src/boot/daemon-tools/index.ts +8 -0
  201. package/src/boot/daemon-tools/types.ts +2 -0
  202. package/src/boot/daemon-tools/web-search.ts +1 -0
  203. package/src/budget/unified-usage.ts +75 -0
  204. package/src/channel-bot-token.ts +37 -10
  205. package/src/chat/ctrl-c-in-input.ts +14 -0
  206. package/src/chat/index.ts +61 -9
  207. package/src/chat/pickers/state.ts +6 -2
  208. package/src/cli/agent-cli.ts +7 -0
  209. package/src/cli/decisions-cli.ts +25 -3
  210. package/src/cli/doctor-bundle-upload.ts +68 -0
  211. package/src/cli/doctor-cli.ts +38 -4
  212. package/src/cli/op-proxy-sync.ts +83 -0
  213. package/src/cli/ops-cli.ts +20 -0
  214. package/src/cli/pod-cli.ts +49 -0
  215. package/src/cli/pr-cli.ts +37 -4
  216. package/src/cli/pwa-stop.ts +163 -13
  217. package/src/cli/pwa-test.ts +3 -0
  218. package/src/cli/release-cli.ts +98 -15
  219. package/src/cli/self-update.ts +14 -0
  220. package/src/cli/storyboard-cli.ts +37 -0
  221. package/src/cli/test-state-dir-flag.ts +11 -4
  222. package/src/connectors/linear.ts +146 -0
  223. package/src/context-bus/coord-events.ts +115 -0
  224. package/src/dashboard/dashboard-boot-fixture.ts +23 -0
  225. package/src/dashboard/dashboard-status-lines.ts +45 -0
  226. package/src/dashboard/first-screen-band.ts +35 -0
  227. package/src/dashboard/index.ts +128 -34
  228. package/src/dashboard/provider-slash-runtime.ts +28 -11
  229. package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
  230. package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
  231. package/src/dashboard/turn-stream-formatter.ts +1 -1
  232. package/src/decisions/decision-cards.ts +50 -2
  233. package/src/decisions/decision-ledger.ts +75 -4
  234. package/src/decisions/decision-linear-projection.ts +157 -0
  235. package/src/decisions/web-push-decision-cards.ts +50 -0
  236. package/src/directive/interpretation-card.ts +36 -0
  237. package/src/discord.ts +2 -1
  238. package/src/domains/coo-admin-tool.ts +62 -0
  239. package/src/domains/core-tools.ts +11 -3
  240. package/src/domains/elanous-card.ts +29 -0
  241. package/src/domains/ops-facts-tool.ts +76 -0
  242. package/src/domains/outbound-alert.ts +41 -13
  243. package/src/domains/release-tool.ts +168 -0
  244. package/src/domains/telegram-kind-route.ts +19 -1
  245. package/src/exec-requests/planner.ts +98 -28
  246. package/src/exec-requests/runner.ts +107 -10
  247. package/src/exec-requests/store.ts +8 -2
  248. package/src/field/field-media.ts +10 -7
  249. package/src/field/field-reel.ts +14 -4
  250. package/src/git-fs/worktree.ts +45 -6
  251. package/src/graph-runner/graph-approval-card.ts +113 -0
  252. package/src/graph-runner/graph-cli.ts +129 -4
  253. package/src/graph-runner/graph-notify.ts +25 -3
  254. package/src/graph-runner/runner.ts +142 -16
  255. package/src/harness/browser-verify.ts +25 -2
  256. package/src/harness/harness-cli-command.ts +231 -35
  257. package/src/harness/harness-incidents-writer.fixture.ts +24 -0
  258. package/src/harness/harness-incidents.ts +129 -0
  259. package/src/harness/harness-stop.ts +53 -3
  260. package/src/hitl/clarification-policy.ts +3 -1
  261. package/src/index.ts +172 -41
  262. package/src/intake-plane/author-goals.ts +37 -4
  263. package/src/intake-plane/check.ts +3 -0
  264. package/src/intake-plane/discord-seat-work.ts +47 -2
  265. package/src/intake-plane/seat-doc-route.ts +170 -6
  266. package/src/intake-plane/telegram-seat-work.ts +44 -2
  267. package/src/intake-plane/telegram-work.ts +1 -0
  268. package/src/maturity/cli-maturity.ts +34 -0
  269. package/src/maturity/feature-maturity.ts +100 -1
  270. package/src/maturity/tui-slash-maturity.ts +31 -0
  271. package/src/memory.ts +32 -4
  272. package/src/nexus/api/exec-requests.ts +33 -3
  273. package/src/nexus/api/http-server.ts +73 -1
  274. package/src/nexus/api/meta-api.ts +38 -0
  275. package/src/nexus/api/operator.ts +61 -0
  276. package/src/nexus/api/ops-api.ts +148 -0
  277. package/src/nexus/api/ops-seats-sources.ts +95 -0
  278. package/src/nexus/api/ops-seats.ts +124 -0
  279. package/src/nexus/api/public-routes.ts +14 -1
  280. package/src/nexus/api/seat-requests.ts +88 -8
  281. package/src/nexus/api/sessions-store.ts +63 -0
  282. package/src/nexus/api/setup-claim.ts +21 -0
  283. package/src/nexus/api/setup-codex-login.ts +160 -0
  284. package/src/nexus/api/usage-api.ts +102 -0
  285. package/src/nexus/config/secrets/file-backend.ts +56 -18
  286. package/src/nexus/index.ts +45 -4
  287. package/src/onboarding/first-run-doctor.ts +54 -0
  288. package/src/onboarding/first-run.ts +89 -0
  289. package/src/onboarding/lan-address.ts +45 -0
  290. package/src/onboarding/web-first.ts +119 -24
  291. package/src/onboarding.ts +4 -0
  292. package/src/ops-now/ops-now-refresh.ts +100 -0
  293. package/src/project/project-store.ts +115 -0
  294. package/src/release-loop/checklist.ts +76 -61
  295. package/src/release-loop/feature-store.ts +460 -0
  296. package/src/release-loop/manifest.ts +59 -5
  297. package/src/release-loop/release-schedule.ts +53 -0
  298. package/src/repl/index.ts +7 -0
  299. package/src/seat-dispatch/ceo-commands.ts +98 -0
  300. package/src/seat-dispatch/ceo-intent.ts +38 -0
  301. package/src/seat-loop/seat-loop.ts +231 -0
  302. package/src/seat-loop/seat-report.ts +64 -0
  303. package/src/self-dev/decompose.ts +66 -11
  304. package/src/self-dev/run-store.ts +10 -1
  305. package/src/self-implement/execution-clarification.ts +115 -0
  306. package/src/self-implement/gate-cli.ts +6 -3
  307. package/src/self-implement/orchestrator.ts +37 -3
  308. package/src/self-implement/pipeline-shape.ts +1 -1
  309. package/src/self-implement/run-status-mapping.ts +5 -3
  310. package/src/session/chat.ts +2 -0
  311. package/src/session/index.ts +9 -0
  312. package/src/shell-env-bootstrap.ts +17 -0
  313. package/src/skills/tools/usage-slash.ts +8 -7
  314. package/src/steward/directive.ts +51 -2
  315. package/src/steward/launch.ts +250 -0
  316. package/src/steward/outcome-notify.ts +115 -0
  317. package/src/steward/steward-cards.ts +8 -2
  318. package/src/steward/triage-plan.ts +32 -4
  319. package/src/steward/triage.ts +93 -9
  320. package/src/steward/working-backwards.ts +64 -0
  321. package/src/storyboard/storyboard.ts +141 -0
  322. package/src/task-cards/card-store.ts +2 -2
  323. package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
  324. package/src/telegram-card-followup.ts +233 -0
  325. package/src/telegram-commands.ts +16 -0
  326. package/src/telegram-report.ts +15 -9
  327. package/src/telegram-run.ts +90 -13
  328. package/src/telegram.ts +29 -8
  329. package/src/thinking-line.ts +28 -20
  330. package/src/tool-runtime/types.ts +4 -0
  331. package/src/tui/chat/execution-badge.ts +1 -1
  332. package/src/tui-client/dashboard-session.ts +2 -0
  333. package/src/user-config.ts +118 -11
  334. package/src/ux-sim/turn-states.ts +1 -1
  335. package/src/version/packed-revision.json +1 -1
  336. package/src/web-push/notify-exec-request.ts +32 -0
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
  382. package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
  383. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
  384. package/src/dashboard/turn-typeahead-echo.ts +0 -23
  385. /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
@@ -0,0 +1,260 @@
1
+ import type { PodPoolMember, PoolKubectl } from './pod-pool.js';
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ // `get pods --all-namespaces -o json` on a busy cluster is several MB — the default spawnSync
5
+ // buffer fails with ENOBUFS (10-03 node-b measured). Same proxy stripping as the Pod launch kubectl.
6
+ const LEASE_KUBECTL_MAX_BUFFER = 256 * 1024 * 1024;
7
+ export function leaseKubectl(args: readonly string[]): { status: number | null; stdout: string; stderr: string } {
8
+ const env = { ...process.env };
9
+ for (const k of ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy']) delete env[k];
10
+ const r = spawnSync('kubectl', [...args], { encoding: 'utf8', env, timeout: 120_000, maxBuffer: LEASE_KUBECTL_MAX_BUFFER });
11
+ return { status: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error ? String(r.error) : '') };
12
+ }
13
+
14
+ export interface PodLeaseMember {
15
+ context: string;
16
+ capacity: number;
17
+ running: number | null;
18
+ pending: number | null;
19
+ memoryLimitBytes: number | null;
20
+ /** All-node allocatable memory, before existing reservations. */
21
+ allocatableMemoryBytes: number | null;
22
+ allocatableCpuMillicores: number | null;
23
+ /** Node-by-node memory left after all assigned Pod requests and harness Running limits. */
24
+ availableMemoryByNodeBytes: number[] | null;
25
+ reason: string | null;
26
+ }
27
+
28
+ export interface PoolLeaseMeasure {
29
+ members: PodLeaseMember[];
30
+ }
31
+
32
+ export interface PoolLeaseRecommendation {
33
+ recommended: number | null;
34
+ limitedBy: 'capacity' | 'memory' | null;
35
+ reason: string | null;
36
+ capacitySlots: number | null;
37
+ memorySlots: number | null;
38
+ placeableSlots: number | null;
39
+ accountSlots: number | null;
40
+ running: number | null;
41
+ pending: number | null;
42
+ }
43
+
44
+ /** Kubernetes quantity to bytes (memory) or millicores (CPU). */
45
+ function quantity(raw: string, unit: 'memory' | 'cpu'): number | null {
46
+ const m = /^(\d+(?:\.\d+)?)(n|u|m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$/.exec(raw);
47
+ if (!m) return null;
48
+ const scale: Record<string, number> = { n: 1e-9, u: 1e-6, m: 1e-3, k: 1e3, M: 1e6, G: 1e9, T: 1e12, P: 1e15, E: 1e18, Ki: 1024, Mi: 1024 ** 2, Gi: 1024 ** 3, Ti: 1024 ** 4, Pi: 1024 ** 5, Ei: 1024 ** 6 };
49
+ const suffix = m[2] ?? '';
50
+ if (unit === 'memory' && ['n', 'u', 'm'].includes(suffix)) return null;
51
+ if (unit === 'cpu' && suffix.endsWith('i')) return null;
52
+ const result = Number(m[1]) * (scale[suffix] ?? 1) * (unit === 'cpu' ? 1000 : 1);
53
+ return Number.isFinite(result) && result >= 0 ? result : null;
54
+ }
55
+
56
+ function items(value: unknown): Record<string, unknown>[] | null {
57
+ if (!value || typeof value !== 'object' || !Array.isArray((value as { items?: unknown }).items)) return null;
58
+ const list = (value as { items: unknown[] }).items;
59
+ return list.every((v) => v !== null && typeof v === 'object' && !Array.isArray(v)) ? list as Record<string, unknown>[] : null;
60
+ }
61
+
62
+ function object(value: unknown): Record<string, unknown> | null {
63
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
64
+ }
65
+
66
+ /** Effective scheduling reservation: max(sum of app requests, largest init request, Pod-level request) + overhead. */
67
+ function requestedMemory(spec: Record<string, unknown>): number | null {
68
+ const request = (resources: unknown): number | null => {
69
+ const r = object(resources);
70
+ if (resources !== undefined && !r) return null;
71
+ const requests = object(r?.requests);
72
+ const limits = object(r?.limits);
73
+ if ((r?.requests !== undefined && !requests) || (r?.limits !== undefined && !limits)) return null;
74
+ const raw = requests?.memory ?? limits?.memory;
75
+ return raw === undefined ? 0 : typeof raw === 'string' ? quantity(raw, 'memory') : null;
76
+ };
77
+ const containers = spec.containers;
78
+ if (!Array.isArray(containers) || !containers.length) return null;
79
+ let app = 0;
80
+ for (const c of containers) {
81
+ if (!object(c)) return null;
82
+ const v = request(object(c)?.resources);
83
+ if (v === null) return null;
84
+ app += v;
85
+ }
86
+ let init = 0;
87
+ if (spec.initContainers !== undefined && !Array.isArray(spec.initContainers)) return null;
88
+ for (const c of (spec.initContainers ?? []) as unknown[]) {
89
+ if (!object(c)) return null;
90
+ const v = request(object(c)?.resources);
91
+ if (v === null) return null;
92
+ init = Math.max(init, v);
93
+ }
94
+ const pod = spec.resources === undefined ? 0 : request(spec.resources);
95
+ const overheadRaw = object(spec.overhead)?.memory;
96
+ const overhead = spec.overhead !== undefined && !object(spec.overhead) ? null
97
+ : overheadRaw === undefined ? 0 : typeof overheadRaw === 'string' ? quantity(overheadRaw, 'memory') : null;
98
+ return pod === null || overhead === null ? null : Math.max(app, init, pod) + overhead;
99
+ }
100
+
101
+ function runningLimit(spec: Record<string, unknown>): number | null {
102
+ if (!Array.isArray(spec.containers) || !spec.containers.length) return null;
103
+ let total = 0;
104
+ for (const c of spec.containers) {
105
+ const raw = object(object(object(c)?.resources)?.limits)?.memory;
106
+ const value = typeof raw === 'string' ? quantity(raw, 'memory') : null;
107
+ if (value === null) return null;
108
+ total += value;
109
+ }
110
+ return total;
111
+ }
112
+
113
+ /** Read only: each cluster probe is kubectl get. Missing reservations cannot become zero. */
114
+ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl } = {}): PoolLeaseMeasure {
115
+ const kubectl = deps.kubectl ?? leaseKubectl;
116
+ return { members: members.map((member) => {
117
+ const base = ['--context', member.context, '--request-timeout=10s'];
118
+ const result: PodLeaseMember = { context: member.context, capacity: member.capacity, running: null, pending: null, memoryLimitBytes: null, allocatableMemoryBytes: null, allocatableCpuMillicores: null, availableMemoryByNodeBytes: null, reason: null };
119
+ try {
120
+ const nodes = kubectl([...base, 'get', 'nodes', '-o', 'json']);
121
+ const jobs = kubectl([...base, '-n', 'elanous-test', 'get', 'jobs', '-l', 'elanous.substrate=pod', '-o', 'json']);
122
+ // All namespaces are necessary: unrelated workloads also reserve memory on these nodes.
123
+ const pods = kubectl([...base, 'get', 'pods', '--all-namespaces', '-o', 'json']);
124
+ const reasons: string[] = [];
125
+ const parsedItems = (response: ReturnType<PoolKubectl>): Record<string, unknown>[] | null => {
126
+ if (response.status !== 0) return null;
127
+ try { return items(JSON.parse(response.stdout)); } catch { return null; }
128
+ };
129
+ const nodeItems = parsedItems(nodes);
130
+ const nodeFree = new Map<string, number>();
131
+ const nodeNames = new Set<string>();
132
+ if (!nodeItems?.length) reasons.push(`cluster nodes: ${nodes.status === 0 ? 'invalid/empty response' : nodes.stderr.trim().split('\n').pop() || `rc=${nodes.status}`}`);
133
+ else {
134
+ let memory = 0, cpu = 0, valid = true;
135
+ for (const node of nodeItems) {
136
+ const name = object(node.metadata)?.name;
137
+ const status = object(node.status);
138
+ const allocatable = object(status?.allocatable);
139
+ const memRaw = allocatable?.memory, cpuRaw = allocatable?.cpu;
140
+ const mem = typeof memRaw === 'string' ? quantity(memRaw, 'memory') : null;
141
+ const cores = typeof cpuRaw === 'string' ? quantity(cpuRaw, 'cpu') : null;
142
+ const conditions = status?.conditions;
143
+ const ready = Array.isArray(conditions) ? conditions.filter((c) => object(c)?.type === 'Ready') : [];
144
+ const spec = object(node.spec);
145
+ // Harness Pod Jobs declare no tolerations, so a NoSchedule/NoExecute taint keeps them off that node.
146
+ const taints = spec?.taints;
147
+ const taintList = taints === undefined ? [] : Array.isArray(taints) ? taints.map(object) : null;
148
+ if (typeof name !== 'string' || !name || nodeNames.has(name) || mem === null || cores === null ||
149
+ ready.length !== 1 || !['True', 'False', 'Unknown'].includes(String(object(ready[0])?.status)) ||
150
+ (node.spec !== undefined && !spec) || (spec?.unschedulable !== undefined && typeof spec.unschedulable !== 'boolean') ||
151
+ taintList === null || taintList.some((t) => !t || typeof t.effect !== 'string')) {
152
+ valid = false; break;
153
+ }
154
+ nodeNames.add(name);
155
+ const repels = taintList.some((t) => t!.effect === 'NoSchedule' || t!.effect === 'NoExecute');
156
+ if (spec?.unschedulable !== true && !repels && object(ready[0])?.status === 'True') nodeFree.set(name, mem);
157
+ memory += mem; cpu += cores;
158
+ }
159
+ if (valid) { result.allocatableMemoryBytes = memory; result.allocatableCpuMillicores = cpu; }
160
+ else { nodeFree.clear(); nodeNames.clear(); reasons.push('cluster nodes: invalid name/allocatable memory/cpu/readiness'); }
161
+ }
162
+ const jobItems = parsedItems(jobs);
163
+ const podItems = parsedItems(pods);
164
+ if (!jobItems) reasons.push(`cluster jobs: ${jobs.status === 0 ? 'invalid response' : jobs.stderr.trim().split('\n').pop() || `rc=${jobs.status}`}`);
165
+ if (!podItems) reasons.push(`cluster pods: ${pods.status === 0 ? 'invalid response' : pods.stderr.trim().split('\n').pop() || `rc=${pods.status}`}`);
166
+ if (podItems && jobItems) {
167
+ const jobNames = new Set<string>();
168
+ let jobsValid = true;
169
+ for (const job of jobItems) {
170
+ const meta = object(job.metadata);
171
+ if (!meta || !object(meta.labels)) { jobsValid = false; continue; }
172
+ if (object(meta.labels)?.['elanous.substrate'] !== 'pod') continue;
173
+ if (typeof meta.name !== 'string' || !meta.name) { jobsValid = false; continue; }
174
+ jobNames.add(meta.name);
175
+ }
176
+ if (!jobsValid) reasons.push('cluster jobs: missing name/labels');
177
+ let running = 0, pending = 0, limits = 0, limitsValid = true, reservationsValid = nodeItems !== null && nodeNames.size === nodeItems.length;
178
+ let unknownPhase = false;
179
+ const seenPods = new Set<string>();
180
+ for (const pod of podItems) {
181
+ const metadata = object(pod.metadata);
182
+ const labels = object(metadata?.labels);
183
+ const phase = object(pod.status)?.phase;
184
+ if (phase === 'Succeeded' || phase === 'Failed') continue;
185
+ if (phase !== 'Running' && phase !== 'Pending') { unknownPhase = true; reservationsValid = false; continue; }
186
+ if (typeof metadata?.namespace !== 'string') reservationsValid = false;
187
+ const identity = typeof metadata?.namespace === 'string' && typeof metadata.name === 'string' && metadata.name
188
+ ? `name:${metadata.namespace}/${metadata.name}` : null;
189
+ if (identity) {
190
+ if (seenPods.has(identity)) { reservationsValid = false; continue; }
191
+ seenPods.add(identity);
192
+ } else reservationsValid = false;
193
+ const harness = metadata?.namespace === 'elanous-test' &&
194
+ (labels?.['elanous.substrate'] === 'pod' || jobNames.has(String(labels?.['elanous.job'] ?? '')));
195
+ const spec = object(pod.spec);
196
+ let limit: number | null = null;
197
+ if (harness) {
198
+ if (phase === 'Pending') pending++;
199
+ else {
200
+ running++;
201
+ limit = spec ? runningLimit(spec) : null;
202
+ if (limit === null) limitsValid = false;
203
+ else limits += limit;
204
+ }
205
+ }
206
+ // Pending Pods with no assigned node have no node reservation yet. They already occupy a pool slot.
207
+ const nodeName = spec?.nodeName;
208
+ if ((nodeName === undefined || nodeName === '') && phase === 'Pending' && spec && typeof metadata?.namespace === 'string' &&
209
+ requestedMemory(spec) !== null) continue;
210
+ const requested = spec ? requestedMemory(spec) : null;
211
+ if (typeof nodeName !== 'string' || !nodeNames.has(nodeName) || requested === null) { reservationsValid = false; continue; }
212
+ if (nodeFree.has(nodeName)) nodeFree.set(nodeName, nodeFree.get(nodeName)! - Math.max(requested, limit ?? 0));
213
+ }
214
+ if (unknownPhase) reasons.push('cluster pods: missing/Unknown phase');
215
+ if (jobsValid && !unknownPhase) { result.running = running; result.pending = pending; }
216
+ if (limitsValid && jobsValid && !unknownPhase) result.memoryLimitBytes = limits;
217
+ else if (!limitsValid) reasons.push('cluster pods: Running Pod memory limit missing/invalid');
218
+ if (reservationsValid && limitsValid && jobsValid && !unknownPhase) result.availableMemoryByNodeBytes = [...nodeFree.values()];
219
+ else if (!reservationsValid && !unknownPhase) reasons.push('cluster reservations: node assignment or memory request/limit missing/invalid');
220
+ }
221
+ result.reason = reasons.length ? reasons.join('; ').slice(0, 240) : null;
222
+ } catch (error) {
223
+ result.running = null; result.pending = null; result.memoryLimitBytes = null;
224
+ result.allocatableMemoryBytes = null; result.allocatableCpuMillicores = null; result.availableMemoryByNodeBytes = null;
225
+ result.reason = `cluster: ${error instanceof Error ? error.message : String(error)}`.split('\n')[0]!.slice(0, 240);
226
+ }
227
+ return result;
228
+ }) };
229
+ }
230
+
231
+ /** Per-node memory and per-member slots cannot be exchanged across nodes/clusters. */
232
+ export function recommendConcurrency(measure: PoolLeaseMeasure, options: { capacity: number; perGoalMemory?: string; accounts: number; perAccount: number }): PoolLeaseRecommendation {
233
+ const empty: PoolLeaseRecommendation = { recommended: null, limitedBy: null, reason: null, capacitySlots: null, memorySlots: null, placeableSlots: null, accountSlots: null, running: null, pending: null };
234
+ const goalBytes = quantity(options.perGoalMemory ?? '16Gi', 'memory');
235
+ if (goalBytes === null || goalBytes <= 0) return { ...empty, reason: '측정 불가: perGoalMemory' };
236
+ if (!measure.members.length || measure.members.some((m) => m.running === null || m.pending === null || m.allocatableMemoryBytes === null || m.memoryLimitBytes === null || m.allocatableCpuMillicores === null || m.availableMemoryByNodeBytes === null)) {
237
+ return { ...empty, reason: `측정 불가: cluster${measure.members.map((m) => m.reason ? ` ${m.context}: ${m.reason}` : '').join('')}` };
238
+ }
239
+ if (!Number.isSafeInteger(options.capacity) || options.capacity < 0) return { ...empty, reason: '측정 불가: capacity' };
240
+ const running = measure.members.reduce((sum, m) => sum + m.running!, 0);
241
+ const pending = measure.members.reduce((sum, m) => sum + m.pending!, 0);
242
+ const capacitySlots = Math.max(0, Math.min(options.capacity - running - pending,
243
+ measure.members.reduce((sum, m) => sum + Math.max(0, m.capacity - m.running! - m.pending!), 0)));
244
+ const memberMemorySlots = measure.members.map((m) => m.availableMemoryByNodeBytes!.reduce(
245
+ (sum, bytes) => sum + Math.max(0, Math.floor(bytes / goalBytes)), 0));
246
+ // Preserve the requested allocatable-minus-harness-limit metric. The node-local
247
+ // placement bound additionally accounts for every namespace's reservations.
248
+ const memorySlots = measure.members.reduce((sum, m) => sum + Math.max(0,
249
+ Math.floor((m.allocatableMemoryBytes! - m.memoryLimitBytes!) / goalBytes)), 0);
250
+ const placeableSlots = measure.members.reduce((sum, m, i) => sum + Math.min(
251
+ Math.max(0, m.capacity - m.running! - m.pending!), memberMemorySlots[i]!,
252
+ ), 0);
253
+ // Account count is not a concurrency limit (same IP · own accounts, 10-03 decision) — observed only, never a bound.
254
+ const accountSlots = [options.accounts, options.perAccount].every((n) => Number.isSafeInteger(n) && n >= 0)
255
+ ? Math.max(0, options.accounts * options.perAccount - running) : null;
256
+ const memoryBound = Math.min(memorySlots, placeableSlots);
257
+ const recommended = Math.min(capacitySlots, memoryBound);
258
+ const limitedBy = recommended === memoryBound && memoryBound < capacitySlots ? 'memory' : 'capacity';
259
+ return { recommended, limitedBy, reason: null, capacitySlots, memorySlots, placeableSlots, accountSlots, running, pending };
260
+ }
@@ -0,0 +1,233 @@
1
+ import { chmodSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { join, resolve } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { debug } from './debug/log.js';
6
+ import { effectiveInstanceRoot } from './instance/resolve.js';
7
+ import { lastJsonObject, runGraph, type GraphRunState } from './graph-runner/runner.js';
8
+ import type { TgIncoming } from './telegram.js';
9
+
10
+ const GRAPH = resolve(import.meta.dir, '../plugins/card-followup/graphs/card-followup.yaml');
11
+ // Latin keywords are whole words only — «postcard» or «cardinal» is not a card request (review round 3).
12
+ const KEYWORD = /명함|전략|메일\s*초안|초안|팔로업|\bcard\b|\bCRM\b/i;
13
+ /** Untagged albums start one graph run, not one per photo (chatId:mediaGroupId → first seen at). */
14
+ const startedAlbums = new Map<string, number>();
15
+ const ALBUM_TTL_MS = 10 * 60_000;
16
+ function firstOfAlbum(chatId: number, mediaGroupId: string | undefined, now: number): boolean {
17
+ if (!mediaGroupId) return true;
18
+ for (const [key, at] of startedAlbums) if (now - at > ALBUM_TTL_MS) startedAlbums.delete(key);
19
+ const key = `${chatId}:${mediaGroupId}`;
20
+ if (startedAlbums.has(key)) return false;
21
+ startedAlbums.set(key, now);
22
+ return true;
23
+ }
24
+
25
+ // Captionless photos start a run only when they look like a business card (OP 10-02 18:54 · cheap signals first, ambiguous → nothing).
26
+ export type CardDetectDecision = 'card' | 'skip' | 'ambiguous';
27
+ export function cardShape(width?: number, height?: number): { decision: 'skip' | 'pass'; signal: string } {
28
+ if (!width || !height) return { decision: 'pass', signal: 'ratio:unknown' };
29
+ const ratio = Math.max(width, height) / Math.min(width, height);
30
+ if (ratio < 1.15) return { decision: 'skip', signal: 'ratio:square' };
31
+ if (ratio > 2.0) return { decision: 'skip', signal: 'ratio:screenshot' };
32
+ return { decision: 'pass', signal: `ratio:${ratio.toFixed(2)}` };
33
+ }
34
+ const EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i;
35
+ const PHONE = /(?:\+?\d{1,3}[\s.-]?)?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{4}/;
36
+ const WEB = /\b(?:https?:\/\/|www\.)\S+|\b[a-z0-9-]+\.(?:com|co\.kr|kr|io|ai|net|org)\b/i;
37
+ const TITLE = /대표|이사|팀장|부장|과장|실장|매니저|주식회사|\(주\)|\b(?:CEO|CTO|COO|CFO|Director|Manager|Founder|Inc|Ltd|Corp)\b/i;
38
+ /** Card text is short and carries contact patterns; long text is a document. Returns signal names only, never the text. */
39
+ export function cardText(ocr: string | null): { decision: CardDetectDecision; signal: string } {
40
+ if (ocr === null) return { decision: 'ambiguous', signal: 'ocr:unavailable' };
41
+ const chars = ocr.replace(/\s+/g, '').length;
42
+ if (chars < 15) return { decision: 'ambiguous', signal: `ocr:chars<15` };
43
+ if (chars > 700) return { decision: 'skip', signal: 'ocr:document' };
44
+ const hits = ([['email', EMAIL], ['phone', PHONE], ['web', WEB], ['title', TITLE]] as const).filter(([, re]) => re.test(ocr)).map(([name]) => name);
45
+ const signal = `ocr:${hits.join(',') || 'none'}`;
46
+ if (hits.length >= 2) return { decision: 'card', signal };
47
+ return { decision: hits.length === 0 ? 'skip' : 'ambiguous', signal };
48
+ }
49
+ const OCR_SCRIPT = resolve(import.meta.dir, '../scripts/ocr-text-regions.swift');
50
+ /** Local macOS Vision OCR (no network · no new dependency); null where it cannot run. */
51
+ export function localOcrText(path: string, timeoutMs = 30_000): Promise<string | null> {
52
+ if (process.platform !== 'darwin') return Promise.resolve(null);
53
+ return new Promise((done) => {
54
+ let out = '';
55
+ let child: ReturnType<typeof spawn>;
56
+ try { child = spawn('swift', [OCR_SCRIPT, path], { stdio: ['ignore', 'pipe', 'ignore'] }); } catch { done(null); return; }
57
+ const timer = setTimeout(() => { child.kill('SIGKILL'); done(null); }, timeoutMs);
58
+ child.stdout?.on('data', (chunk) => { out += String(chunk); });
59
+ child.once('error', () => { clearTimeout(timer); done(null); });
60
+ child.once('close', (code) => {
61
+ clearTimeout(timer);
62
+ if (code !== 0) { done(null); return; }
63
+ done(out.split('\n').map((l) => /^\s+[\d.]+\s+(.*?)\s+box=/.exec(l)?.[1]).filter(Boolean).join('\n'));
64
+ });
65
+ });
66
+ }
67
+
68
+ type Picked = 'all' | 'strategy' | 'draft' | 'crm';
69
+ export interface CardPhotoDeps {
70
+ isOwner: (userId: number) => boolean;
71
+ isFieldAlbum?: (id: string) => boolean;
72
+ downloadFile: (fileId: string, destDir: string) => Promise<{ localPath: string }>;
73
+ sendMessage: (chatId: number, text: string, opts: { replyTo: number; threadId?: number }) => Promise<unknown>;
74
+ sendDocument: (chatId: number, body: string | Buffer, filename: string, opts: { replyTo: number; threadId?: number; mimeType?: string }) => Promise<unknown>;
75
+ runGraph?: typeof runGraph;
76
+ rootDir?: () => string;
77
+ /** OCR text of a downloaded photo, or null when unavailable (default: local macOS Vision). */
78
+ ocrText?: (path: string) => Promise<string | null>;
79
+ }
80
+
81
+ const obj = (value: unknown): Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
82
+ const text = (value: unknown): string => typeof value === 'string' ? value.trim() : '';
83
+ const line = (value: unknown): string => (typeof value === 'number' && Number.isFinite(value) ? String(value) : text(value)).replace(/[\r\n]+/g, ' ') || '—';
84
+
85
+ function isNotACard(state: GraphRunState): boolean {
86
+ if (state.status !== 'failed') return false;
87
+ const read = state.nodes.find((node) => node.nodeId === 'read-card');
88
+ if (!read || !read.ok) return false;
89
+ const output = lastJsonObject(read.output);
90
+ if (output?.outcome === 'ok' && output.card && typeof output.card === 'object') {
91
+ const card = obj(output.card);
92
+ return !['name', 'company', 'title', 'email', 'phone', 'url', 'linkedin'].some((field) => text(card[field]));
93
+ }
94
+ return output?.outcome === 'fail' && typeof output.reason === 'string'
95
+ && /\bnot a (?:business )?card\b|명함이?\s*아님|명함이?\s*아닙니다/i.test(output.reason);
96
+ }
97
+
98
+ function validateReport(report: Record<string, unknown>): void {
99
+ const card = obj(report.card), fit = obj(report.fit), approach = obj(report.approach);
100
+ const next = obj(report.nextAction), draft = obj(report.draft);
101
+ const score = fit.score;
102
+ if (report.sent !== false || !text(card.name) || !text(card.company)
103
+ || !(score === null || (typeof score === 'number' && Number.isFinite(score)))
104
+ || !text(fit.label) || !text(approach.problem) || !text(approach.proposal)
105
+ || !text(approach.channel) || !text(next.what) || !text(next.due)
106
+ || !text(draft.subject) || !text(draft.body)) {
107
+ throw new Error('incomplete or sent card report');
108
+ }
109
+ }
110
+
111
+ function pick(caption: string): Picked {
112
+ if (/\bCRM\b/i.test(caption)) return 'crm';
113
+ if (/메일\s*초안|초안/i.test(caption)) return 'draft';
114
+ if (/전략/.test(caption)) return 'strategy';
115
+ return 'all';
116
+ }
117
+
118
+ /** Starts a private graph run without blocking the Telegram message turn. */
119
+ export async function maybeHandleCardPhoto(ctx: TgIncoming, deps: CardPhotoDeps): Promise<boolean> {
120
+ const photo = ctx.attachments.find((att) => att.kind === 'photo');
121
+ const caption = ctx.text.trim();
122
+ if (!ctx.isDm || ctx.isGroup || !deps.isOwner(ctx.userId) || !photo
123
+ || (ctx.mediaGroupId && deps.isFieldAlbum?.(ctx.mediaGroupId))
124
+ || /#현장/.test(caption) || (caption && !KEYWORD.test(caption))) return false;
125
+ const detect = (decision: CardDetectDecision, signal: string) =>
126
+ debug.log('telegram.card-followup', 'detect', { chatId: ctx.chatId, decision, signal });
127
+ if (!caption) {
128
+ const shape = cardShape(photo.width, photo.height);
129
+ if (shape.decision === 'skip') { detect('skip', shape.signal); return false; }
130
+ }
131
+ // Album items after the first: a keyword caption sits on one item only, so later items follow the normal flow.
132
+ if (!firstOfAlbum(ctx.chatId, ctx.mediaGroupId, Date.now())) return false;
133
+
134
+ const picked = pick(caption);
135
+ const started = Date.now();
136
+ const reply = { replyTo: ctx.messageId, ...(ctx.threadId === undefined ? {} : { threadId: ctx.threadId }) };
137
+ const log = (event: 'started' | 'not-a-card' | 'replied' | 'failed') =>
138
+ debug.log('card.followup', event, { chatId: ctx.chatId, picked, ms: Date.now() - started });
139
+
140
+ try {
141
+ const root = (deps.rootDir ?? effectiveInstanceRoot)();
142
+ const base = join(root, 'graph-runs', 'card-followup');
143
+ const runId = randomUUID();
144
+ const dir = join(base, runId);
145
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
146
+ chmodSync(base, 0o700);
147
+ chmodSync(dir, 0o700);
148
+ const prepare = async (): Promise<string | null> => {
149
+ const { localPath } = await deps.downloadFile(photo.fileId, dir);
150
+ chmodSync(localPath, 0o600);
151
+ if (!caption) {
152
+ const verdict = cardText(await (deps.ocrText ?? localOcrText)(localPath));
153
+ detect(verdict.decision, verdict.signal);
154
+ if (verdict.decision !== 'card') { rmSync(dir, { recursive: true, force: true }); return null; }
155
+ }
156
+ log('started');
157
+ await deps.sendMessage(ctx.chatId, '명함으로 보고 정리하는 중입니다(1~3분)', reply);
158
+ return localPath;
159
+ };
160
+ // A keyword caption is an explicit request: download before returning. A bare photo is judged in the background.
161
+ const prepared = caption ? await prepare() : null;
162
+ void (async () => {
163
+ let localPath = prepared;
164
+ try {
165
+ if (!caption) {
166
+ try { localPath = await prepare(); } catch { log('failed'); return; }
167
+ if (!localPath) return;
168
+ }
169
+ const state = await (deps.runGraph ?? runGraph)(GRAPH, { runId, deps: { root }, input: {
170
+ image: localPath!, outDir: dir, ...(caption ? { context: caption } : {}),
171
+ } });
172
+ if (!deps.runGraph) {
173
+ const statePath = join(base, `${runId}.json`);
174
+ chmodSync(statePath, 0o600);
175
+ const contexts = `${statePath}.contexts`;
176
+ try {
177
+ chmodSync(contexts, 0o700);
178
+ for (const file of readdirSync(contexts)) chmodSync(join(contexts, file), 0o600);
179
+ } catch (error) {
180
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
181
+ }
182
+ }
183
+ if (isNotACard(state)) {
184
+ log('not-a-card');
185
+ return;
186
+ }
187
+ if (state.status !== 'done') throw new Error('graph did not finish');
188
+ const report = obj(JSON.parse(readFileSync(join(dir, 'followup.json'), 'utf8')));
189
+ validateReport(report);
190
+ const card = obj(report.card), fit = obj(report.fit), approach = obj(report.approach);
191
+ const next = obj(report.nextAction), draft = obj(report.draft);
192
+ const summary = `① 요약\n${line(card.name)} · ${line(card.company)} · ${line(card.title)}\n타겟 판정: ${line(fit.score)} · ${line(fit.label)}`;
193
+ const strategy = `② 전략\n${[approach.problem, approach.proposal, approach.channel].map(line).join('\n')}\n다음 행동: ${line(next.what)} · ${line(next.due)}`;
194
+ const mail = `③ 팔로업 초안 (보내지 않았습니다)\n제목: ${line(draft.subject)}\n${text(draft.body)}`;
195
+ const markdown = readFileSync(join(dir, 'followup.md'));
196
+ const crmLine = picked === 'crm'
197
+ ? /^## ② CRM 한 줄\r?\n파일: [^\r\n]*\r?\n([^\r\n]+)/m.exec(markdown.toString('utf8'))?.[1]
198
+ : undefined;
199
+ if (picked === 'crm' && !crmLine) throw new Error('CRM row missing from report');
200
+ const messages = picked === 'strategy' ? [strategy] : picked === 'draft' ? [mail] : picked === 'crm' ? [`CRM 한 줄\n${crmLine}`] : [summary, strategy, mail];
201
+ for (const message of messages) await deps.sendMessage(ctx.chatId, message, reply);
202
+ const sent = await deps.sendDocument(ctx.chatId, markdown, 'followup.md', { ...reply, mimeType: 'text/markdown' });
203
+ if (sent === undefined) throw new Error('document delivery failed');
204
+ log('replied');
205
+ } catch {
206
+ log('failed');
207
+ try {
208
+ await deps.sendMessage(ctx.chatId, '명함 정리에 실패했습니다. 다시 시도해 주세요.', reply);
209
+ } catch { /* Telegram delivery itself failed; the failure is already observed. */ }
210
+ } finally {
211
+ if (!deps.runGraph) {
212
+ // The runner may throw before returning its state; seal any partial state and contexts too.
213
+ const statePath = join(base, `${runId}.json`);
214
+ try { chmodSync(statePath, 0o600); } catch { /* no state yet */ }
215
+ const contexts = `${statePath}.contexts`;
216
+ try {
217
+ chmodSync(contexts, 0o700);
218
+ for (const file of readdirSync(contexts)) chmodSync(join(contexts, file), 0o600);
219
+ } catch { /* no contexts yet */ }
220
+ }
221
+ }
222
+ })();
223
+ } catch {
224
+ log('failed');
225
+ // A bare photo was never claimed as a card — failing to prepare it stays silent.
226
+ if (caption) {
227
+ try {
228
+ await deps.sendMessage(ctx.chatId, '명함 정리에 실패했습니다. 다시 시도해 주세요.', reply);
229
+ } catch { /* Telegram delivery itself failed; the failure is already observed. */ }
230
+ }
231
+ }
232
+ return !!caption;
233
+ }
@@ -775,6 +775,22 @@ export function defaultTelegramCommands(): TgSlashCommand[] {
775
775
  return service.listOpen(String(ctx.chatId));
776
776
  },
777
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
+ })),
778
794
  {
779
795
  name: 'work',
780
796
  description: '글을 알맞은 곳(흡수·태스크·하니스)에 넣고 결과를 여기로 답합니다 — /work [absorb|tasks|graph] <글>',
@@ -11,7 +11,7 @@
11
11
  // the report channel while interactive Q&A stays on the main bot.
12
12
 
13
13
  import { TelegramBot } from './telegram.js';
14
- import { kindRouteTarget } from './domains/telegram-kind-route.js';
14
+ import { isOperationalKind, kindRouteTarget, logKindRouteFallback, mainHomeTarget } from './domains/telegram-kind-route.js';
15
15
  export { DEFAULT_KIND_ROLES, roleForKind } from './domains/telegram-kind-route.js';
16
16
  import { resolveChannelBotToken } from './channel-bot-token.js';
17
17
  import type { UserConfig } from './user-config.js';
@@ -51,18 +51,24 @@ export interface ReportTarget {
51
51
  chatId: number;
52
52
  }
53
53
 
54
- /** Resolve the send target. With a purpose `kind` that maps to a role and explicit `telegram.channels`,
55
- * the channel holding that role wins (fallback: the `default` channel). Otherwise the legacy report
56
- * channel: `botToken` falls back to the main Q&A bot token when the report channel is served by the
57
- * same bot. Returns null when nothing is configured. */
54
+ /** Explicit role channels win. Without a role channel, operational kinds use the main bot's
55
+ * home when the legacy report channel uses another bot; same-bot reports and trading kinds
56
+ * keep the legacy report destination. Returns null when no safe target is configured. */
58
57
  export function resolveReportTarget(cfg: UserConfig, kind?: string): ReportTarget | null {
59
58
  const routed = kindRouteTarget(cfg, kind);
60
59
  if (routed) return routed;
61
60
  const rc = cfg.telegram.reportChannel;
62
- if (!rc || !Number.isFinite(rc.chatId)) return null;
63
- const botToken = rc.botToken ?? resolveChannelBotToken('telegram', cfg)?.token;
64
- if (!botToken) return null;
65
- return { botToken, chatId: rc.chatId };
61
+ const mainToken = resolveChannelBotToken('telegram', cfg).token;
62
+ const sameBot = !!mainToken && !!rc && (rc.botToken ?? mainToken) === mainToken;
63
+ if (isOperationalKind(kind) && !sameBot) {
64
+ const home = mainHomeTarget(cfg);
65
+ logKindRouteFallback(kind, home ? 'main-home' : 'none', sameBot);
66
+ return home;
67
+ }
68
+ const botToken = rc?.botToken ?? mainToken;
69
+ const target = rc && Number.isFinite(rc.chatId) && botToken ? { botToken, chatId: rc.chatId } : null;
70
+ logKindRouteFallback(kind, target ? 'report-channel' : 'none', sameBot);
71
+ return target;
66
72
  }
67
73
 
68
74
  export interface SendReportOpts {