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
@@ -0,0 +1,202 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
6
+ import { findTrack } from '../autopilot/mission-phase-track.js';
7
+ import { appendGithubStarSnapshots, collectGithubStars, ghApiSearchRepos, type SearchGithubRepos } from './collect-github.js';
8
+ import { ingestIntakeItems, parseRawIntakeJsonl, type IntakeSource, type RegisteredIntakeSourceKind, type RawIntakeItem } from './items.js';
9
+
10
+ export type SourceKind = 'github-query' | 'rss' | 'command';
11
+ export interface RegisteredIntakeSource {
12
+ id: string;
13
+ seat: string;
14
+ kind: SourceKind;
15
+ spec: string;
16
+ every: string;
17
+ until?: string;
18
+ why?: string;
19
+ lastRunAt?: string;
20
+ }
21
+
22
+ export function canonicalSeat(value: string): string {
23
+ const seat = findTrack(value.trim());
24
+ if (!seat) throw new Error(`알 수 없는 자리: ${value}`);
25
+ return seat.id;
26
+ }
27
+
28
+ export function cadenceMs(every: string): number {
29
+ const match = /^([1-9]\d*)([hdw])$/.exec(every);
30
+ if (!match) throw new Error(`--every 는 Nh · Nd · Nw 형식이어야 한다: ${every}`);
31
+ const ms = Number(match[1]) * ({ h: 3600_000, d: 86_400_000, w: 604_800_000 }[match[2]!] ?? 0);
32
+ if (!Number.isSafeInteger(ms)) throw new Error(`--every 가 너무 크다: ${every}`);
33
+ return ms;
34
+ }
35
+
36
+ function untilMs(until: string): number {
37
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(until) || !Number.isFinite(Date.parse(`${until}T00:00:00Z`))
38
+ || new Date(`${until}T00:00:00Z`).toISOString().slice(0, 10) !== until) throw new Error(`--until 날짜 형식 오류: ${until}`);
39
+ return Date.parse(`${until}T00:00:00Z`);
40
+ }
41
+
42
+ export function expiredSource(source: RegisteredIntakeSource, now: Date): boolean {
43
+ return source.until !== undefined && now.getTime() >= untilMs(source.until);
44
+ }
45
+
46
+ export function sourceIsDue(source: RegisteredIntakeSource, now: Date): boolean {
47
+ return !expiredSource(source, now) && (source.lastRunAt === undefined
48
+ || Date.parse(source.lastRunAt) + cadenceMs(source.every) <= now.getTime());
49
+ }
50
+
51
+ export function sourcesFile(stateDir: string): string { return join(stateDir, 'intake', 'sources.json'); }
52
+
53
+ function readSources(stateDir: string): RegisteredIntakeSource[] {
54
+ const file = sourcesFile(stateDir);
55
+ if (!existsSync(file)) return [];
56
+ const parsed: unknown = JSON.parse(readFileSync(file, 'utf8'));
57
+ if (!Array.isArray(parsed)) throw new Error(`원천 등록부 모양 오류: ${file}`);
58
+ return parsed as RegisteredIntakeSource[];
59
+ }
60
+
61
+ function saveSources(stateDir: string, sources: RegisteredIntakeSource[]): void {
62
+ const file = sourcesFile(stateDir);
63
+ mkdirSync(join(stateDir, 'intake'), { recursive: true });
64
+ const temp = `${file}.${randomUUID()}.tmp`;
65
+ try {
66
+ const fd = openSync(temp, 'wx', 0o600);
67
+ try { writeFileSync(fd, JSON.stringify(sources, null, 2) + '\n'); } finally { closeSync(fd); }
68
+ chmodSync(temp, 0o600);
69
+ renameSync(temp, file);
70
+ } catch (error) {
71
+ if (existsSync(temp)) unlinkSync(temp);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ function commandArgv(spec: string): string[] {
77
+ // Shell syntax and interpreter wrappers are rejected; the allowed invocation uses execFileSync without a shell.
78
+ if (!spec.trim() || /["'\\`$;|&<>(){}\[\]*?\r\n]/.test(spec)) throw new Error('command spec 은 셸 구문 없이 실행 파일과 인자만 허용한다');
79
+ const argv = spec.trim().split(/\s+/);
80
+ // Allow-list, not deny-list (review R3): any other program (`nodejs scripts/x.ts`, a renamed wrapper) could emit
81
+ // private memos without a privacy mark. 1st edition: only the elanous CLI itself, whose intake commands mark privacy.
82
+ const executable = argv[0]!.split('/').pop()!.toLowerCase();
83
+ if (executable !== 'elanous' && executable !== 'eln') throw new Error('command 원천은 1판에서 elanous CLI(elanous·eln)만 받는다');
84
+ if (argv.some((arg) => /collect-telegram-saved/i.test(arg))) throw new Error('개인 메모(collect-telegram-saved)는 command 원천으로 등록할 수 없다');
85
+ return argv;
86
+ }
87
+
88
+ export function addSource(input: RegisteredIntakeSource, stateDir = effectiveInstanceRoot()): RegisteredIntakeSource {
89
+ const seat = canonicalSeat(input.seat);
90
+ if (!['github-query', 'rss', 'command'].includes(input.kind)) throw new Error(`알 수 없는 원천 종류: ${input.kind}`);
91
+ cadenceMs(input.every);
92
+ if (input.until !== undefined) untilMs(input.until);
93
+ if (!input.id?.trim() || !/^[\w.-]+$/.test(input.id)) throw new Error('원천 id 는 영문·숫자·_·-·. 만 허용한다');
94
+ if (!input.spec?.trim()) throw new Error('원천 spec 이 비었다');
95
+ if (input.kind === 'rss') {
96
+ let protocol = '';
97
+ try { protocol = new URL(input.spec).protocol; } catch { /* validation below */ }
98
+ if (protocol !== 'http:' && protocol !== 'https:') throw new Error('RSS spec 은 HTTP(S) URL 이어야 한다');
99
+ }
100
+ if (input.kind === 'command') commandArgv(input.spec);
101
+ const sources = readSources(stateDir);
102
+ if (sources.some((source) => source.id === input.id)) throw new Error(`이미 등록된 원천 id: ${input.id}`);
103
+ const source = { ...input, seat };
104
+ saveSources(stateDir, [...sources, source]);
105
+ return source;
106
+ }
107
+
108
+ export function listSources(filter: { seat?: string } = {}, stateDir = effectiveInstanceRoot()): RegisteredIntakeSource[] {
109
+ const seat = filter.seat === undefined ? undefined : canonicalSeat(filter.seat);
110
+ return readSources(stateDir).filter((source) => seat === undefined || source.seat === seat);
111
+ }
112
+
113
+ export function removeSource(id: string, stateDir = effectiveInstanceRoot()): boolean {
114
+ const sources = readSources(stateDir);
115
+ if (!sources.some((source) => source.id === id)) return false;
116
+ saveSources(stateDir, sources.filter((source) => source.id !== id));
117
+ return true;
118
+ }
119
+
120
+ export function dueSources(now: Date = new Date(), stateDir = effectiveInstanceRoot()): RegisteredIntakeSource[] {
121
+ return listSources({}, stateDir).filter((source) => sourceIsDue(source, now));
122
+ }
123
+
124
+ export interface RunDueDeps {
125
+ stateDir?: string;
126
+ searchGithub?: SearchGithubRepos;
127
+ fetchFeed?: (url: string) => Promise<string>;
128
+ runCommand?: (command: string) => Promise<string>;
129
+ ingest?: typeof ingestIntakeItems;
130
+ }
131
+
132
+ function xmlText(value: string): string {
133
+ return value.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').replace(/<[^>]+>/g, '')
134
+ .replace(/&(?:amp|lt|gt|quot|apos|#(\d+)|#x([a-f\d]+));/gi, (entity, dec: string | undefined, hex: string | undefined) => {
135
+ if (dec || hex) return String.fromCodePoint(Number.parseInt(dec ?? hex!, dec ? 10 : 16));
136
+ return ({ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&apos;': "'" } as Record<string, string>)[entity.toLowerCase()] ?? entity;
137
+ }).trim();
138
+ }
139
+
140
+ export function parseFeed(xml: string): RawIntakeItem[] {
141
+ // A login page or broken XML is a failure, not «0 items» — otherwise lastRunAt moves and the retry waits a whole cycle (review R3).
142
+ const head = xml.replace(/^\uFEFF/, '').replace(/<\?xml[^>]*\?>/i, '').replace(/<!--[\s\S]*?-->/g, '').trimStart();
143
+ if (!/^<(?:rss|feed|rdf:RDF)\b/i.test(head)) throw new Error('RSS/Atom 피드가 아니다(루트가 rss·feed·rdf:RDF 아님)');
144
+ const entries = xml.match(/<(?:item|entry)\b[^>]*>[\s\S]*?<\/(?:item|entry)>/gi) ?? [];
145
+ return entries.map((entry) => {
146
+ const field = (name: string): string => xmlText(entry.match(new RegExp(`<${name}\\b[^>]*>([\\s\\S]*?)<\\/${name}>`, 'i'))?.[1] ?? '');
147
+ const link = entry.match(/<link\b[^>]*\bhref=["']([^"']+)["'][^>]*\/?\s*>/i)?.[1] ?? field('link');
148
+ return { url: xmlText(link), title: field('title'), text: field('description') || field('summary') || field('content'), kind: 'rss' as const };
149
+ }).filter((entry) => !!entry.url);
150
+ }
151
+
152
+ export async function runDue({ now = new Date(), seat, deps = {} }: { now?: Date; seat?: string; deps?: RunDueDeps } = {}): Promise<{ ran: Array<{ id: string; items: number; error?: string }> }> {
153
+ const stateDir = deps.stateDir ?? effectiveInstanceRoot();
154
+ const selectedSeat = seat === undefined ? undefined : canonicalSeat(seat);
155
+ const ran: Array<{ id: string; items: number; error?: string }> = [];
156
+ for (const source of dueSources(now, stateDir).filter((source) => selectedSeat === undefined || source.seat === selectedSeat)) {
157
+ try {
158
+ let raws: RawIntakeItem[];
159
+ let githubSnapshots: { day: string; repo: string; stars: number }[] | undefined;
160
+ let intakeSource: IntakeSource | RegisteredIntakeSourceKind;
161
+ if (source.kind === 'github-query') {
162
+ const collected = await collectGithubStars(stateDir, deps.searchGithub ?? ghApiSearchRepos, {
163
+ queries: [source.spec], day: now.toISOString().slice(0, 10), dryRun: true,
164
+ });
165
+ githubSnapshots = collected.repos.map((repo) => ({ day: collected.day, repo: repo.repo, stars: repo.stars }));
166
+ raws = collected.repos.map((repo) => ({ url: repo.url, title: repo.title, text: repo.text, kind: 'repo',
167
+ signals: { stars: repo.stars, ...(repo.starsDelta === undefined ? {} : { starsDelta: repo.starsDelta }),
168
+ ...(repo.starsPerDay === undefined ? {} : { starsPerDay: repo.starsPerDay }) } }));
169
+ intakeSource = 'github';
170
+ } else if (source.kind === 'rss') {
171
+ const feed = await (deps.fetchFeed ?? (async (url: string) => {
172
+ const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
173
+ if (!response.ok) throw new Error(`RSS HTTP ${response.status}`);
174
+ return response.text();
175
+ }))(source.spec);
176
+ raws = parseFeed(feed);
177
+ intakeSource = 'rss';
178
+ } else {
179
+ const argv = commandArgv(source.spec);
180
+ const stdout = await (deps.runCommand ?? (async () => execFileSync(argv[0]!, argv.slice(1), { encoding: 'utf8', timeout: 600_000, maxBuffer: 16 * 1024 * 1024 })))(source.spec);
181
+ const parsed = parseRawIntakeJsonl(stdout);
182
+ if (parsed.bad) throw new Error(`잘못된 JSONL ${parsed.bad}줄`);
183
+ if (parsed.raws.some((raw) => (raw as RawIntakeItem & { privacy?: string; source?: string }).privacy === 'user-private'
184
+ || ['memo', 'telegram-saved'].includes((raw as RawIntakeItem & { source?: string }).source ?? ''))) {
185
+ throw new Error('개인 메모(user-private)는 command 원천에서 받을 수 없다');
186
+ }
187
+ raws = parsed.raws;
188
+ intakeSource = 'command';
189
+ }
190
+ const items = (deps.ingest ?? ingestIntakeItems)(stateDir, intakeSource, raws.map((raw) => ({ ...raw, seat: source.seat })), now.toISOString()).shaped;
191
+ if (githubSnapshots) appendGithubStarSnapshots(stateDir, githubSnapshots);
192
+ // Re-read after each run so one failure cannot overwrite another source's successful timestamp.
193
+ const current = readSources(stateDir);
194
+ const found = current.find((entry) => entry.id === source.id);
195
+ if (found) saveSources(stateDir, current.map((entry) => entry.id === source.id ? { ...entry, lastRunAt: now.toISOString() } : entry));
196
+ ran.push({ id: source.id, items });
197
+ } catch (error) {
198
+ ran.push({ id: source.id, items: 0, error: error instanceof Error ? error.message : String(error) });
199
+ }
200
+ }
201
+ return { ran };
202
+ }
@@ -4,19 +4,22 @@ import { createHash } from 'node:crypto';
4
4
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { debug } from '../debug/log.js';
7
+ import { findTrack } from '../autopilot/mission-phase-track.js';
7
8
 
8
9
  export const INTAKE_SOURCES = ['x', 'youtube', 'github', 'telegram-saved', 'telegram-bot', 'memo', 'pwa'] as const;
10
+ export type RegisteredIntakeSourceKind = 'rss' | 'command';
9
11
  export type IntakeSource = typeof INTAKE_SOURCES[number];
10
- export type IntakeKind = 'video' | 'repo' | 'post' | 'article' | 'note';
12
+ export type IntakeKind = 'video' | 'repo' | 'post' | 'article' | 'note' | 'rss';
11
13
  export type IntakePrivacy = 'public' | 'user-private';
12
14
  export const INTAKE_STATUSES = ['new', 'queued', 'absorbed', 'checked', 'routed', 'discarded', 'deferred'] as const;
13
15
  export type IntakeStatus = typeof INTAKE_STATUSES[number];
14
16
 
15
17
  export interface IntakeItem {
16
18
  id: string;
17
- source: IntakeSource;
19
+ source: IntakeSource | RegisteredIntakeSourceKind;
18
20
  /** 같은 항목이 다른 입력원에서도 왔으면 여기 모인다 — «두 입력원에서 동시에 뜬 것»이 신호다. */
19
- sources: IntakeSource[];
21
+ sources: (IntakeSource | RegisteredIntakeSourceKind)[];
22
+ seat?: string;
20
23
  url?: string;
21
24
  kind: IntakeKind;
22
25
  title?: string;
@@ -34,6 +37,7 @@ export interface IntakeItem {
34
37
 
35
38
  /** 수집기가 내는 한 줄 — 모양 맞추기 전. */
36
39
  export interface RawIntakeItem {
40
+ seat?: string;
37
41
  url?: string;
38
42
  title?: string;
39
43
  text?: string;
@@ -76,13 +80,23 @@ export function canonicalUrl(raw: string): string | undefined {
76
80
  return `https://${host}${path}${q}`;
77
81
  }
78
82
 
79
- export function intakeItemId(source: IntakeSource, raw: Pick<RawIntakeItem, 'url' | 'text'>): string {
83
+ function normalizedSeat(seat: unknown): string | undefined {
84
+ if (seat === undefined) return undefined;
85
+ if (typeof seat !== 'string') throw new Error(`알 수 없는 자리: ${String(seat)}`);
86
+ const track = findTrack(seat.trim());
87
+ if (!track) throw new Error(`알 수 없는 자리: ${seat}`);
88
+ return track.id;
89
+ }
90
+
91
+ export function intakeItemId(source: IntakeSource | RegisteredIntakeSourceKind, raw: Pick<RawIntakeItem, 'url' | 'text'> & { seat?: string }): string {
92
+ const seat = normalizedSeat(raw.seat);
80
93
  const url = raw.url ? canonicalUrl(raw.url) : undefined;
81
94
  const basis = url ?? `${source}:${(raw.text ?? '').trim().slice(0, 200)}`;
82
- return createHash('sha256').update(basis).digest('hex').slice(0, 16);
95
+ // Unassigned items retain their original IDs; assigned items have an independent lifecycle per seat.
96
+ return createHash('sha256').update(seat ? JSON.stringify([seat, basis]) : basis).digest('hex').slice(0, 16);
83
97
  }
84
98
 
85
- function kindFor(source: IntakeSource, url: string | undefined): IntakeKind {
99
+ function kindFor(source: IntakeSource | RegisteredIntakeSourceKind, url: string | undefined): IntakeKind {
86
100
  if (url?.startsWith('https://www.youtube.com/')) return 'video';
87
101
  if (url?.startsWith('https://github.com/')) return 'repo';
88
102
  if (url?.startsWith('https://x.com/')) return 'post';
@@ -91,12 +105,13 @@ function kindFor(source: IntakeSource, url: string | undefined): IntakeKind {
91
105
  }
92
106
 
93
107
  /** 개인 메모 입력원은 user-private — 원문을 바깥 LLM 에 보내지 않는다(RFC §3.4). PWA 칸에 사람이 쓴 글도 같다(RFC-pwa-intake-front-door). */
94
- export function privacyFor(source: IntakeSource): IntakePrivacy {
108
+ export function privacyFor(source: IntakeSource | RegisteredIntakeSourceKind): IntakePrivacy {
95
109
  return source === 'telegram-saved' || source === 'memo' || source === 'pwa' ? 'user-private' : 'public';
96
110
  }
97
111
 
98
112
  /** ① 모양 맞추기 — 수집기 한 줄 → IntakeItem. 비어 있으면(URL·본문 둘 다 없음) null. */
99
- export function shapeIntakeItem(source: IntakeSource, raw: RawIntakeItem, now: string): IntakeItem | null {
113
+ export function shapeIntakeItem(source: IntakeSource | RegisteredIntakeSourceKind, raw: RawIntakeItem, now: string): IntakeItem | null {
114
+ const seat = normalizedSeat(raw.seat);
100
115
  const url = raw.url ? canonicalUrl(raw.url) : undefined;
101
116
  const text = raw.text?.trim() || undefined;
102
117
  if (!url && !text) return null;
@@ -104,9 +119,10 @@ export function shapeIntakeItem(source: IntakeSource, raw: RawIntakeItem, now: s
104
119
  const signals: Record<string, number> = {};
105
120
  for (const [k, v] of Object.entries(raw.signals ?? {})) if (Number.isFinite(v)) signals[`${source}.${k}`] = v;
106
121
  return {
107
- id: intakeItemId(source, { url, text }),
122
+ id: intakeItemId(source, { url, text, seat }),
108
123
  source,
109
124
  sources: [source],
125
+ ...(seat ? { seat } : {}),
110
126
  ...(url ? { url } : {}),
111
127
  kind: raw.kind ?? kindFor(source, url),
112
128
  ...(raw.title?.trim() ? { title: raw.title.trim().slice(0, 300) } : {}),
@@ -155,7 +171,7 @@ const SETTLED: ReadonlySet<IntakeStatus> = new Set(['absorbed', 'discarded', 'ro
155
171
  * ② 중복 없애기 — 같은 id 는 새 항목을 만들지 않고 신호·입력원을 합친다.
156
172
  * 이미 흡수·버림·갈래가 끝난 것은 «seen» 으로 세고 상태를 되돌리지 않는다(다시 올리지 않는다).
157
173
  */
158
- export function ingestIntakeItems(instanceRoot: string, source: IntakeSource, raws: RawIntakeItem[], now = new Date().toISOString(),
174
+ export function ingestIntakeItems(instanceRoot: string, source: IntakeSource | RegisteredIntakeSourceKind, raws: RawIntakeItem[], now = new Date().toISOString(),
159
175
  log: (event: string, data: Record<string, unknown>) => void = (e, d) => { debug.log('intake.normalize', e, d); }): IngestResult {
160
176
  const { items: existing, badLines } = loadIntakeLedger(instanceRoot);
161
177
  const result: IngestResult = { shaped: 0, added: 0, merged: 0, seen: 0, skipped: 0, badLines };
@@ -203,9 +219,9 @@ export function markIntakeItem(instanceRoot: string, id: string, patch: { status
203
219
  return true;
204
220
  }
205
221
 
206
- export function listIntakeItems(instanceRoot: string, filter: { status?: IntakeStatus; source?: IntakeSource } = {}): IntakeItem[] {
222
+ export function listIntakeItems(instanceRoot: string, filter: { status?: IntakeStatus; source?: IntakeSource; seat?: string } = {}): IntakeItem[] {
207
223
  return [...loadIntakeLedger(instanceRoot).items.values()]
208
- .filter((i) => (!filter.status || i.status === filter.status) && (!filter.source || i.sources.includes(filter.source)))
224
+ .filter((i) => (!filter.status || i.status === filter.status) && (!filter.source || i.sources.includes(filter.source)) && (!filter.seat || i.seat === filter.seat))
209
225
  .sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
210
226
  }
211
227
 
@@ -0,0 +1,207 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import type { MissionOrigin } from '../autopilot/mission-origin.js';
6
+ import { debug } from '../debug/log.js';
7
+ import { sendOutbound } from '../domains/outbound-alert.js';
8
+ import { defaultSpillUpload } from '../storage/content-spill.js';
9
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
10
+ import type { ExecRequest } from '../exec-requests/store.js';
11
+ import { resolveDaemonEndpoint } from '../nexus/daemon-endpoint.js';
12
+ import { parseSeatAddress, resolveSeat } from '../seat-address/seat-address.js';
13
+
14
+ const POLL_MS = 5_000;
15
+ const TIMEOUT_MS = 30 * 60_000;
16
+ const DOC = /전략|한\s*장|원페이저|기획|보도자료|글|메모|초안|\b(?:one-pager|strategy|post|memo|draft)\b/i;
17
+ const CODE = /구현|버그|고쳐|\b(?:fix|implement|PR)\b/i;
18
+ const RESEARCH = /전략|시장|경쟁|\bstrategy\b/i;
19
+ /** EV12a — a question or a request for status / a report is answered, never turned into a code goal. */
20
+ const ASK = /상황|현황|진행|준비|어때|어떻게|알려|보고|정리|요약|공유|\?|?|\b(?:status|update|report|summary|summari[sz]e|how|what)\b/i;
21
+ /** Only an explicit build/fix verb makes a question a code request («버그 현황 알려줘» is still a report). */
22
+ const CODE_ACTION = /구현|고쳐|수정해|만들어\s*줘|\b(?:fix|implement)\b/i;
23
+
24
+ type ExecSnapshot = Pick<ExecRequest, 'status' | 'summary' | 'results'> & { seats?: ExecRequest['seats'] };
25
+ type Source = { title: string; url: string };
26
+
27
+ export interface SeatDocDeps {
28
+ research?: (body: string) => Promise<Source[]>;
29
+ submitExec?: (text: string) => Promise<{ id: string }>;
30
+ getExec?: (id: string) => Promise<ExecSnapshot | null>;
31
+ getFile?: (path: string) => Promise<string>;
32
+ publishFile?: (content: string, key: string) => string | null;
33
+ sendOutbound?: typeof sendOutbound;
34
+ sleep?: (ms: number) => Promise<void>;
35
+ now?: () => number;
36
+ pwaUrl?: () => string | undefined;
37
+ log?: (event: 'submitted' | 'research' | 'settled' | 'delivered' | 'delivery-failed', data: Record<string, unknown>) => void;
38
+ }
39
+
40
+ export function isSeatDocRequest(text: string): boolean {
41
+ const addressed = parseSeatAddress(text);
42
+ if (!addressed || !addressed.seats.every(seat => !!resolveSeat(seat))) return false;
43
+ if (DOC.test(addressed.body) && !CODE.test(addressed.body)) return true;
44
+ return ASK.test(addressed.body) && !CODE_ACTION.test(addressed.body);
45
+ }
46
+
47
+ /** CO1 — anything addressed to the COO seat that is not a code request goes to the planner, which splits it across
48
+ * seats (the A5 exec-request path), instead of becoming a code goal. */
49
+ export function isCooPlannerRequest(text: string): boolean {
50
+ const addressed = parseSeatAddress(text);
51
+ return !!addressed && addressed.seats.some(seat => resolveSeat(seat)?.title === 'COO')
52
+ && addressed.seats.every(seat => !!resolveSeat(seat)) && !!addressed.body.trim() && !CODE.test(addressed.body);
53
+ }
54
+
55
+ async function research(body: string): Promise<Source[]> {
56
+ const cli = fileURLToPath(new URL('../../bin/elanous.mjs', import.meta.url));
57
+ const child = Bun.spawn([process.execPath, cli, '--test', 'research', '--json', '--limit', '5', body], {
58
+ stdout: 'pipe', stderr: 'ignore',
59
+ });
60
+ const [out, code] = await Promise.all([new Response(child.stdout).text(), child.exited]);
61
+ if (code !== 0) throw new Error(`research exit ${code}`);
62
+ const payload = JSON.parse(out) as { output?: unknown; metadata?: { totalHits?: number; perEngine?: Record<string, { error?: string }> } };
63
+ if (typeof payload.output !== 'string') throw new Error('research JSON output missing');
64
+ const sources = [...payload.output.matchAll(/^- \[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/gm)]
65
+ .slice(0, 5).map(([, title, url]) => ({ title: title!, url: url! }));
66
+ if (!sources.length && (!payload.metadata?.totalHits || Object.values(payload.metadata.perEngine ?? {}).some(engine => engine.error))) {
67
+ throw new Error('research unavailable');
68
+ }
69
+ return sources;
70
+ }
71
+
72
+ function daemonUrl(): string {
73
+ const base = resolveDaemonEndpoint({ purpose: 'write' })?.baseUrl;
74
+ if (!base) throw new Error('실행 요청 데몬을 찾을 수 없습니다');
75
+ return base;
76
+ }
77
+
78
+ async function execResponse(path: string, text?: string): Promise<Response> {
79
+ const token = readFileSync(join(getElanousConfigDir(), 'acp-token'), 'utf8').trim();
80
+ const response = await fetch(`${daemonUrl()}/v1/exec-requests${path}`, {
81
+ method: text === undefined ? 'GET' : 'POST',
82
+ headers: { Authorization: `Bearer ${token}`, ...(text === undefined ? {} : { 'Content-Type': 'application/json' }) },
83
+ ...(text === undefined ? {} : { body: JSON.stringify({ text }) }),
84
+ });
85
+ if (!response.ok) throw new Error(`실행 요청 HTTP ${response.status}`);
86
+ return response;
87
+ }
88
+
89
+ async function execApi(path: string, text?: string): Promise<ExecRequest> {
90
+ return await (await execResponse(path, text)).json() as ExecRequest;
91
+ }
92
+
93
+ async function execFile(path: string): Promise<string> {
94
+ return (await execResponse(path.slice('/v1/exec-requests'.length))).text();
95
+ }
96
+
97
+ function chatPwaUrl(): string | undefined {
98
+ const endpoint = resolveDaemonEndpoint({ purpose: 'write' });
99
+ return endpoint?.pwaUrl;
100
+ }
101
+
102
+ function accessiblePwaUrl(value: string | undefined): string | undefined {
103
+ if (!value) return undefined;
104
+ try {
105
+ const url = new URL(value);
106
+ // The owner opens the authenticated PWA, not the bearer-only /v1/exec-requests/files endpoint.
107
+ // A loopback address works on the daemon host, not on the sender's phone.
108
+ if (url.protocol !== 'https:' || !url.hostname || url.hostname === 'localhost'
109
+ || url.hostname === '127.0.0.1' || url.hostname === '0.0.0.0'
110
+ || url.username || url.password || url.pathname !== '/app/' || url.search || url.hash) return undefined;
111
+ return url.toString();
112
+ } catch { return undefined; }
113
+ }
114
+
115
+ async function fileLinks(id: string, result: ExecSnapshot, deps: SeatDocDeps): Promise<Map<string, string>> {
116
+ const links = new Map<string, string>();
117
+ for (const entry of result.results) {
118
+ if (entry.kind !== 'report' && entry.kind !== 'text') continue;
119
+ if (!entry.url.startsWith(`/v1/exec-requests/${encodeURIComponent(id)}/files/`)) continue;
120
+ try {
121
+ // The authenticated daemon route is the only source of bytes; a raw file path is never trusted.
122
+ const content = await (deps.getFile ?? execFile)(entry.url);
123
+ const ext = entry.kind === 'report' ? 'md' : 'txt';
124
+ const digest = createHash('sha256').update(content).digest('hex');
125
+ const key = `seat-doc/${encodeURIComponent(id)}/${digest}.${ext}`;
126
+ const url = (deps.publishFile ?? ((value, path) => defaultSpillUpload(value, path, ext)))(content, key);
127
+ if (url && /^https:\/\/[^/]+/.test(url)) links.set(entry.url, url);
128
+ else links.set(entry.url, `\n${content}`);
129
+ } catch (error) {
130
+ debug.log('intake.seat-doc', 'file-link-failed', { id, reason: String(error).slice(0, 200) });
131
+ }
132
+ }
133
+ return links;
134
+ }
135
+
136
+ function message(id: string, result: ExecSnapshot | null, withoutResearch: boolean, lookupAvailable: boolean, pwaUrl?: string, links = new Map<string, string>()): string {
137
+ const suffix = withoutResearch ? '\n조사 없이 진행했습니다.' : '';
138
+ const location = pwaUrl ? `PWA 에서 확인: ${pwaUrl} (접수번호 ${id})` : `PWA 에서 확인 (접수번호 ${id} · 외부 접속 주소 없음)`;
139
+ if (!result) return `${lookupAvailable ? '아직 도는 중' : '상태를 확인할 수 없어 확인을 종료했습니다'} · ${location}${suffix}`;
140
+ if (result.status === 'failed') {
141
+ const reason = result.summary || result.seats?.find(seat => seat.reason)?.reason || '실행 요청 실패';
142
+ return `문서 요청 실패 — ${reason.split('\n')[0]!.slice(0, 200)} · ${location}${suffix}`;
143
+ }
144
+ const summary = result.summary?.split('\n')[0]?.slice(0, 200) || '문서 요청 완료';
145
+ // File endpoints require an Authorization header even when opened from the same origin.
146
+ // Only send externally usable links for genuinely external artifacts; file names remain findable in the PWA.
147
+ const artifacts = result.results.map(entry => `${entry.title} (${entry.kind})${links.has(entry.url) ? ` · ${links.get(entry.url)}` : entry.kind === 'link' && /^https?:\/\//i.test(entry.url) ? ` · ${entry.url}` : ''}`);
148
+ return `${summary}\n${artifacts.join('\n')}${artifacts.length ? '\n' : ''}${location}${suffix}`;
149
+ }
150
+
151
+ export async function submitSeatDocRequest({ text, reportTo, deps = {} }: {
152
+ text: string; reportTo?: MissionOrigin; deps?: SeatDocDeps;
153
+ }): Promise<{ id: string }> {
154
+ const log = deps.log ?? ((event: Parameters<NonNullable<SeatDocDeps['log']>>[0], data: Record<string, unknown>) => debug.log('intake.seat-doc', event, data));
155
+ const body = parseSeatAddress(text)?.body.trim() ?? text.trim();
156
+ let withoutResearch = false;
157
+ let requestText = text;
158
+ if (RESEARCH.test(body)) {
159
+ try {
160
+ const sources = (await (deps.research ?? research)(body)).slice(0, 5);
161
+ if (sources.length) requestText += `\n\n참고 자료\n${sources.map(source => `${source.title} · ${source.url}`).join('\n')}`;
162
+ log('research', { count: sources.length });
163
+ } catch (error) {
164
+ withoutResearch = true;
165
+ log('research', { failed: true, reason: String(error).slice(0, 200) });
166
+ }
167
+ }
168
+ const { id } = await (deps.submitExec ?? ((value) => execApi('', value)))(requestText);
169
+ if (!id) throw new Error('실행 요청 접수번호가 없습니다');
170
+ log('submitted', { id });
171
+
172
+ const watch = async () => {
173
+ const now = deps.now ?? Date.now;
174
+ const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
175
+ const deadline = now() + TIMEOUT_MS;
176
+ let result: ExecSnapshot | null = null;
177
+ let lookupAvailable = false;
178
+ let lastConfirmedStatus: ExecSnapshot['status'] | undefined;
179
+ while (now() < deadline) {
180
+ try {
181
+ const snapshot = await (deps.getExec ?? ((key) => execApi(`/${encodeURIComponent(key)}`)))(id);
182
+ if (!snapshot) throw new Error('실행 요청을 찾을 수 없습니다');
183
+ lookupAvailable = true;
184
+ lastConfirmedStatus = snapshot.status;
185
+ if (snapshot.status === 'done' || snapshot.status === 'failed') {
186
+ result = snapshot;
187
+ break;
188
+ }
189
+ } catch (error) {
190
+ lookupAvailable = false;
191
+ debug.log('intake.seat-doc', 'poll-error', { id, reason: String(error).slice(0, 200) });
192
+ }
193
+ if (now() < deadline) await sleep(Math.min(POLL_MS, Math.max(0, deadline - now())));
194
+ }
195
+ log('settled', { id, status: result?.status ?? (lookupAvailable ? 'timeout' : 'lookup-unavailable'), lastConfirmedStatus });
196
+ const links = result?.status === 'done' ? await fileLinks(id, result, deps) : new Map<string, string>();
197
+ try {
198
+ const sent = await (deps.sendOutbound ?? sendOutbound)(message(id, result, withoutResearch, lookupAvailable,
199
+ accessiblePwaUrl((deps.pwaUrl ?? chatPwaUrl)()), links), 'report', reportTo);
200
+ log(sent ? 'delivered' : 'delivery-failed', { id });
201
+ } catch (error) {
202
+ log('delivery-failed', { id, reason: String(error).slice(0, 200) });
203
+ }
204
+ };
205
+ void watch();
206
+ return { id };
207
+ }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * - absorb → 흡수 원장(`ingestIntakeItems`) — URL 이 있으면 URL 마다, 없으면 글 한 덩이(PWA `absorbItemsFromText` 와 같은 규칙).
6
6
  * - tasks → TOX(`dispatchTaskCreate`) — 외부 출처(텔레그램 등)는 `external` 로 실어 M1(backlog ⊕ 승인 대기)을 탄다.
7
- * - graph → 하니스 ask(`handleHarnessAskPost` 를 요청 하나로 부른다 — 흐름을 복제하지 않는다).
7
+ * - graph → 자리 문서 요청은 A5 실행 요청, 나머지는 하니스 ask(`handleHarnessAskPost`).
8
8
  * - ask-human → 실행하지 않는다. 고를 갈래를 돌려준다.
9
9
  *
10
10
  * 로그엔 원문이 아니라 길이만 남긴다(앞문 RFC · `user-private`).
@@ -16,6 +16,7 @@ import type { ExternalTaskSource } from '../task-orchestrator/types.js';
16
16
  import { TASK_DEFAULTS } from '../task-orchestrator/types.js';
17
17
  import { decideIntakeFrontRoute, type IntakeFrontRouteDecision } from './front-route-rules.js';
18
18
  import { ingestIntakeItems, intakeItemId, type IntakeSource, type RawIntakeItem } from './items.js';
19
+ import { isCooPlannerRequest, isSeatDocRequest, submitSeatDocRequest, type SeatDocDeps } from './seat-doc-route.js';
19
20
 
20
21
  export type IntakeWorkTrack = 'absorb' | 'tasks' | 'graph';
21
22
 
@@ -40,6 +41,8 @@ export interface SubmitIntakeWorkDeps {
40
41
  ingest?: typeof ingestIntakeItems;
41
42
  createTask?: (input: CreateTaskInput) => Promise<{ taskId?: string; deduplicated?: boolean; output: string }>;
42
43
  askHarness?: (text: string, origin?: MissionOrigin) => Promise<{ acceptanceId?: string; error?: string }>;
44
+ seatDoc?: typeof submitSeatDocRequest;
45
+ seatDocDeps?: SeatDocDeps;
43
46
  handleHarnessAskPost?: typeof import('../nexus/api/harness-api.js').handleHarnessAskPost;
44
47
  log?: (event: string, data: Record<string, unknown>) => void;
45
48
  }
@@ -130,6 +133,11 @@ export async function submitIntakeWork(
130
133
  return { ok: true, track: 'tasks', taskId: res.taskId, deduplicated: res.deduplicated ?? false };
131
134
  }
132
135
  const reportTo = input.origin.kind === 'external' ? input.origin.reportTo : undefined;
136
+ if (isSeatDocRequest(text) || isCooPlannerRequest(text)) {
137
+ const { id } = await (deps.seatDoc ?? submitSeatDocRequest)({ text, reportTo, deps: deps.seatDocDeps });
138
+ log('submitted', { ...base, acceptanceId: id });
139
+ return { ok: true, track: 'graph', acceptanceId: id };
140
+ }
133
141
  const res = await (deps.askHarness ? deps.askHarness(text, reportTo) : defaultAskHarness(text, reportTo, deps.handleHarnessAskPost));
134
142
  if (!res.acceptanceId) {
135
143
  log('failed', { ...base, reason: res.error ?? 'no-acceptance-id' });
@@ -0,0 +1,72 @@
1
+ import { debug } from '../debug/log.js';
2
+ import { parseSeatAddress, resolveSeat } from '../seat-address/seat-address.js';
3
+ import {
4
+ submitIntakeWork,
5
+ type SubmitIntakeWorkDeps,
6
+ } from './submit-intake-work.js';
7
+ import type { TelegramWorkMessage } from './telegram-work.js';
8
+
9
+ export interface TelegramSeatWorkDeps extends SubmitIntakeWorkDeps {
10
+ submit?: typeof submitIntakeWork;
11
+ }
12
+
13
+ /** Addressed Telegram work goes through graph intake, not the chat turn. */
14
+ export async function handleTelegramSeatWork(
15
+ text: string,
16
+ msg: TelegramWorkMessage,
17
+ deps: TelegramSeatWorkDeps = {},
18
+ ): Promise<string | null> {
19
+ if (!text.startsWith('@')) return null;
20
+ const address = parseSeatAddress(text);
21
+ if (!address) return null;
22
+
23
+ const seats = address.seats.map((name) => ({ name, seat: resolveSeat(name) }));
24
+ const names = address.seats;
25
+ debug.log('seat-address.telegram', 'parsed', { seats: names });
26
+ const unknown = seats.filter(({ seat }) => !seat).map(({ name }) => `@${name}`);
27
+ if (unknown.length) {
28
+ debug.log('seat-address.telegram', 'rejected', { seats: names, reason: 'unknown-seat' });
29
+ return `어느 좌석을 말씀하시나요? ${unknown.join(', ')}은(는) 등록된 좌석이 아닙니다. 좌석을 확인해 다시 보내 주세요.`;
30
+ }
31
+ const body = address.body.trim();
32
+ if (!body) {
33
+ debug.log('seat-address.telegram', 'rejected', { seats: names, reason: 'empty-body' });
34
+ return '어떤 일을 맡길까요? 좌석 주소 뒤에 요청 내용을 적어 다시 보내 주세요.';
35
+ }
36
+
37
+ const reportTo = {
38
+ channel: 'telegram' as const,
39
+ chatId: msg.chatId,
40
+ ...(msg.botId ? { botId: msg.botId } : {}),
41
+ ...(msg.threadId ? { threadId: msg.threadId } : {}),
42
+ };
43
+ const replies: string[] = [];
44
+ for (const { seat } of seats) {
45
+ if (!seat) continue;
46
+ const label = seat.title ?? seat.id;
47
+ let result: Awaited<ReturnType<typeof submitIntakeWork>>;
48
+ try {
49
+ result = await (deps.submit ?? submitIntakeWork)({
50
+ text: `@${label} ${body}`,
51
+ track: 'graph',
52
+ origin: {
53
+ kind: 'external', ledgerSource: 'telegram-bot', provider: 'telegram',
54
+ ref: `telegram:${msg.chatId}:${msg.messageId}`, reportTo,
55
+ },
56
+ }, deps);
57
+ } catch (error) {
58
+ // 접수 문이 던져도 사람은 같은 대화에서 결과를 받는다(리뷰 must-fix: 예외 시 응답 누락).
59
+ debug.log('seat-address.telegram', 'rejected', { seats: [label], reason: 'submission-threw' });
60
+ replies.push(`@${label} 접수 실패 — ${String(error instanceof Error ? error.message : error).split('\n')[0]!.slice(0, 200)}`);
61
+ continue;
62
+ }
63
+ if (result.ok && result.track === 'graph') {
64
+ debug.log('seat-address.telegram', 'enqueued', { seats: [label] });
65
+ replies.push(`@${label} 접수번호: ${result.acceptanceId}`);
66
+ } else {
67
+ debug.log('seat-address.telegram', 'rejected', { seats: [label], reason: 'submission-failed' });
68
+ replies.push(`@${label} 접수 실패 — ${result.ok ? '접수번호가 없습니다' : result.reason.split('\n')[0]!.slice(0, 200)}`);
69
+ }
70
+ }
71
+ return replies.join('\n');
72
+ }