elanous 0.2.5 → 0.2.7

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 (479) hide show
  1. package/.env.example +10 -7
  2. package/README.md +9 -4
  3. package/apps/pwa/out/404/index.html +1 -1
  4. package/apps/pwa/out/404.html +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/{1751.50b96ea9c37b49b6.js → 2415.ed6428ff334c7ce2.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/{4daa2798-1363d725c79f3593.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  18. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.js → 4323.c5dfb6f503304775.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.js → 5080-1613752891fcadb4.js} +3 -3
  24. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{5348-bbf838e119661eee.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/{4440-18bb160d95819f4e.js → 6090-85a3f05f954e5697.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/{f6f3850d-7f324c6566002113.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-f89e9a303515a033.js → page-10ed24f413ed66e9.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/board/{page-4f403cd060442c95.js → page-211810b6c93ab66a.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-0ac5d053f0444d21.js → page-624f7a2f89b60c8f.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/morning/{page-bccce0babceae30f.js → page-55d50e9b0006a91d.js} +1 -1
  68. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/{webpack-2bf59c720fe9542c.js → webpack-e5bea36f183b25e6.js} +1 -1
  95. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
  96. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
  97. package/apps/pwa/out/approvals/index.html +1 -1
  98. package/apps/pwa/out/approvals/index.txt +22 -22
  99. package/apps/pwa/out/autopilot/index.html +1 -1
  100. package/apps/pwa/out/autopilot/index.txt +22 -22
  101. package/apps/pwa/out/board/index.html +1 -1
  102. package/apps/pwa/out/board/index.txt +22 -22
  103. package/apps/pwa/out/botlab/index.html +1 -1
  104. package/apps/pwa/out/botlab/index.txt +21 -21
  105. package/apps/pwa/out/bots/index.html +1 -1
  106. package/apps/pwa/out/bots/index.txt +21 -21
  107. package/apps/pwa/out/chat/index.html +1 -1
  108. package/apps/pwa/out/chat/index.txt +21 -21
  109. package/apps/pwa/out/consult/index.html +1 -0
  110. package/apps/pwa/out/consult/index.txt +32 -0
  111. package/apps/pwa/out/control/index.html +1 -1
  112. package/apps/pwa/out/control/index.txt +22 -22
  113. package/apps/pwa/out/design-check/index.html +1 -1
  114. package/apps/pwa/out/design-check/index.txt +22 -22
  115. package/apps/pwa/out/editor/index.html +1 -1
  116. package/apps/pwa/out/editor/index.txt +22 -22
  117. package/apps/pwa/out/exec/index.html +1 -0
  118. package/apps/pwa/out/exec/index.txt +32 -0
  119. package/apps/pwa/out/field/index.html +1 -0
  120. package/apps/pwa/out/field/index.txt +32 -0
  121. package/apps/pwa/out/healthz +1 -1
  122. package/apps/pwa/out/icons/elanous-16.png +0 -0
  123. package/apps/pwa/out/icons/elanous-180.png +0 -0
  124. package/apps/pwa/out/icons/elanous-192.png +0 -0
  125. package/apps/pwa/out/icons/elanous-32.png +0 -0
  126. package/apps/pwa/out/icons/elanous-512.png +0 -0
  127. package/apps/pwa/out/index.html +1 -1
  128. package/apps/pwa/out/index.txt +21 -21
  129. package/apps/pwa/out/intake/index.html +1 -1
  130. package/apps/pwa/out/intake/index.txt +22 -22
  131. package/apps/pwa/out/live/index.html +1 -1
  132. package/apps/pwa/out/live/index.txt +22 -22
  133. package/apps/pwa/out/market/index.html +1 -1
  134. package/apps/pwa/out/market/index.txt +21 -21
  135. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  136. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  137. package/apps/pwa/out/morning/index.html +1 -1
  138. package/apps/pwa/out/morning/index.txt +22 -22
  139. package/apps/pwa/out/observatory/index.html +1 -1
  140. package/apps/pwa/out/observatory/index.txt +22 -22
  141. package/apps/pwa/out/reflection/index.html +1 -1
  142. package/apps/pwa/out/reflection/index.txt +22 -22
  143. package/apps/pwa/out/scheduler/index.html +1 -1
  144. package/apps/pwa/out/scheduler/index.txt +22 -22
  145. package/apps/pwa/out/sessions/index.html +1 -1
  146. package/apps/pwa/out/sessions/index.txt +21 -21
  147. package/apps/pwa/out/settings/devices/index.html +1 -1
  148. package/apps/pwa/out/settings/devices/index.txt +22 -22
  149. package/apps/pwa/out/settings/index.html +1 -1
  150. package/apps/pwa/out/settings/index.txt +22 -22
  151. package/apps/pwa/out/setup/done/index.html +1 -1
  152. package/apps/pwa/out/setup/done/index.txt +22 -22
  153. package/apps/pwa/out/setup/index.html +1 -1
  154. package/apps/pwa/out/setup/index.txt +22 -22
  155. package/apps/pwa/out/share/index.html +1 -1
  156. package/apps/pwa/out/share/index.txt +22 -22
  157. package/apps/pwa/out/showroom/index.html +1 -1
  158. package/apps/pwa/out/showroom/index.txt +21 -21
  159. package/apps/pwa/out/tasks/index.html +1 -1
  160. package/apps/pwa/out/tasks/index.txt +22 -22
  161. package/apps/pwa/out/term/index.html +1 -1
  162. package/apps/pwa/out/term/index.txt +22 -22
  163. package/apps/pwa/out/trace/index.html +1 -1
  164. package/apps/pwa/out/trace/index.txt +22 -22
  165. package/apps/pwa/out/vault/index.html +1 -1
  166. package/apps/pwa/out/vault/index.txt +22 -22
  167. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  168. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  169. package/apps/pwa/out/workflows/index.html +1 -1
  170. package/apps/pwa/out/workflows/index.txt +22 -22
  171. package/apps/pwa/out/workspace/index.html +1 -1
  172. package/apps/pwa/out/workspace/index.txt +22 -22
  173. package/apps/pwa/out/worktrees/index.html +1 -1
  174. package/apps/pwa/out/worktrees/index.txt +22 -22
  175. package/bin/elanous.cjs +107 -0
  176. package/graphs/field/field-feed.yaml +24 -0
  177. package/graphs/field/recipes.yaml +12 -0
  178. package/graphs/landing/landing-heal.yaml +3 -1
  179. package/graphs/landing/recipes.yaml +3 -0
  180. package/graphs/release/recipes.yaml +13 -0
  181. package/graphs/release/release-loop.yaml +13 -4
  182. package/package.json +5 -5
  183. package/plugins/card-followup/README.md +42 -0
  184. package/plugins/card-followup/examples/card-sample.json +7 -0
  185. package/plugins/card-followup/graphs/card-followup.yaml +18 -0
  186. package/plugins/card-followup/graphs/recipes.yaml +16 -0
  187. package/plugins/card-followup/graphs/run-step.ts +276 -0
  188. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  189. package/plugins/card-followup/plugin.json +25 -0
  190. package/plugins/doc-draft/README.md +13 -0
  191. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  192. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  193. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  194. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  195. package/plugins/doc-draft/plugin.json +24 -0
  196. package/plugins/geo-check/README.md +15 -0
  197. package/plugins/geo-check/examples/brand-sample.json +8 -0
  198. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  199. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  200. package/plugins/geo-check/graphs/run-step.ts +198 -0
  201. package/plugins/geo-check/plugin.json +24 -0
  202. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  203. package/plugins/job-coach/README.md +31 -1
  204. package/plugins/job-coach/graphs/run-step.ts +2 -2
  205. package/plugins/job-coach/plugin.json +1 -1
  206. package/plugins/job-coach/plugin.ts +1 -1
  207. package/scripts/affinity/endcard.sh +3 -2
  208. package/scripts/affinity/reframe.sh +3 -1
  209. package/scripts/ci-public-export-leak-gate.ts +48 -0
  210. package/scripts/codex-quota-alert.ts +122 -258
  211. package/scripts/coord-tracks.json +4 -4
  212. package/scripts/daemon-port-baseline.txt +1 -2
  213. package/scripts/field-feed/deliver-node.ts +52 -0
  214. package/scripts/field-feed/feed-node.ts +67 -0
  215. package/scripts/field-feed/lib.ts +162 -0
  216. package/scripts/field-feed/reel-node.ts +24 -0
  217. package/scripts/field-feed/start.ts +62 -0
  218. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  219. package/scripts/install.sh +92 -13
  220. package/scripts/landing-heal/clean-checkout.ts +114 -0
  221. package/scripts/landing-heal/run-mission.ts +38 -3
  222. package/scripts/lib/android-test-device.ts +32 -0
  223. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  224. package/scripts/public-export.ts +8 -4
  225. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  226. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  227. package/scripts/release-loop/env-known-failures.ts +31 -0
  228. package/scripts/release-loop/gate-diff.ts +29 -0
  229. package/scripts/release-loop/gate-node.ts +124 -29
  230. package/scripts/release-loop/known-issues-node.ts +159 -0
  231. package/scripts/release-loop/npm-publish-node.ts +118 -0
  232. package/scripts/release-loop/ops-upgrade-node.ts +87 -0
  233. package/scripts/release-loop/release-readiness.ts +58 -0
  234. package/scripts/release-loop/shard-plan.ts +47 -0
  235. package/scripts/release-loop/unattended-release.ts +93 -0
  236. package/scripts/ux/measure-first-text.ts +102 -0
  237. package/scripts/ux/record-page.ts +4 -1
  238. package/scripts/vflow-collect.ts +20 -7
  239. package/scripts/vflow-search.ts +73 -0
  240. package/scripts/video-free-line-test-walker.ts +36 -0
  241. package/scripts/video-pipeline.ts +2 -2
  242. package/skills/explainer-video/SKILL.md +24 -3
  243. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
  244. package/skills/explainer-video/engine/build.mjs +2 -2
  245. package/skills/explainer-video/engine/reel.mjs +235 -0
  246. package/skills/explainer-video/engine/reel.sh +9 -0
  247. package/skills/explainer-video/engine/vo.ts +58 -16
  248. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  249. package/skills/grill-me/SKILL.md +1 -1
  250. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  251. package/src/a2a/server.ts +2 -0
  252. package/src/acp/elanous-aware-peers.ts +30 -0
  253. package/src/acp/server.ts +91 -15
  254. package/src/agent/monad-agent-turn.ts +25 -6
  255. package/src/agent/shared-app-tools.ts +5 -3
  256. package/src/agent-substrate/review-intent.ts +6 -2
  257. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  258. package/src/auth/github-app-token.ts +4 -1
  259. package/src/autopilot/mission-origin.ts +20 -4
  260. package/src/autopilot/mission-phase-track.ts +11 -1
  261. package/src/autopilot/track-agent-shadow.ts +151 -0
  262. package/src/autopilot/track-agent.ts +26 -6
  263. package/src/autopilot/track-courier.ts +2 -2
  264. package/src/boot/acp-server-help.ts +16 -0
  265. package/src/boot/daemon-prompt-turn.ts +31 -10
  266. package/src/boot/daemon-tools/index.ts +7 -1
  267. package/src/budget/codex-credit-pace.ts +84 -0
  268. package/src/budget/codex-credit-plan.ts +154 -0
  269. package/src/budget/codex-reset-credit-state.ts +2 -0
  270. package/src/chat/chat-cli.ts +4 -1
  271. package/src/chat/short-question.ts +40 -0
  272. package/src/cli/agent-cli.ts +24 -15
  273. package/src/cli/ask-bare.ts +42 -0
  274. package/src/cli/cli-user-error.ts +25 -0
  275. package/src/cli/codex-credit-plan-view.ts +25 -0
  276. package/src/cli/dependence-gauge.ts +237 -0
  277. package/src/cli/doctor-cli.ts +5 -0
  278. package/src/cli/doctor-modes.ts +48 -0
  279. package/src/cli/intake-cli.ts +12 -2
  280. package/src/cli/intake-source-cli.ts +58 -0
  281. package/src/cli/machine-cli.ts +23 -0
  282. package/src/cli/msg-cli.ts +137 -0
  283. package/src/cli/nexus-ios-bind.ts +25 -8
  284. package/src/cli/phone-link.ts +117 -0
  285. package/src/cli/plugin-cli.ts +52 -0
  286. package/src/cli/pr-cli.ts +36 -0
  287. package/src/cli/private-ledger-commands.ts +31 -0
  288. package/src/cli/provider-cli.ts +5 -0
  289. package/src/cli/release-cli.ts +56 -12
  290. package/src/cli/release-run-if-ready.ts +48 -0
  291. package/src/cli/setup-cli.ts +27 -6
  292. package/src/cli/setup-recommend.ts +8 -0
  293. package/src/cli/usage-cli.ts +11 -1
  294. package/src/codex/setup.ts +27 -8
  295. package/src/dashboard/index.ts +1 -1
  296. package/src/discord.ts +56 -6
  297. package/src/domains/core-tools.ts +37 -14
  298. package/src/domains/outbound-alert.ts +55 -16
  299. package/src/exec-requests/planner.ts +140 -0
  300. package/src/exec-requests/runner.ts +338 -0
  301. package/src/exec-requests/store.ts +76 -0
  302. package/src/field/field-media.ts +250 -0
  303. package/src/field/field-reel.ts +196 -0
  304. package/src/grok/credential.ts +7 -5
  305. package/src/harness/aside-run-status.ts +33 -0
  306. package/src/harness/browser-verify.ts +48 -17
  307. package/src/harness/harness-cli-command.ts +25 -3
  308. package/src/harness/image-load-state.ts +21 -0
  309. package/src/harness/process-garbage.ts +45 -0
  310. package/src/index.ts +104 -38
  311. package/src/instance/leader.ts +13 -1
  312. package/src/intake-plane/collect-github.ts +5 -3
  313. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  314. package/src/intake-plane/discord-seat-work.ts +56 -0
  315. package/src/intake-plane/intake-sources.ts +202 -0
  316. package/src/intake-plane/items.ts +28 -12
  317. package/src/intake-plane/seat-doc-route.ts +207 -0
  318. package/src/intake-plane/submit-intake-work.ts +9 -1
  319. package/src/intake-plane/telegram-seat-work.ts +72 -0
  320. package/src/llm/model-defaults.ts +24 -1
  321. package/src/llm.ts +5 -4
  322. package/src/machines/machine-ledger.ts +127 -0
  323. package/src/market/pack-internal-refs.ts +28 -0
  324. package/src/market/publish.ts +31 -4
  325. package/src/maturity/feature-maturity.ts +65 -0
  326. package/src/model-tier/turn-tier.ts +24 -0
  327. package/src/msg/msg-store.ts +149 -0
  328. package/src/native-tool-catalog.ts +2 -1
  329. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  330. package/src/nexus/api/consult-requests.ts +117 -0
  331. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  332. package/src/nexus/api/exec-requests.ts +51 -0
  333. package/src/nexus/api/field-uploads.ts +180 -0
  334. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  335. package/src/nexus/api/graph-approvals.ts +120 -4
  336. package/src/nexus/api/harness-api.ts +219 -5
  337. package/src/nexus/api/http-server.ts +68 -5
  338. package/src/nexus/api/seat-requests.ts +254 -0
  339. package/src/nexus/api/trace-run-scan.ts +37 -0
  340. package/src/nexus/api/trace.ts +19 -3
  341. package/src/nexus/index.ts +3 -0
  342. package/src/nexus/setup-status.ts +4 -5
  343. package/src/notify/llm-budget-alert.ts +125 -0
  344. package/src/oauth/codex-account-rotation.ts +26 -4
  345. package/src/oauth/codex-account-store.ts +20 -2
  346. package/src/oauth/codex-browser-login.ts +173 -0
  347. package/src/oauth/codex.ts +4 -1
  348. package/src/oauth/fallback-chain.ts +5 -5
  349. package/src/oauth/store.ts +42 -5
  350. package/src/onboarding/continue-here.ts +29 -0
  351. package/src/onboarding/entry-hints.ts +8 -0
  352. package/src/onboarding/full-screen-io.ts +35 -3
  353. package/src/onboarding/help/skills.md +1 -1
  354. package/src/onboarding/non-interactive.ts +1 -0
  355. package/src/onboarding/subscription-hint.ts +27 -0
  356. package/src/onboarding/web-first.ts +83 -0
  357. package/src/onboarding.ts +230 -23
  358. package/src/plugins/maker/node-install.ts +92 -0
  359. package/src/plugins/maker/node-maker.ts +139 -0
  360. package/src/plugins/maker/plugin-maker.ts +220 -0
  361. package/src/policy/llm-policy.ts +268 -0
  362. package/src/prompt-library/session-guidance.ts +7 -1
  363. package/src/prompt-library/universal-preamble.ts +2 -2
  364. package/src/relay/channel.ts +184 -0
  365. package/src/relay/mux.ts +182 -0
  366. package/src/relay/relay-cli.ts +20 -0
  367. package/src/relay/relay-server.ts +220 -0
  368. package/src/relay/test-vectors.json +29 -0
  369. package/src/release-loop/checklist.ts +36 -9
  370. package/src/seat-address/seat-address.ts +34 -0
  371. package/src/self-dev/decompose.ts +1 -0
  372. package/src/self-dev/dev-pipeline.ts +6 -3
  373. package/src/self-dev/run-record-mirror.ts +44 -0
  374. package/src/self-dev/run-store.ts +12 -3
  375. package/src/self-implement/harness-policy-gates.ts +4 -1
  376. package/src/self-implement/report-origin.ts +12 -1
  377. package/src/self-implement/run-ledger-compact.ts +1 -1
  378. package/src/session/chat.ts +17 -5
  379. package/src/session/index.ts +23 -10
  380. package/src/skills/shared-agent-skill-roots.ts +25 -0
  381. package/src/steward/failure-streak.ts +78 -0
  382. package/src/steward/steward-cards.ts +24 -0
  383. package/src/steward/triage-budget.ts +127 -0
  384. package/src/steward/triage-plan.ts +75 -0
  385. package/src/steward/triage.ts +244 -42
  386. package/src/task-cards/card-store.ts +1 -1
  387. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  388. package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
  389. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  390. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  391. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  392. package/src/telegram.ts +145 -2
  393. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  394. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  395. package/src/user-config.ts +84 -4
  396. package/src/version/packed-revision.json +1 -1
  397. package/src/video-pipeline/recipes/character.ts +42 -5
  398. package/src/video-pipeline/recipes/explainer.ts +213 -0
  399. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  400. package/src/video-pipeline/recipes/free-line.ts +16 -3
  401. package/src/video-pipeline/recipes/remote.ts +7 -1
  402. package/src/video-pipeline/vflow-reference.ts +109 -0
  403. package/src/video-pipeline/walk-line.ts +3 -2
  404. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
  411. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
  419. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
  477. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  478. package/apps/pwa/out/_next/static/ktMqfrBbf8BMGac0jqLrb/_buildManifest.js +0 -1
  479. /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
@@ -6,7 +6,8 @@
6
6
 
7
7
  import * as childProcess from 'node:child_process';
8
8
  import { existsSync, readFileSync, appendFileSync, mkdirSync, unlinkSync } from 'node:fs';
9
- import { dirname, join } from 'node:path';
9
+ import { dirname, join, relative } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
10
11
  import { getElanousConfigDir } from '../elanous-config-dir.js';
11
12
  import { conatusEnv } from './conatus-env.js';
12
13
  import { spillLongContent } from '../storage/content-spill.js';
@@ -54,7 +55,7 @@ function recentSendBurst(): { recentCount: number; burst: boolean } {
54
55
 
55
56
  /** 발송 1건을 logs.db 에 관측(fail-open) — 발송 시각·mode·kind·밀림 판정. */
56
57
  function logSend(
57
- mode: 'realtime' | 'deferred' | 'quiet-bypass' | 'flush',
58
+ mode: 'realtime' | 'quiet-bypass' | 'flush',
58
59
  kind: string,
59
60
  extra: Record<string, unknown> = {},
60
61
  opts?: { level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'critical' },
@@ -111,15 +112,16 @@ export function userRecentlyActive(now: Date = new Date()): boolean {
111
112
 
112
113
  /** 야간 보류 적재 (jsonl append — 크론 동시 실행에 안전). origin 있으면 함께 적재 —
113
114
  * 아침 flush 가 그 origin(발신 채널)으로 되돌려 발송(없으면 report 묶음). */
114
- function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null, path = DEFERRED_PATH): void {
115
+ function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null, path = DEFERRED_PATH): boolean {
115
116
  try {
116
117
  mkdirSync(dirname(path), { recursive: true });
117
118
  const rec = { ts: new Date().toISOString(), kind, text, ...(origin ? { origin } : {}) };
118
119
  appendFileSync(path, JSON.stringify(rec) + '\n');
119
- console.log(`[outbound] 야간 무음(00:00~06:30 KST) — 보류 적재 (${kind})`);
120
- logSend('deferred', kind, { reason: 'quiet-hours' }); // 밀림 적재 관측
120
+ console.error(`[outbound] 야간 무음(00:00~06:30 KST) — 보류 적재 (${kind})`);
121
+ return true;
121
122
  } catch (e) {
122
- console.log(`[outbound] 보류 적재 실패 — 콘솔 출력\n${text}`, e instanceof Error ? e.message : '');
123
+ console.error(`[outbound] 보류 적재 실패 — 콘솔 출력\n${text}`, e instanceof Error ? e.message : '');
124
+ return false;
123
125
  }
124
126
  }
125
127
 
@@ -139,8 +141,29 @@ function resolveBotToken(botId?: string): string | null {
139
141
  } catch { return null; }
140
142
  }
141
143
 
142
- /** origin(발신 채널)으로 직접 텔레그램 발송. telegram+chatId+토큰 해석 성공 시 true. */
144
+ /** origin(발신 채널)으로 직접 발송. 실패하면 호출자가 report 폴백/재시도한다. */
143
145
  function deliverToOrigin(origin: MissionOrigin, text: string): boolean {
146
+ if (origin.channel === 'discord') {
147
+ if (!origin.channelId) return false;
148
+ try {
149
+ const token = resolveChannelBotToken('discord', getUserConfig())?.token;
150
+ if (!token) return false;
151
+ const channelId = origin.discordThreadId || origin.channelId;
152
+ const out = spillLongContent(text).text;
153
+ for (let start = 0; start < out.length;) {
154
+ let end = Math.min(start + 2000, out.length);
155
+ if (end < out.length && /[\uD800-\uDBFF]/.test(out[end - 1]!)) end--;
156
+ const response = curlPost(
157
+ `https://discord.com/api/v10/channels/${channelId}/messages`,
158
+ JSON.stringify({ content: out.slice(start, end) }),
159
+ [`Authorization: Bot ${token}`, 'Content-Type: application/json'],
160
+ );
161
+ if (typeof response?.id !== 'string' || !response.id) return false;
162
+ start = end;
163
+ }
164
+ return out.length > 0;
165
+ } catch { return false; }
166
+ }
144
167
  if (origin.channel !== 'telegram' || origin.chatId == null) return false;
145
168
  const token = resolveBotToken(origin.botId);
146
169
  if (!token) return false;
@@ -189,11 +212,13 @@ export function flushDeferred(path = DEFERRED_PATH): number {
189
212
  // origin 없는 것 = report 묶음(기존 동작·finance/크론 알림).
190
213
  const noOrigin = items.filter(i => !i.origin);
191
214
  if (noOrigin.length && deliver(fmt(noOrigin), 'report')) delivered += noOrigin.length;
192
- // ★ origin 있는 것 = 발신 채널(botId+chatId)별 묶음으로 되돌려 발송(미션 알림 → 메인 Q&A 봇).
215
+ // origin 있는 것 = 발신 채널·스레드별로 묶어 서로 다른 수신자에게 섞이지 않게 발송.
193
216
  const groups = new Map<string, { origin: MissionOrigin; list: Item[] }>();
194
217
  for (const it of items) {
195
218
  if (!it.origin) continue;
196
- const key = `${it.origin.botId ?? ''}:${it.origin.chatId ?? ''}`;
219
+ const key = it.origin.channel === 'discord'
220
+ ? JSON.stringify(['discord', it.origin.channelId, it.origin.discordThreadId ?? null])
221
+ : JSON.stringify([it.origin.channel, it.origin.botId ?? null, it.origin.chatId ?? null, it.origin.threadId ?? null]);
197
222
  if (!groups.has(key)) groups.set(key, { origin: it.origin, list: [] });
198
223
  groups.get(key)!.list.push(it);
199
224
  }
@@ -228,31 +253,45 @@ export function recordOutbound(text: string, kind: string): void {
228
253
  /** 발송 진입점 — 야간 무음 게이트(00:00~06:30 KST 보류) + 보류분 자동 플러시.
229
254
  * origin(발신 채널) 이 주어지면 그 채널(메인 Q&A 봇 등)로 되돌려 발송 — 무음이면 origin 을
230
255
  * 함께 보류했다가 아침에 그 채널로 flush. origin 없거나 발송 실패 시 report 폴백(기존 동작). */
231
- export function sendOutbound(text: string, kind = 'alert', origin?: MissionOrigin | null): boolean {
256
+ export function sendOutbound(text: string, kind = 'alert', origin?: (MissionOrigin & { surface?: string }) | null): boolean {
257
+ // Prefer the producer's surface; otherwise keep the caller's repository-relative path
258
+ // so two producers named index.ts cannot collapse into one sender.
259
+ const caller = new Error().stack?.split('\n')[2] ?? '';
260
+ const callerPath = caller.match(/(?:\(|\s)(file:\/\/[^\s()]+|\/[^\s()]+\.[cm]?[jt]s):\d+:\d+\)?/)?.[1];
261
+ const repoRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
262
+ const source = origin?.surface || (callerPath
263
+ ? relative(repoRoot, callerPath.startsWith('file://') ? fileURLToPath(callerPath) : callerPath).replace(/\\/g, '/')
264
+ : 'unknown');
265
+ const logOutcome = (event: 'sent' | 'deferred' | 'failed'): void => {
266
+ try { debug.log('outbound.send', event, { kind, source, chars: text.length }); } catch { /* fail-open */ }
267
+ };
232
268
  // ★ 무음 우회(대표 2026-07-14) — 사용자가 최근(기본 30분) genuine 인텐트(타이핑/버튼탭)를 냈으면
233
269
  // 깨어있으므로 야간 무음이어도 즉시 발송(사용자 발원 흐름의 결과물이 아침까지 묶이지 않게).
234
270
  // 우회 시 flushDeferred 로 그간 보류분도 함께 전달(사용자가 지금 볼 수 있음).
235
271
  if (inQuietHours() && !userRecentlyActive()) {
236
- deferOutbound(text, kind, origin);
272
+ const queued = deferOutbound(text, kind, origin);
237
273
  recordOutbound(text, kind); // 보류도 논리적 발송 — 회상 대상(데몬 미경유라 클라 기록)
238
- return true; // 보류 = 수락 (호출측 재시도/에러 루프 방지)
274
+ logOutcome(queued ? 'deferred' : 'failed');
275
+ return true; // 보류 시 수락 — 적재 오류에서도 기존 호출측 재시도 계약 유지
239
276
  }
240
277
  const bypass = inQuietHours();
241
- if (bypass) console.log('[outbound] 야간 무음 우회 — 최근 사용자 활동(깨어있음) → 즉시 발송 + 보류분 flush');
278
+ if (bypass) console.error('[outbound] 야간 무음 우회 — 최근 사용자 활동(깨어있음) → 즉시 발송 + 보류분 flush');
242
279
  try { flushDeferred(); } catch { /* fail-soft */ }
243
280
  // ★ 발송 관측(대표 지시) — 발송 시각·mode·밀림(burst) 판정을 logs.db 에. burst=최근 2분 5건+
244
281
  // (몰려 나가는 중 = 밀림 의심). `elanous logs --category outbound.send` 로 실시간/밀림 구분.
245
282
  const { recentCount, burst } = recentSendBurst();
246
283
  logSend(bypass ? 'quiet-bypass' : 'realtime', kind, { burst, recentCount, ...(burst ? { backlog: true } : {}) });
247
284
  // ★ origin 되돌림(무음 밖) — 발신 채널로 직접 발송. 성공 시 종료, 실패면 report 폴백.
248
- if (origin && origin.channel === 'telegram' && origin.chatId != null && deliverToOrigin(origin, text)) {
285
+ if (origin && deliverToOrigin(origin, text)) {
249
286
  recordOutbound(text, kind);
287
+ logOutcome('sent');
250
288
  return true;
251
289
  }
252
290
  const path = deliver(text, kind);
253
291
  // 원장 기록은 정확히 1회. 데몬 경유(daemon)면 /v1/outbound 핸들러(outbound-report.ts)
254
292
  // 가 기록하므로 클라는 중복 금지 — 직접 폴백(direct·데몬 다운)만 클라가 기록.
255
293
  if (path === 'direct') recordOutbound(text, kind);
294
+ logOutcome(path === false ? 'failed' : 'sent');
256
295
  return path !== false;
257
296
  }
258
297
 
@@ -275,7 +314,7 @@ export function classifyDaemonResponse(j: unknown): DaemonPathClass {
275
314
  function logDaemonPath(classification: DaemonPathClass, kind: string, extra: Record<string, unknown> = {}): void {
276
315
  try { debug.log('outbound.send', 'daemon-path', { classification, kind, ...extra }); } catch { /* fail-soft */ }
277
316
  if (classification === 'ok') return;
278
- try { console.log(`[outbound] daemon-path ${classification}`); } catch { /* fail-soft */ }
317
+ try { console.error(`[outbound] daemon-path ${classification}`); } catch { /* fail-soft */ }
279
318
  }
280
319
 
281
320
  /** elanous `/v1/outbound` 우선 → 실패 시 텔레그램 직접. 성공 경로 반환(원장 중복방지용). */
@@ -334,6 +373,6 @@ function sendTelegramDirect(text: string, kind?: string): boolean {
334
373
  tok = tok || env.TELEGRAM_BOT_TOKEN || '';
335
374
  chat = chat || env.TELEGRAM_CHAT_ID || '';
336
375
  }
337
- if (!tok || !chat) { console.log('[outbound] 토큰/chat 미설정 — 콘솔 출력\n' + text); return false; }
376
+ if (!tok || !chat) { console.error('[outbound] 토큰/chat 미설정 — 콘솔 출력\n' + text); return false; }
338
377
  return sendTelegramRaw(tok, chat, text);
339
378
  }
@@ -0,0 +1,140 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
6
+ import { defaultGraphsDir } from '../self-implement/graph-templates.js';
7
+ import { handleGraphsGet } from '../nexus/api/graphs-api.js';
8
+ import { debug } from '../debug/log.js';
9
+
10
+ export interface InstalledGraph {
11
+ id: string;
12
+ title: string;
13
+ description: string;
14
+ path: string;
15
+ /** 플러그인 그래프가 받는 입력 키(`examples/*.json` 표본에서) — COO 가 inputs 를 채울 수 있게. */
16
+ inputKeys?: string[];
17
+ }
18
+ export interface ExecPlanItem {
19
+ seat: string;
20
+ title: string;
21
+ graphId: string;
22
+ inputs: Record<string, unknown>;
23
+ reason?: string;
24
+ }
25
+
26
+ /** 설치된 플러그인의 실행 그래프 — `<state>/plugins/<market>/<name>/<version>/graphs/{recipes.yaml, <id>.yaml}`
27
+ * (`src/plugins/install/plugin-install.ts` 설치 경로). 이름마다 가장 새 버전 하나 · 제목·설명은 그래프 `loop` 가 없으면
28
+ * `plugin.json` 의 name·description(COO 가 «무엇을 하는 그래프인지» 알게). 2026-10-01 실측: 이것을 안 봐서
29
+ * 한 줄이 자리로 나뉘어도 «맞는 설치 그래프 없음»이었다. */
30
+ export function installedPluginGraphs(pluginsRoot = join(elanousStateRoot(), 'plugins')): { graphs: InstalledGraph[]; errors: string[] } {
31
+ const graphs: InstalledGraph[] = [];
32
+ const errors: string[] = [];
33
+ const dirs = (path: string): string[] => {
34
+ try { return readdirSync(path, { withFileTypes: true }).filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name); }
35
+ catch { return []; }
36
+ };
37
+ for (const market of dirs(pluginsRoot)) {
38
+ for (const name of dirs(join(pluginsRoot, market))) {
39
+ const versions = dirs(join(pluginsRoot, market, name)).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
40
+ const version = versions[0];
41
+ if (!version) continue;
42
+ const root = join(pluginsRoot, market, name, version);
43
+ const graphsDir = join(root, 'graphs');
44
+ if (!existsSync(join(graphsDir, 'recipes.yaml'))) continue;
45
+ let meta: { name?: string; description?: string } = {};
46
+ try { meta = JSON.parse(readFileSync(join(root, 'plugin.json'), 'utf8')) as typeof meta; } catch { errors.push(`${market}/${name}@${version}: plugin.json unreadable`); }
47
+ // 입력 키 — 플러그인이 입력 스키마를 따로 선언하지 않아 `examples/*.json` 첫 표본의 키를 쓴다(값은 가상이라 안 싣는다).
48
+ // 2026-10-01 실측: 키를 몰라 COO 가 `inputs:{}` 로 맡겼고 geo-check 가 «brand is required» 로 첫 노드에서 떨어졌다.
49
+ let inputKeys: string[] | undefined;
50
+ try {
51
+ const sample = readdirSync(join(root, 'examples')).filter(f => f.endsWith('.json')).sort()[0];
52
+ if (sample) {
53
+ const parsed = JSON.parse(readFileSync(join(root, 'examples', sample), 'utf8')) as unknown;
54
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) inputKeys = Object.keys(parsed);
55
+ }
56
+ } catch { /* 표본 없음 · 망가짐 = 키 모름 */ }
57
+ let files: string[] = [];
58
+ try { files = readdirSync(graphsDir).filter(f => /\.ya?ml$/.test(f) && f !== 'recipes.yaml'); } catch { errors.push(`${market}/${name}@${version}: graphs unreadable`); }
59
+ for (const file of files) {
60
+ const path = join(graphsDir, file);
61
+ try {
62
+ const doc = parseYaml(readFileSync(path, 'utf8')) as { graph_id?: string; loop?: { title?: string; description?: string } };
63
+ const id = doc?.graph_id;
64
+ if (!id || !/^[a-z0-9-]+$/.test(id)) continue;
65
+ graphs.push({ id, title: doc.loop?.title ?? meta.name ?? id, description: doc.loop?.description ?? meta.description ?? '', path, ...(inputKeys ? { inputKeys } : {}) });
66
+ } catch { errors.push(`${market}/${name}@${version}: ${file} malformed`); }
67
+ }
68
+ }
69
+ }
70
+ return { graphs, errors };
71
+ }
72
+
73
+ export async function installedGraphs(coreDir = defaultGraphsDir(), mineDir = join(elanousStateRoot(), 'graphs')): Promise<InstalledGraph[]> {
74
+ const base = await coreAndMineGraphs(coreDir, mineDir);
75
+ // 플러그인은 mine 과 같은 상태 뿌리 아래(<root>/graphs ↔ <root>/plugins) — 시험의 임시 mine 은 운영 플러그인을 안 본다.
76
+ const plugins = installedPluginGraphs(join(dirname(mineDir), 'plugins'));
77
+ const seen = new Set(base.map(g => g.id));
78
+ const extra = plugins.graphs.filter(g => !seen.has(g.id));
79
+ debug.log('exec-requests', 'catalog', { coreAndMine: base.length, plugins: extra.length, pluginErrors: plugins.errors.slice(0, 5) });
80
+ return [...base, ...extra];
81
+ }
82
+
83
+ async function coreAndMineGraphs(coreDir: string, mineDir: string): Promise<InstalledGraph[]> {
84
+ const catalog = handleGraphsGet('/v1/graphs', coreDir, { coreDir, mineDir });
85
+ if (!catalog.ok) throw new Error('installed graph catalog unavailable');
86
+ const { graphs } = await catalog.json() as { graphs: Array<{ id: string; source: 'core' | 'mine' }> };
87
+ return graphs.flatMap(({ id, source }) => {
88
+ const dir = source === 'core' ? coreDir : mineDir;
89
+ if (!/^[a-z0-9-]+$/.test(id) || !existsSync(join(dir, 'recipes.yaml'))) return [];
90
+ let names: string[];
91
+ try { names = readdirSync(dir).filter(name => /\.ya?ml$/.test(name)); }
92
+ catch { return []; }
93
+ for (const name of names) {
94
+ const path = join(dir, name);
95
+ try {
96
+ const doc = parseYaml(readFileSync(path, 'utf8')) as { graph_id?: string; loop?: { title?: string; description?: string } };
97
+ if (doc?.graph_id === id) return [{ id, title: doc.loop?.title ?? id, description: doc.loop?.description ?? '', path }];
98
+ } catch { /* A malformed file is not an executable graph. */ }
99
+ }
100
+ return [];
101
+ });
102
+ }
103
+
104
+ export function seatTitles(path = fileURLToPath(new URL('../../scripts/coord-tracks.json', import.meta.url))): string[] {
105
+ const data = JSON.parse(readFileSync(path, 'utf8')) as { tracks: Array<{ title?: string }> };
106
+ return data.tracks.flatMap(track => track.title ? [track.title] : []);
107
+ }
108
+
109
+ export async function judgeExecPlan(text: string, graphs: readonly InstalledGraph[], seats: readonly string[]): Promise<unknown> {
110
+ const prompt = `COO 역할. 요청을 실행 가능한 자리별 그래프로 나눠 맡겨라. JSON 배열만 출력: [{"seat":"자리","title":"할 일","graphId":"설치된 그래프 ID 또는 빈 문자열","inputs":{}}]. 자리 이름은 주어진 목록에서만, 그래프 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)}`;
111
+ const { streamLLM } = await import('../llm.js');
112
+ const { tierModel } = await import('../llm/model-defaults.js');
113
+ const raw = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('best') });
114
+ return JSON.parse(raw.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()) as unknown;
115
+ }
116
+
117
+ export async function planExecRequest(text: string, deps: {
118
+ graphs?: () => Promise<InstalledGraph[]>;
119
+ seats?: () => string[];
120
+ judge?: typeof judgeExecPlan;
121
+ } = {}): Promise<ExecPlanItem[]> {
122
+ const graphs = await (deps.graphs ?? installedGraphs)();
123
+ const seats = (deps.seats ?? seatTitles)();
124
+ const raw = await (deps.judge ?? judgeExecPlan)(text, graphs, seats);
125
+ if (!Array.isArray(raw) || raw.length === 0) throw new Error('COO 계획이 비었거나 배열이 아닙니다');
126
+ const known = new Map(graphs.map(graph => [graph.id, graph]));
127
+ return raw.map((item: unknown) => {
128
+ if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error('COO 계획 항목 형식 오류');
129
+ const row = item as Record<string, unknown>;
130
+ if (typeof row.seat !== 'string' || !seats.includes(row.seat) || typeof row.title !== 'string' || !row.title.trim()
131
+ || typeof row.graphId !== 'string' || !row.inputs || typeof row.inputs !== 'object' || Array.isArray(row.inputs)) {
132
+ throw new Error('COO 계획 항목 형식 오류');
133
+ }
134
+ return {
135
+ seat: row.seat, title: row.title.trim(), graphId: row.graphId,
136
+ inputs: row.inputs as Record<string, unknown>,
137
+ ...(!known.has(row.graphId) ? { reason: `${row.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}),
138
+ };
139
+ });
140
+ }
@@ -0,0 +1,338 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { debug } from '../debug/log.js';
5
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
6
+ import { lastJsonObject, runGraph, type GraphRunState } from '../graph-runner/runner.js';
7
+ import { installedGraphs, planExecRequest, type ExecPlanItem, type InstalledGraph } from './planner.js';
8
+ import { ExecRequestStore, type ExecRequest, type ExecResult, type ExecSeat } from './store.js';
9
+
10
+ export interface ExecRunnerDeps {
11
+ store?: ExecRequestStore;
12
+ graphs?: () => Promise<InstalledGraph[]>;
13
+ plan?: (text: string) => Promise<ExecPlanItem[]>;
14
+ run?: typeof runGraph;
15
+ root?: string;
16
+ }
17
+
18
+ const active = new Set<string>();
19
+ const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
20
+ const kinds: Record<string, ExecResult['kind']> = {
21
+ '.pdf': 'pdf', '.mp4': 'video', '.mov': 'video', '.webm': 'video', '.png': 'image', '.jpg': 'image',
22
+ '.jpeg': 'image', '.webp': 'image', '.md': 'report', '.txt': 'text',
23
+ };
24
+
25
+ function safeFile(path: string, root: string, graphId: string, runId: string): string | null {
26
+ if (!SEGMENT.test(graphId) || !SEGMENT.test(runId)) return null;
27
+ // Only run-prefixed files or the run artifact directory, never a sibling run's output.
28
+ const allowed = join(root, 'graph-runs', graphId);
29
+ const candidate = isAbsolute(path) ? path : resolve(allowed, path);
30
+ try {
31
+ const real = realpathSync(candidate);
32
+ const base = realpathSync(allowed);
33
+ const runPrefix = `${runId}.json.`;
34
+ const local = dirname(real) === base && basename(real).startsWith(runPrefix)
35
+ && !/\.(?:decision\.json|tmp)$/.test(real);
36
+ const artifactsDir = join(allowed, `${runId}.json.artifacts`);
37
+ const realArtifactsDir = existsSync(artifactsDir) && statSync(artifactsDir).isDirectory()
38
+ ? realpathSync(artifactsDir) : null;
39
+ const inRunDirectory = realArtifactsDir === join(base, `${runId}.json.artifacts`)
40
+ && real.startsWith(`${realArtifactsDir}${sep}`);
41
+ if ((!local && !inRunDirectory) || !statSync(real).isFile()) return null;
42
+ return real;
43
+ } catch { return null; }
44
+ }
45
+
46
+ function publicLink(value: unknown): string | null {
47
+ if (typeof value !== 'string') return null;
48
+ try {
49
+ const url = new URL(value);
50
+ return (url.protocol === 'https:' || url.protocol === 'http:') && url.hostname && !url.username && !url.password ? value : null;
51
+ } catch { return null; }
52
+ }
53
+
54
+ function fileName(path: string, runId: string): string {
55
+ const digest = createHash('sha256').update(path).digest('hex');
56
+ return `${runId}--${digest}--${basename(path)}`;
57
+ }
58
+
59
+ function resultOf(output: Record<string, unknown>, seat: ExecSeat, id: string, root: string): ExecResult | null {
60
+ const candidate = typeof output.file === 'string' ? output.file : typeof output.path === 'string' ? output.path : null;
61
+ let url: string;
62
+ let kind: ExecResult['kind'];
63
+ let name: string;
64
+ if (candidate !== null) {
65
+ const path = safeFile(candidate, root, seat.graphId, seat.runId);
66
+ if (!path) return null;
67
+ const fileKind = kinds[basename(path).slice(basename(path).lastIndexOf('.')).toLowerCase()];
68
+ if (!fileKind) return null;
69
+ kind = fileKind;
70
+ name = basename(path);
71
+ url = `/v1/exec-requests/${id}/files/${encodeURIComponent(fileName(path, seat.runId))}`;
72
+ } else {
73
+ const link = publicLink(output.url);
74
+ if (!link) return null;
75
+ kind = 'link';
76
+ name = link;
77
+ url = link;
78
+ }
79
+ const sources = Array.isArray(output.sources) ? output.sources.flatMap((source: unknown) => {
80
+ if (!source || typeof source !== 'object') return [];
81
+ const entry = source as { title?: unknown; url?: unknown };
82
+ const sourceUrl = publicLink(entry.url);
83
+ return typeof entry.title === 'string' && sourceUrl ? [{ title: entry.title, url: sourceUrl }] : [];
84
+ }) : [];
85
+ return { seat: seat.seat, kind, title: typeof output.title === 'string' ? output.title : name,
86
+ url, ...(sources.length ? { sources } : {}) };
87
+ }
88
+
89
+ function syncRun(item: ExecRequest, seat: ExecSeat, state: GraphRunState, root: string): void {
90
+ const previous = seat.status;
91
+ seat.status = state.status === 'done' ? 'done' : state.status === 'failed' || state.status === 'budget-exceeded' ? 'failed' : 'running';
92
+ if (previous !== seat.status) debug.log('exec-requests', 'seat-status', { id: item.id, seat: seat.seat, from: previous, to: seat.status, graphId: seat.graphId, runId: seat.runId });
93
+ const claim = state.status === 'awaiting-approval' && state.pending
94
+ ? `${join(root, 'graph-runs', seat.graphId, `${seat.runId}.json`)}.${state.path.length}.decision.json` : '';
95
+ if (state.status === 'awaiting-approval' && state.pending && !state.pending.decision && !existsSync(claim)) {
96
+ if (!item.approvals.some(a => a.graphId === seat.graphId && a.runId === seat.runId)) {
97
+ item.approvals.push({ graphId: seat.graphId, runId: seat.runId, message: state.pending.message });
98
+ }
99
+ } else item.approvals = item.approvals.filter(a => a.graphId !== seat.graphId || a.runId !== seat.runId);
100
+ for (const node of state.nodes ?? []) {
101
+ const output = lastJsonObject(node.output);
102
+ if (!output) continue;
103
+ const entries = Array.isArray(output.artifacts) ? output.artifacts : [output];
104
+ for (const entry of entries) {
105
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
106
+ const result = resultOf(entry as Record<string, unknown>, seat, item.id, root);
107
+ if (result && !item.results.some(existing => existing.seat === result.seat && existing.url === result.url)) item.results.push(result);
108
+ }
109
+ }
110
+ const summary = [...(state.nodes ?? [])].reverse().map(node => lastJsonObject(node.output)?.summary).find(value => typeof value === 'string');
111
+ if (typeof summary === 'string') item.summary = summary;
112
+ if (seat.status === 'failed') seat.reason = state.nodes?.at(-1)?.error ?? `그래프 런 ${state.status}`;
113
+ }
114
+
115
+ function aggregate(item: ExecRequest): void {
116
+ if (item.status === 'planning' || item.seats.length === 0) return;
117
+ if (item.seats.some(seat => seat.status === 'waiting' || seat.status === 'running')) item.status = 'running';
118
+ else item.status = item.seats.some(seat => seat.status === 'failed') ? 'failed' : 'done';
119
+ // 실패면 요약은 «실패 사유»가 이긴다 — 먼저 끝난 자리의 산출 설명이 남아 있어도 덮는다(혼합 결과에서 성공처럼 보이지 않게).
120
+ if (item.status === 'failed') {
121
+ const reasons = item.seats.filter(seat => seat.status === 'failed').map(seat => seat.reason ?? `${seat.seat}: 그래프 실행 실패`);
122
+ if (reasons.length > 0) item.summary = reasons.join('; ');
123
+ }
124
+ }
125
+
126
+ export class ExecRequestRunner {
127
+ readonly store: ExecRequestStore;
128
+ private readonly root: string;
129
+ constructor(private readonly deps: ExecRunnerDeps = {}) {
130
+ this.store = deps.store ?? new ExecRequestStore();
131
+ this.root = deps.root ?? effectiveInstanceRoot();
132
+ }
133
+
134
+ /** 데몬 «시작» 때 한 번만 부른다(`startNexusHttpServer`). 생성자에서 부르면 모듈을 import 만 한 다른 프로세스
135
+ * (시험·CLI·동시 서버)가 살아 있는 요청을 «재시작 중단»으로 실패 처리한다(리뷰 3라운드 must-fix). */
136
+ reconcileInterrupted(): void {
137
+ for (const item of this.store.list()) {
138
+ if (item.status !== 'planning' && item.status !== 'running') continue;
139
+ if (item.status === 'planning') {
140
+ item.status = 'failed';
141
+ item.summary = '데몬 재시작으로 COO 계획이 중단됐습니다';
142
+ } else {
143
+ if (item.seats.length === 0) {
144
+ item.status = 'failed';
145
+ item.summary = '데몬 재시작으로 자리 배정이 중단됐습니다';
146
+ }
147
+ for (const seat of item.seats) {
148
+ if (seat.status === 'done' || seat.status === 'failed') continue;
149
+ const reason = '데몬 재시작으로 그래프 실행이 중단됐습니다';
150
+ if (SEGMENT.test(seat.graphId) && SEGMENT.test(seat.runId)) {
151
+ try {
152
+ const file = join(this.root, 'graph-runs', seat.graphId, `${seat.runId}.json`);
153
+ const state = JSON.parse(readFileSync(file, 'utf8')) as GraphRunState;
154
+ if (state.graphId === seat.graphId && state.runId === seat.runId) {
155
+ if (state.status === 'done' || state.status === 'failed' || state.status === 'budget-exceeded'
156
+ || (state.status === 'awaiting-approval' && state.pending)) {
157
+ syncRun(item, seat, state, this.root);
158
+ continue;
159
+ }
160
+ }
161
+ } catch { /* No trustworthy completed or approval-pending run to recover. */ }
162
+ }
163
+ const previous = seat.status;
164
+ seat.status = 'failed';
165
+ seat.reason = reason;
166
+ item.approvals = item.approvals.filter(a => a.graphId !== seat.graphId || a.runId !== seat.runId);
167
+ debug.log('exec-requests', 'seat-status', { id: item.id, seat: seat.seat, from: previous, to: 'failed', reason });
168
+ }
169
+ aggregate(item);
170
+ if (item.status === 'failed' && item.seats.length > 0) item.summary = item.seats.filter(seat => seat.status === 'failed')
171
+ .map(seat => seat.reason ?? `${seat.seat}: 그래프 실행 실패`).join('; ');
172
+ }
173
+ this.store.save(item);
174
+ if (item.status === 'failed') debug.log('exec-requests', 'failed', { id: item.id, reason: item.summary });
175
+ }
176
+ }
177
+
178
+ submit(text: string): ExecRequest {
179
+ const item = this.store.create(text);
180
+ void this.execute(item.id).catch(error => {
181
+ const current = this.store.get(item.id);
182
+ if (!current) return;
183
+ current.status = 'failed';
184
+ current.summary = error instanceof Error ? error.message : String(error);
185
+ this.store.save(current);
186
+ debug.log('exec-requests', 'failed', { id: item.id, reason: current.summary });
187
+ });
188
+ return item;
189
+ }
190
+
191
+ private async execute(id: string): Promise<void> {
192
+ const item = this.store.get(id);
193
+ if (!item) return;
194
+ const graphs = await (this.deps.graphs ?? installedGraphs)();
195
+ const plans = await (this.deps.plan ?? ((text: string) => planExecRequest(text, { graphs: async () => graphs })))(item.text);
196
+ const byId = new Map(graphs.map(graph => [graph.id, graph]));
197
+ item.seats = plans.map(plan => ({ seat: plan.seat, title: plan.title, status: plan.reason || !byId.has(plan.graphId) ? 'failed' : 'waiting', graphId: plan.graphId,
198
+ runId: plan.reason || !byId.has(plan.graphId) ? '' : randomUUID(), inputs: plan.inputs,
199
+ ...(plan.reason || !byId.has(plan.graphId) ? { reason: plan.reason ?? `${plan.seat}: 요청에 맞는 설치된 실행 그래프가 없습니다` } : {}) }));
200
+ item.status = 'running';
201
+ debug.log('exec-requests', 'planned', { id, seats: item.seats.map(seat => ({ seat: seat.seat, graphId: seat.graphId, status: seat.status })) });
202
+ this.store.save(item);
203
+ await Promise.all(item.seats.map(async (seat, index) => {
204
+ const graph = byId.get(seat.graphId);
205
+ if (!graph || seat.status === 'failed') return;
206
+ const starting = this.store.get(id)!;
207
+ starting.seats[index]!.status = 'running';
208
+ this.store.save(starting);
209
+ debug.log('exec-requests', 'started', { id, seat: seat.seat, graphId: seat.graphId, runId: seat.runId });
210
+ debug.log('exec-requests', 'seat-status', { id, seat: seat.seat, from: 'waiting', to: 'running' });
211
+ try {
212
+ const state = await (this.deps.run ?? runGraph)(graph.path, { input: seat.inputs, runId: seat.runId, deps: { root: this.root } });
213
+ const current = this.store.get(id)!;
214
+ syncRun(current, current.seats[index]!, state, this.root);
215
+ aggregate(current);
216
+ this.store.save(current);
217
+ } catch (error) {
218
+ const current = this.store.get(id)!;
219
+ current.seats[index]!.status = 'failed';
220
+ current.seats[index]!.reason = error instanceof Error ? error.message : String(error);
221
+ debug.log('exec-requests', 'seat-status', { id, seat: seat.seat, to: 'failed', reason: current.seats[index]!.reason });
222
+ aggregate(current);
223
+ this.store.save(current);
224
+ }
225
+ }));
226
+ const finished = this.store.get(id)!;
227
+ aggregate(finished);
228
+ this.store.save(finished);
229
+ if (finished.status === 'done' || finished.status === 'failed') debug.log('exec-requests', finished.status, { id, results: finished.results.length });
230
+ }
231
+
232
+ get(id: string): ExecRequest | null {
233
+ const item = this.store.get(id);
234
+ if (!item) return null;
235
+ if (item.status === 'planning' || item.status === 'done' || item.status === 'failed') return item;
236
+ for (const seat of item.seats) {
237
+ // An approved decision triggers a resume; failures are terminal and never retried on reads.
238
+ // A terminal seat is never re-synced from a stale awaiting-approval snapshot.
239
+ if (seat.status === 'done' || seat.status === 'failed' || !seat.runId || !SEGMENT.test(seat.graphId) || !SEGMENT.test(seat.runId)) continue;
240
+ const file = join(this.root, 'graph-runs', seat.graphId, `${seat.runId}.json`);
241
+ if (!existsSync(file)) continue;
242
+ try {
243
+ const state = JSON.parse(readFileSync(file, 'utf8')) as GraphRunState;
244
+ if (state.graphId !== seat.graphId || state.runId !== seat.runId) continue;
245
+ syncRun(item, seat, state, this.root);
246
+ if (state.status === 'awaiting-approval' && state.pending && !state.pending.decision) {
247
+ const decision = `${file}.${state.path.length}.decision.json`;
248
+ if (existsSync(decision)) {
249
+ item.approvals = item.approvals.filter(a => a.runId !== seat.runId);
250
+ const key = `${seat.graphId}/${seat.runId}`;
251
+ if (!active.has(key)) {
252
+ active.add(key);
253
+ const launch = async () => {
254
+ try {
255
+ const graphs = await (this.deps.graphs ?? installedGraphs)();
256
+ const graph = graphs.find(candidate => candidate.id === seat.graphId);
257
+ if (!graph) throw new Error('설치된 그래프가 없습니다');
258
+ const resumed = await (this.deps.run ?? runGraph)(graph.path, { resumeRunId: seat.runId, deps: { root: this.root } });
259
+ const current = this.store.get(id);
260
+ const target = current?.seats.find(s => s.runId === seat.runId);
261
+ if (current && target) { syncRun(current, target, resumed, this.root); aggregate(current); this.store.save(current); }
262
+ } catch (error) {
263
+ const reason = error instanceof Error ? error.message : String(error);
264
+ const current = this.store.get(id);
265
+ const target = current?.seats.find(s => s.graphId === seat.graphId && s.runId === seat.runId);
266
+ if (current && target && target.status !== 'done') {
267
+ target.status = 'failed';
268
+ target.reason = `그래프 재개 실패: ${reason}`;
269
+ current.approvals = current.approvals.filter(a => a.graphId !== seat.graphId || a.runId !== seat.runId);
270
+ aggregate(current);
271
+ this.store.save(current);
272
+ debug.log('exec-requests', 'seat-status', { id, seat: seat.seat, to: 'failed', reason: target.reason });
273
+ }
274
+ debug.log('exec-requests', 'failed', { id, seat: seat.seat, reason });
275
+ } finally { active.delete(key); }
276
+ };
277
+ void launch();
278
+ }
279
+ }
280
+ }
281
+ } catch (error) { debug.log('exec-requests', 'failed', { id, seat: seat.seat, reason: String(error) }); }
282
+ }
283
+ const current = this.store.get(id) ?? item;
284
+ for (const seat of item.seats) {
285
+ const target = current.seats.find(candidate => candidate.runId && candidate.runId === seat.runId);
286
+ if (target && target.status !== 'done' && target.status !== 'failed') {
287
+ target.status = seat.status;
288
+ if (seat.reason) target.reason = seat.reason;
289
+ }
290
+ }
291
+ const approvals = new Map(current.approvals.map(approval => [`${approval.graphId}/${approval.runId}`, approval]));
292
+ for (const approval of item.approvals) approvals.set(`${approval.graphId}/${approval.runId}`, approval);
293
+ current.approvals = [...approvals.values()].filter(approval => {
294
+ const seat = current.seats.find(candidate => candidate.graphId === approval.graphId && candidate.runId === approval.runId);
295
+ if (!seat || seat.status === 'done' || seat.status === 'failed') return false;
296
+ const file = join(this.root, 'graph-runs', approval.graphId, `${approval.runId}.json`);
297
+ try {
298
+ const state = JSON.parse(readFileSync(file, 'utf8')) as GraphRunState;
299
+ return state.status === 'awaiting-approval' && !!state.pending && !state.pending.decision
300
+ && !existsSync(`${file}.${state.path.length}.decision.json`);
301
+ } catch { return false; }
302
+ });
303
+ for (const result of item.results) {
304
+ if (!current.results.some(existing => existing.url === result.url)) current.results.push(result);
305
+ }
306
+ if (item.summary && !current.summary) current.summary = item.summary;
307
+ aggregate(current);
308
+ this.store.save(current);
309
+ return current;
310
+ }
311
+
312
+ file(id: string, name: string): string | null {
313
+ // Artifact names may contain spaces and Unicode; only a single safe path component is accepted.
314
+ if (!name || name === '.' || name === '..' || name !== basename(name) || /[\\/\x00-\x1f\x7f]/.test(name)) return null;
315
+ const item = this.get(id);
316
+ const result = item?.results.find(entry => entry.url === `/v1/exec-requests/${id}/files/${encodeURIComponent(name)}`);
317
+ if (!result) return null;
318
+ const seat = item!.seats.find(entry => name.startsWith(`${entry.runId}--`) && entry.seat === result.seat);
319
+ if (!seat) return null;
320
+ try {
321
+ const state = JSON.parse(readFileSync(join(this.root, 'graph-runs', seat.graphId, `${seat.runId}.json`), 'utf8')) as GraphRunState;
322
+ if (state.graphId !== seat.graphId || state.runId !== seat.runId) return null;
323
+ for (const node of state.nodes ?? []) {
324
+ const output = lastJsonObject(node.output);
325
+ if (!output) continue;
326
+ for (const entry of Array.isArray(output.artifacts) ? output.artifacts : [output]) {
327
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
328
+ const artifact = entry as Record<string, unknown>;
329
+ const candidate = typeof artifact.file === 'string' ? artifact.file : artifact.path;
330
+ if (typeof candidate !== 'string') continue;
331
+ const path = safeFile(candidate, this.root, seat.graphId, seat.runId);
332
+ if (path && fileName(path, seat.runId) === name) return path;
333
+ }
334
+ }
335
+ } catch { return null; }
336
+ return null;
337
+ }
338
+ }