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
@@ -11,6 +11,7 @@ import { getUserConfig } from '../../user-config.js';
11
11
  import { debug } from '../../debug/log.js';
12
12
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
13
13
  import { collectPodArtifacts } from './pod-artifact-return.js';
14
+ import { podBunCacheVolume } from './pod-bun-cache.js';
14
15
  import { type PodPoolMember, PodPoolScheduler, resolvePodPoolSpec, parsePodPool, checkPodPool, syncPoolImages, type PoolKubectl } from './pod-pool.js';
15
16
  import { readSkillEnvFiles } from './pod-skills.js';
16
17
  import { podSourceScript, type PodSource } from './pod-source-receive.js';
@@ -76,6 +77,7 @@ export interface PodCommandJobInput {
76
77
  /** 저장소를 clone 한다 — 비공개 저장소라 GitHub 토큰이 Secret 으로 간다(명시 opt-in). 기본은 clone 없이 `~/work` 에서 이미지의 `elanous` 로 돈다. */
77
78
  clone?: boolean;
78
79
  hostMirror?: string;
80
+ bunCache?: string;
79
81
  /** 컨테이너 메모리 한도(기본 16Gi) — 게이트가 파일 하나 격리 Job 에 올린다. */
80
82
  memoryLimit?: string;
81
83
  source?: PodSource;
@@ -94,6 +96,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
94
96
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) throw new Error(`pod command skill name rejected: ${name}`);
95
97
  }
96
98
  const grok = o.llm === 'grok';
99
+ const bunCache = o.bunCache ? podBunCacheVolume(o.bunCache) : undefined;
97
100
  const quotedArgs = o.command.map(bashSingleQuote).join(' ');
98
101
  const credLines = [
99
102
  ...(grok ? ['mkdir -p ~/.grok && install -m 600 /creds/grok-auth.json ~/.grok/auth.json'] : []),
@@ -108,6 +111,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
108
111
  ...(o.clone
109
112
  ? ['export GH_TOKEN="$(cat /creds/gh-token)" && gh auth setup-git', podSourceScript(o.source ?? { kind: 'default' }, o.repoUrl)]
110
113
  : ['mkdir -p ~/work && cd ~/work']),
114
+ ...(bunCache ? [bunCache.shellPrefix] : []),
111
115
  `set -- ${quotedArgs}`,
112
116
  '"$@"',
113
117
  'rc=$?',
@@ -138,14 +142,16 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
138
142
  { name: 'ELANOUS_SUBSTRATE', value: 'pod' },
139
143
  { name: 'ELANOUS_POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } },
140
144
  ],
141
- ...(secretKeys || o.hostMirror ? { volumeMounts: [
145
+ ...(secretKeys || o.hostMirror || bunCache ? { volumeMounts: [
142
146
  ...(secretKeys ? [{ name: 'creds', mountPath: '/creds', readOnly: true }] : []),
143
147
  ...(o.hostMirror ? [{ name: 'host-mirror', mountPath: '/host-mirror', readOnly: true }] : []),
148
+ ...(bunCache ? [bunCache.volumeMount] : []),
144
149
  ] } : {}),
145
150
  }],
146
- ...(secretKeys || o.hostMirror ? { volumes: [
151
+ ...(secretKeys || o.hostMirror || bunCache ? { volumes: [
147
152
  ...(secretKeys ? [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }] : []),
148
153
  ...(o.hostMirror ? [{ name: 'host-mirror', hostPath: { path: o.hostMirror, type: 'Directory' } }] : []),
154
+ ...(bunCache ? [bunCache.volume] : []),
149
155
  ] } : {}),
150
156
  },
151
157
  },
@@ -201,6 +207,7 @@ export interface RunPodCommandOptions {
201
207
  llm?: 'grok';
202
208
  clone?: boolean;
203
209
  hostMirror?: string;
210
+ bunCache?: string;
204
211
  /** 컨테이너 메모리 한도(기본 16Gi). */
205
212
  memoryLimit?: string;
206
213
  source?: PodSource;
@@ -263,6 +270,8 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
263
270
  const hostMirror = (options.hostMirror ?? env.ELANOUS_POD_HOST_MIRROR
264
271
  ?? (options.configHostMirror ?? (() => getUserConfig().pod?.hostMirror))())?.trim() || undefined;
265
272
  if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
273
+ const bunCache = options.bunCache?.trim() || undefined;
274
+ if (bunCache && !isAbsolute(bunCache)) throw new Error('pod.bunCache must be an absolute directory path');
266
275
 
267
276
  let member: PodPoolMember | null = null;
268
277
  let pool = options.poolScheduler;
@@ -328,7 +337,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
328
337
  name, namespace, image: member?.imageRef ?? image,
329
338
  ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}),
330
339
  repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
331
- ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(options.memoryLimit ? { memoryLimit: options.memoryLimit } : {}), deadlineSeconds,
340
+ ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(options.memoryLimit ? { memoryLimit: options.memoryLimit } : {}), deadlineSeconds,
332
341
  ...(options.runId ? { runId: options.runId } : {}),
333
342
  });
334
343
  kubectl(['-n', namespace, 'delete', 'job', name, '--ignore-not-found']);
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } fr
2
2
  import { gunzipSync } from 'node:zlib';
3
3
  import { debug } from '../../debug/log.js';
4
4
  import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
5
+ import { ledgerLineToLogEvent } from './pod-ledger-events.js';
5
6
 
6
7
  type Ledger = { runId: string; jsonl: string };
7
8
  type Incomplete = { runId: string; reason: string };
@@ -124,7 +125,7 @@ export function createPodLedgerFollower(opts: {
124
125
  let owned = false;
125
126
  let disabled = false;
126
127
  let lastProgressAt = now();
127
- let lastProgressEvent: string | null = null;
128
+ let visibleProgressEvent: string | null = null;
128
129
  let lastActivityMeasuredAt = lastProgressAt;
129
130
  let lastActivityLogAt = -Infinity;
130
131
  let nextStallMinute = stallMinutes;
@@ -132,11 +133,11 @@ export function createPodLedgerFollower(opts: {
132
133
  function checkStall(ledgerRead: boolean, activityMeasured: boolean) {
133
134
  const idleMinutes = Math.max(0, (now() - lastProgressAt) / 60_000);
134
135
  if (idleMinutes >= nextStallMinute) {
135
- log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent, idleMinutes });
136
+ log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent: visibleProgressEvent, idleMinutes });
136
137
  const observation = ledgerRead
137
138
  ? (activityMeasured ? '원장·작업 트리 모두 조용함' : '원장 조용함')
138
139
  : '원장 못 잼';
139
- onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${lastProgressEvent ?? '없음'}${activityMeasured ? '' : ' (작업 트리 못 잼)'}`);
140
+ onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${visibleProgressEvent ?? '없음'}${activityMeasured ? '' : ' (작업 트리 못 잼)'}`);
140
141
  stalled = true;
141
142
  nextStallMinute = (Math.floor(idleMinutes / stallMinutes) + 1) * stallMinutes;
142
143
  }
@@ -169,7 +170,7 @@ export function createPodLedgerFollower(opts: {
169
170
  if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
170
171
  stalled = false;
171
172
  lastProgressAt = now();
172
- lastProgressEvent = 'worktree-activity';
173
+ visibleProgressEvent = 'worktree-activity';
173
174
  nextStallMinute = stallMinutes;
174
175
  if (now() - lastActivityLogAt >= 5 * 60_000) {
175
176
  log('self-implement.pod', 'worktree-activity', { runId: opts.runId, files });
@@ -190,6 +191,11 @@ export function createPodLedgerFollower(opts: {
190
191
  log('self-implement.pod', 'ledger-live-appended', { runId: opts.runId, lines: complete.split('\n').filter(Boolean).length, bytes: Buffer.byteLength(complete), offset });
191
192
  for (const line of complete.split('\n')) {
192
193
  if (!line) continue;
194
+ let logEvent: ReturnType<typeof ledgerLineToLogEvent>;
195
+ try {
196
+ logEvent = ledgerLineToLogEvent(line, opts.runId);
197
+ if (logEvent) log(logEvent.category, logEvent.event, logEvent.data);
198
+ } catch { /* A failed host log write must not interrupt ledger following or stall accounting. */ }
193
199
  let entry: unknown;
194
200
  try { entry = JSON.parse(line); } catch { continue; }
195
201
  const event = (entry && typeof entry === 'object' && 'event' in entry) ? entry.event : undefined;
@@ -198,7 +204,7 @@ export function createPodLedgerFollower(opts: {
198
204
  if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
199
205
  stalled = false;
200
206
  lastProgressAt = now();
201
- lastProgressEvent = event;
207
+ visibleProgressEvent = logEvent?.event ?? '[redacted]';
202
208
  nextStallMinute = stallMinutes;
203
209
  }
204
210
  checkStall(true, activityMeasured);
@@ -0,0 +1,68 @@
1
+ const SECRET_FIELD = /token|secret|password|authorization|key/i;
2
+ const SAFE_FIELD = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
3
+ const SECRET_VALUE = /(?:\b(?:bearer|basic)\s+\S+|\b(?:token|secret|password|authorization|api[-_]?key|access[-_]?key|private[-_]?key)\b\s*(?:is|[=:])\s*["']?\S+|\b(?:sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}))\b/i;
4
+ const MAX_FIELDS = 32;
5
+ // Review R2: only lifecycle events the host needs to see, and no free text. Event names must start with a known
6
+ // lifecycle prefix; string fields survive only when they are short code-like values in a fixed set of keys.
7
+ const EVENT_ALLOWED = /^(?:gate|gated|review|reviewed|repair|repaired|rework|merge|merged|pr|land|landed|stage|round|supervisor|child|plan|planned|decompose|decomposed|shard|stall|harvest|decision|run|pipeline|progress|human|start|started|finish|finished|done|implement|implemented|commit|test|typecheck)(?:-(?:started|finished|round|opened|closed|failed|passed|blocked|done|entry|node|outcome|delivery|decision|stop|merged|ready|requested|resolved|result|fix|must-fix|verdict|skipped|origin|cleared))*$/;
8
+ const CODE_FIELDS = new Set(['stage', 'outcome', 'verdict', 'status', 'kind', 'phase', 'role', 'result']);
9
+ const CODE_VALUE = /^[a-z0-9_.:-]{1,40}$/i;
10
+
11
+ function secretValues(root: Record<string, unknown>): string[] {
12
+ const secrets: string[] = [];
13
+ const pending: unknown[] = [root];
14
+ while (pending.length) {
15
+ const current = pending.pop();
16
+ if (!current || typeof current !== 'object') continue;
17
+ if (Array.isArray(current)) {
18
+ pending.push(...current);
19
+ continue;
20
+ }
21
+ for (const [name, value] of Object.entries(current)) {
22
+ if (SECRET_FIELD.test(name) && typeof value === 'string' && value.length >= 3) secrets.push(value);
23
+ if (value && typeof value === 'object') pending.push(value);
24
+ }
25
+ }
26
+ return secrets;
27
+ }
28
+
29
+ /** Project bounded ledger metadata, excluding named secrets and recognizable credential values. */
30
+ export function ledgerLineToLogEvent(line: string, runId: string): {
31
+ category: string;
32
+ event: string;
33
+ data: Record<string, unknown>;
34
+ } | undefined {
35
+ let entry: unknown;
36
+ try { entry = JSON.parse(line); } catch { return undefined; }
37
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !('event' in entry) || typeof entry.event !== 'string') return undefined;
38
+ // The real run-ledger writer wraps observer fields in `data`; older/synthetic lines can be flat.
39
+ const details = 'data' in entry && entry.data && typeof entry.data === 'object' && !Array.isArray(entry.data)
40
+ ? entry.data as Record<string, unknown> : undefined;
41
+ const sources = [entry, details].filter((source): source is Record<string, unknown> => source !== undefined);
42
+ const knownSecrets = secretValues(entry);
43
+ const containsCredential = (value: string) => SECRET_VALUE.test(value) || knownSecrets.some((secret) => value.includes(secret));
44
+ if (containsCredential(entry.event) || !EVENT_ALLOWED.test(entry.event)) return undefined;
45
+
46
+ const data: Record<string, unknown> = { runId };
47
+ let fields = 0;
48
+ for (const source of sources) {
49
+ for (const [name, value] of Object.entries(source)) {
50
+ if (name === 'event' || name === 'runId' || name === 'data' || !SAFE_FIELD.test(name) || SECRET_FIELD.test(name)) continue;
51
+ if (fields >= MAX_FIELDS) break;
52
+ const countName = `${name}Count`;
53
+ const outputName = Array.isArray(value) || value && typeof value === 'object' ? countName : name;
54
+ if (!SAFE_FIELD.test(outputName) || SECRET_FIELD.test(outputName) || Object.hasOwn(data, outputName)) continue;
55
+ if (typeof value === 'string') {
56
+ // Free text (reasons, messages, patches) never leaves the pod ledger; only short code values in known keys.
57
+ if (!CODE_FIELDS.has(name) || !CODE_VALUE.test(value) || containsCredential(value)) continue;
58
+ data[name] = value;
59
+ } else if (typeof value === 'number' && Number.isFinite(value) || typeof value === 'boolean' || value === null) {
60
+ data[name] = value;
61
+ } else if (Array.isArray(value) || value && typeof value === 'object') {
62
+ data[countName] = Array.isArray(value) ? value.length : Object.keys(value).length;
63
+ } else continue;
64
+ fields += 1;
65
+ }
66
+ }
67
+ return { category: 'self-implement.pod.ledger', event: entry.event, data };
68
+ }
@@ -50,7 +50,7 @@ import { findGitDir } from '../../git-fs/locate.js';
50
50
  import { debug } from '../../debug/log.js';
51
51
  import { selectLiveDetail } from '../../live/detail-switch.js';
52
52
  import { authStorePath } from '../../oauth/store.js';
53
- import { grokAuthFilePath, resolveGrokCredential } from '../../grok/credential.js';
53
+ import { grokAuthFilePath, isGrokSubscriptionExpiring, refreshGrokSubscriptionToken, resolveGrokCredential } from '../../grok/credential.js';
54
54
  import { defaultGrokModel } from '../../grok/models.js';
55
55
  import { resolveHostId } from '../../platform/host-id.js';
56
56
  import { envLiteral } from '../../platform/env-literal.js';
@@ -171,12 +171,26 @@ export function hostCredentials(account: string, storePath: string = authStorePa
171
171
  return { elanousAuth, codexAuth, ghToken: ghToken() };
172
172
  }
173
173
 
174
- /** resolveGrokCredential 과 동일한 출처를 읽되 Pod 로는 refresh 없는 access 사본만 보낸다. */
174
+ /** A Pod gets a refresh-less access copy, so the copy must outlive the Job: same rule as the Codex path (3 h). */
175
+ export const POD_GROK_MIN_VALIDITY_MS = 3 * 3600_000;
176
+
177
+ /** resolveGrokCredential 과 동일한 출처를 읽되 Pod 로는 refresh 없는 access 사본만 보낸다.
178
+ * POD4 (10-01): the copy used to ship whatever the host had — an AUTH1 shard copied a token that had expired
179
+ * 4 minutes earlier and died at `grok-credential-preflight status=expired`. Refresh on the host first. */
175
180
  export function hostGrokCredentials(opts: {
176
181
  home?: string; env?: NodeJS.ProcessEnv; apiKeyOptIn?: boolean; ghToken?: () => string;
182
+ /** Test seams — the host refresh (read-only `grok models`) and the expiry probe. */
183
+ refresh?: () => void; isExpiring?: () => boolean;
177
184
  } = {}): { grokAuth?: string; grokApiKey?: string; ghToken: string } {
178
- const credential = resolveGrokCredential({ home: opts.home, env: opts.env });
185
+ let credential = resolveGrokCredential({ home: opts.home, env: opts.env });
179
186
  if (credential?.kind === 'subscription') {
187
+ const expiring = opts.isExpiring ?? (() => isGrokSubscriptionExpiring({ home: opts.home, bufferMs: POD_GROK_MIN_VALIDITY_MS }));
188
+ if (expiring()) {
189
+ try { (opts.refresh ?? (() => { refreshGrokSubscriptionToken({ home: opts.home }); }))(); } catch { /* judged below */ }
190
+ if (expiring()) throw new Error('grok: 구독 access 토큰이 3시간 안에 만료 — 호스트 갱신(grok models)도 못 늘렸다 · 호스트에서 grok login 후 다시');
191
+ credential = resolveGrokCredential({ home: opts.home, env: opts.env });
192
+ if (credential?.kind !== 'subscription') throw new Error('grok: 갱신 뒤 구독 자격을 다시 읽지 못했다');
193
+ }
180
194
  const scopes = JSON.parse(readFileSync(grokAuthFilePath(opts.home), 'utf8')) as Record<string, unknown>;
181
195
  const redacted: Record<string, { key: string; expires_at?: string; user_id?: string }> = {};
182
196
  for (const [name, value] of Object.entries(scopes)) {
@@ -219,6 +233,64 @@ function githubRepositoryFromUrl(repoUrl: string): string | null {
219
233
 
220
234
  /** Job 스크립트의 salvage 단계 — 자식 rc 가 0 이 아닐 때만, 변경·미푸시 커밋이 있는 격리 워크트리를 `salvage/<job>/<worktree>` 로 push.
221
235
  * main·self-impl/ 로는 절대 안 민다. 실패해도 제어 흐름을 바꾸지 않는다(호출자는 이 뒤에 `exit $rc`). */
236
+ /**
237
+ * App installation tokens live one hour and only the host process renews them (at 10 minutes left).
238
+ * If that process dies (host reboot), nothing renews the Pod's token. hosts.yml unchanged for this long
239
+ * means the host is gone. Push a snapshot while the token still works.
240
+ */
241
+ export const POD_GH_STALE_SECONDS = 53 * 60;
242
+
243
+ /** Snapshot every worktree to salvage/<job>/<wt>-early without touching the child's index, worktree or HEAD. */
244
+ export function podEarlySalvageScript(): string {
245
+ return [
246
+ 'job_name="${ELANOUS_POD_NAME:-unknown-job}"',
247
+ 'while IFS= read -r wt; do',
248
+ ' [ -n "$wt" ] || continue',
249
+ ' case "$wt" in /*) ;; *) continue ;; esac',
250
+ ' wt_name=$(basename -- "$wt")',
251
+ " case \"$wt_name\" in ''|.*|*/*|*'..'*) continue ;; esac",
252
+ ' branch="salvage/${job_name}/${wt_name}-early"',
253
+ ' (',
254
+ ' set +e',
255
+ ' cd -- "$wt" || exit 0',
256
+ ' head=$(git rev-parse --verify -q HEAD) || { printf \'ELANOUS_POD_SALVAGE_NONE early-no-head\\n\'; exit 0; }',
257
+ ' idx=$(mktemp) || exit 0',
258
+ ' cp -- "$(git rev-parse --git-path index)" "$idx" 2>/dev/null || :',
259
+ ' GIT_INDEX_FILE="$idx" git add -A -- . >/dev/null 2>&1',
260
+ ' tree=$(GIT_INDEX_FILE="$idx" git write-tree 2>/dev/null)',
261
+ ' rm -f -- "$idx"',
262
+ ' [ -n "$tree" ] || { printf \'ELANOUS_POD_SALVAGE_NONE early-write-tree-failed\\n\'; exit 0; }',
263
+ ' base=$(git rev-parse --verify -q origin/HEAD || git rev-parse --verify -q origin/main || true)',
264
+ ' if [ "$tree" = "$(git rev-parse "${head}^{tree}")" ]; then',
265
+ ' if [ -z "$base" ] || [ "$(git rev-list --count "${base}..${head}" 2>/dev/null || echo 1)" = 0 ]; then printf \'ELANOUS_POD_SALVAGE_NONE clean\\n\'; exit 0; fi',
266
+ ' commit=$head',
267
+ ' else',
268
+ ' commit=$(git commit-tree "$tree" -p "$head" -m "salvage(early): ${job_name} host token refresh stopped") || { printf \'ELANOUS_POD_SALVAGE_NONE commit-failed\\n\'; exit 0; }',
269
+ ' fi',
270
+ ' git push --force origin "${commit}:refs/heads/${branch}" || { printf \'ELANOUS_POD_SALVAGE_NONE push-failed\\n\'; exit 0; }',
271
+ " printf 'ELANOUS_POD_SALVAGE %s %s\\n' \"$branch\" \"$commit\"",
272
+ ' )',
273
+ "done < <(git worktree list --porcelain 2>/dev/null | awk '/^worktree / { sub(/^worktree /, \"\"); print }')",
274
+ ].join('\n');
275
+ }
276
+
277
+ /** Background watchdog: fires the early snapshot once when hosts.yml has not been rewritten for staleSeconds. */
278
+ export function podGithubWatchdogScript(staleSeconds: number, intervalSeconds = 30): string {
279
+ return [
280
+ '(',
281
+ ` while sleep ${intervalSeconds}; do`,
282
+ ' f="$GH_CONFIG_DIR/hosts.yml"',
283
+ ' m=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null) || continue',
284
+ ' age=$(( $(date +%s) - m ))',
285
+ ` [ "$age" -ge ${staleSeconds} ] || continue`,
286
+ " printf 'ELANOUS_POD_GH_STALE %s\\n' \"$age\"",
287
+ ` ${podEarlySalvageScript().split('\n').join('\n ')}`,
288
+ ' break',
289
+ ' done',
290
+ ') & gh_watch_pid=$!',
291
+ ].join('\n');
292
+ }
293
+
222
294
  export function podSalvageScript(): string {
223
295
  return [
224
296
  'if [ "${rc:-0}" -ne 0 ]; then',
@@ -368,7 +440,7 @@ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Read
368
440
  return { limit: a !== null && b !== null && a > b ? base : high, tier, source };
369
441
  }
370
442
 
371
- export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; goalDoc?: string; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean }): Record<string, unknown> {
443
+ export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; goalDoc?: string; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number }): Record<string, unknown> {
372
444
  const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
373
445
  const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
374
446
  const askBaseIndex = o.goalDoc ? o.args.indexOf('--base') : -1;
@@ -394,6 +466,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
394
466
  ] : []),
395
467
  // Only fixed command shapes leave the Pod; arbitrary argv (including shell -c text) is never emitted.
396
468
  `(while :; do { mem=$(if [ -r /sys/fs/cgroup/memory.current ]; then cat /sys/fs/cgroup/memory.current; elif [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then cat /sys/fs/cgroup/memory/memory.usage_in_bytes; else printf -- -; fi); top=$(ps -eo rss=,comm=,args= --sort=-rss | head -5 | awk 'function encode(s) { gsub(/%/, "%25", s); gsub(/:/, "%3A", s); gsub(/ /, "%20", s); gsub(/\\t/, "%09", s); return s } { rss=$1; name=$2; sub(/^[[:space:]]*[0-9]+[[:space:]]+[^[:space:]]+[[:space:]]*/, ""); n=split($0, a, /[[:space:]]+/); cmd="<redacted>"; if (name=="sleep" && a[2]=="30") cmd="sleep 30" (n>2 ? " <redacted>" : ""); else if (name=="bun") { cmd="bun <redacted>"; if (a[2]=="test") cmd="bun test <redacted>"; else if (a[2]=="run") cmd="bun run <redacted>"; else if ((a[2]=="x" || a[2]=="exec") && a[3]=="tsc") cmd="bun x tsc <redacted>" } else if (name=="tsc") cmd="tsc <redacted>"; else if (name=="elanous") { cmd="elanous <redacted>"; if (a[2]=="self") cmd="elanous self <redacted>"; else if (a[2]=="harness") cmd="elanous harness <redacted>" } else if (name=="node") cmd="node <redacted>"; if (name=="sleep" && cmd=="<redacted>") cmd="sleep <redacted>"; else if (cmd=="<redacted>" && name!="bash" && name!="sh") name="other"; printf " %s:%s:%s", rss, encode(name), encode(substr(cmd,1,120)) }'); printf "ELANOUS_MEM %s %s%s\\n" "$(date +%s)" "$mem" "$top"; } || true; sleep 15 || break; done) & mem_sampler_pid=$!`,
469
+ ...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
397
470
  // 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
398
471
  goalPath
399
472
  ? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?; bun -e 'const fs=require("fs");const p="/tmp/si.out";const s=fs.readFileSync(p,"utf8");const line=s.trimEnd().split("\\n").at(-1);try{const o=JSON.parse(line);if(o.kind==="self"&&o.result&&typeof o.result==="object")fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n")}catch{}'`
@@ -454,6 +527,7 @@ for ledger in "\${ELANOUS_STATE_DIR:-$HOME/.elanous}"/run-ledger/*.jsonl; do
454
527
  done
455
528
  if [ "$found" -eq 0 ]; then echo ELANOUS_RUN_LEDGER_NONE; fi`,
456
529
  'kill "$mem_sampler_pid" 2>/dev/null || true',
530
+ ...(o.appCredential ? ['kill "$gh_watch_pid" 2>/dev/null || true'] : []),
457
531
  'wait "$mem_sampler_pid" 2>/dev/null || true',
458
532
  'tail -n 1 /tmp/si.out',
459
533
  podSalvageScript(),
package/src/telegram.ts CHANGED
@@ -66,7 +66,11 @@ import { observeDevRequestRouteFailSoft } from './skills/dev-request-router.js';
66
66
  import { runUrlRoute } from './skills/url-route-exec.js';
67
67
  import { recordRoutedLinkAbsorbed } from './intake-plane/link-ledger.js';
68
68
  import { classifyFrontInput } from './intake-plane/front-classifier.js';
69
+ import { handleTelegramSeatWork, type TelegramSeatWorkDeps } from './intake-plane/telegram-seat-work.js';
69
70
  import { effectiveInstanceRoot } from './instance/resolve.js';
71
+ import { getElanousConfigDir } from './elanous-config-dir.js';
72
+ import { defaultFieldEvent, FieldUploadError, parseFieldCaption, resolveFieldMime, saveFieldMedia } from './field/field-media.js';
73
+ import { scheduleFieldReel, type FieldReelOptions } from './field/field-reel.js';
70
74
 
71
75
  export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
72
76
 
@@ -105,6 +109,10 @@ export interface TgIncoming {
105
109
  botId?: string;
106
110
  /** 답장 대상 메시지의 텍스트(force_reply 정정 가로채기용). parseUpdate 가 채운다. */
107
111
  replyToText?: string;
112
+ /** 앨범(media group) id — `#현장` 캡션은 앨범의 첫 항목에만 붙으므로 나머지를 같은 행사로 묶는 데 쓴다. */
113
+ mediaGroupId?: string;
114
+ /** 영상 — `#현장` 저장 전용. `attachments` 에 넣지 않는다(LLM 경로의 동작을 안 바꾸려고). */
115
+ video?: TgIncomingAttachment;
108
116
  }
109
117
 
110
118
  /** Streaming surface passed to the message handler. The handler can
@@ -200,6 +208,14 @@ export interface TelegramBotOpts {
200
208
  * independently of the LLM chat path. Best-effort: errors are
201
209
  * swallowed by the caller — never blocks chat reply. */
202
210
  onTriggerTap?: (event: TgTriggerEvent) => void | Promise<void>;
211
+ /** Test seam for addressed seat intake; defaults to the real intake door. */
212
+ seatWorkDeps?: TelegramSeatWorkDeps;
213
+ /** `#현장` 기본 행사 — 없으면 `slashContext.userConfig.telegram.fieldDefaultEvent` · 그것도 없으면 `field-<로컬 날짜>`. */
214
+ fieldDefaultEvent?: string;
215
+ /** 시험 seam — 현장 폴더 뿌리(기본 `getElanousConfigDir()`). */
216
+ fieldRootDir?: () => string;
217
+ /** 시험에서는 실제 렌더 대신 가짜 실행기를 주입한다. */
218
+ fieldReel?: FieldReelOptions;
203
219
  }
204
220
 
205
221
  /** Surface-unification v2 (FU-2) — Telegram trigger event passed to
@@ -314,6 +330,15 @@ export class TelegramBot {
314
330
  * the bot under Telegram's ~30/sec global cap without blocking
315
331
  * further if load is bursty but average-safe. */
316
332
  private readonly globalWindow: number[] = [];
333
+ private readonly fieldDefaultEventOpt?: string;
334
+ private readonly fieldRootDir: () => string;
335
+ private readonly fieldReel?: FieldReelOptions;
336
+ private readonly seatWorkDeps: TelegramSeatWorkDeps;
337
+ /** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
338
+ private readonly fieldGroups = new Map<string, {
339
+ event: string; at: number; chatId: number; threadId?: number; replyTo: number; count: number;
340
+ timer?: ReturnType<typeof setTimeout>;
341
+ }>();
317
342
 
318
343
  constructor(opts: TelegramBotOpts) {
319
344
  if (!opts.token) throw new Error('TelegramBot: token required');
@@ -340,6 +365,10 @@ export class TelegramBot {
340
365
  this.slashCommands = opts.slashCommands ?? [];
341
366
  this.slashContext = opts.slashContext;
342
367
  this.voiceAdapter = opts.voiceAdapter;
368
+ this.fieldDefaultEventOpt = opts.fieldDefaultEvent;
369
+ this.fieldRootDir = opts.fieldRootDir ?? getElanousConfigDir;
370
+ this.fieldReel = opts.fieldReel;
371
+ this.seatWorkDeps = opts.seatWorkDeps ?? {};
343
372
  if (opts.onTriggerTap) {
344
373
  // Assign through `as unknown` to bypass `readonly` + private —
345
374
  // the tap is set once at construction and never reassigned.
@@ -777,6 +806,7 @@ export class TelegramBot {
777
806
  replyTo?: number;
778
807
  threadId?: number;
779
808
  caption?: string;
809
+ mimeType?: string;
780
810
  } = {}): Promise<{ messageId: number } | undefined> {
781
811
  // Per-chat gap mirror — keep telegram happy on rapid document spills.
782
812
  const last = this.chatLastSentAt.get(chatId) ?? 0;
@@ -797,7 +827,7 @@ export class TelegramBot {
797
827
  new Uint8Array(blobBytes).set(bytes);
798
828
  form.set(
799
829
  'document',
800
- new Blob([blobBytes], { type: 'text/plain' }),
830
+ new Blob([blobBytes], { type: opts.mimeType ?? 'text/plain' }),
801
831
  filename,
802
832
  );
803
833
 
@@ -1347,11 +1377,107 @@ export class TelegramBot {
1347
1377
  return false;
1348
1378
  }
1349
1379
 
1380
+ /** `#현장` 캡션 사진·영상(이미지·영상 문서 포함)을 현장 폴더에 저장하고 한 줄로 답한다.
1381
+ * 처리했으면 true. 저장 규칙은 HTTP 입구와 같은 `saveFieldMedia` 다. */
1382
+ private async tryHandleFieldUpload(ctx: TgIncoming): Promise<boolean> {
1383
+ const media = [
1384
+ ...ctx.attachments.filter((a) => a.kind === 'photo'
1385
+ || (a.kind === 'document' && resolveFieldMime(a.mimeType, a.fileName ?? '') !== null)),
1386
+ ...(ctx.video ? [ctx.video] : []),
1387
+ ];
1388
+ if (media.length === 0) return false;
1389
+ const now = this.nowImpl();
1390
+ for (const [id, g] of this.fieldGroups) if (now - g.at > 10 * 60_000 && !g.timer) this.fieldGroups.delete(id);
1391
+ const tag = parseFieldCaption(ctx.text);
1392
+ const group = ctx.mediaGroupId ? this.fieldGroups.get(ctx.mediaGroupId) : undefined;
1393
+ if (!tag && !group) return false;
1394
+ const event = tag
1395
+ ? tag.event ?? defaultFieldEvent(
1396
+ this.fieldDefaultEventOpt ?? this.slashContext?.userConfig?.telegram?.fieldDefaultEvent, new Date(now))
1397
+ : group!.event;
1398
+
1399
+ let count: number | null = null;
1400
+ let failure: string | null = null;
1401
+ try {
1402
+ const inputs = [];
1403
+ for (const att of media) {
1404
+ const downloaded = await this.downloadFile(att.fileId);
1405
+ try {
1406
+ inputs.push({
1407
+ originalName: att.fileName ?? downloaded.fileName ?? 'media',
1408
+ mimeType: att.mimeType ?? (att.kind === 'photo' ? 'image/jpeg' : undefined),
1409
+ bytes: new Uint8Array(readFileSync(downloaded.localPath)),
1410
+ });
1411
+ } finally {
1412
+ try { unlinkSync(downloaded.localPath); } catch { /* 이미 없음 */ }
1413
+ }
1414
+ }
1415
+ // 글(자막)은 캡션이 달린 항목에만 있다 — 앨범이면 첫 장에만 붙는다(MK 2026-09-30).
1416
+ const saved = saveFieldMedia(inputs, { rootDir: this.fieldRootDir(), event, device: 'telegram', ...(tag?.text ? { caption: tag.text } : {}) });
1417
+ count = saved.count;
1418
+ scheduleFieldReel(saved.dir, {
1419
+ ...this.fieldReel,
1420
+ notificationKey: `telegram:${this.botId}:${ctx.chatId}:${ctx.threadId ?? ''}:${ctx.mediaGroupId ?? ctx.messageId}`,
1421
+ onDone: async (result) => {
1422
+ const reply = { replyTo: ctx.messageId, threadId: ctx.threadId };
1423
+ if (result.ok && result.file) {
1424
+ const sent = await this.sendDocument(ctx.chatId, readFileSync(result.file), 'reel-9x16.mp4', {
1425
+ ...reply, mimeType: 'video/mp4', caption: `현장 영상 · ${event} · ${result.seconds}초`,
1426
+ });
1427
+ if (!sent) throw new Error('sendDocument failed');
1428
+ } else {
1429
+ await this.sendMessage(ctx.chatId, `현장 영상 실패 · ${event} · ${result.error ?? 'render failed'}`, reply);
1430
+ }
1431
+ },
1432
+ });
1433
+ debug.log('field.upload', 'saved', { surface: 'telegram', files: inputs.length, count, album: !!ctx.mediaGroupId });
1434
+ } catch (err) {
1435
+ failure = err instanceof FieldUploadError ? err.code : 'download-failed';
1436
+ debug.log('field.upload', 'refused', { surface: 'telegram', reason: failure });
1437
+ }
1438
+
1439
+ if (failure !== null) {
1440
+ await this.sendMessage(ctx.chatId, `현장 저장 실패 · ${event} · ${failure}`, { replyTo: ctx.messageId, threadId: ctx.threadId });
1441
+ return true;
1442
+ }
1443
+ const line = (n: number) => `현장 폴더에 저장 · ${event} · 지금 ${n}장`;
1444
+ if (!ctx.mediaGroupId) {
1445
+ await this.sendMessage(ctx.chatId, line(count!), { replyTo: ctx.messageId, threadId: ctx.threadId });
1446
+ return true;
1447
+ }
1448
+ // 앨범은 항목마다 답하지 않는다 — 마지막 항목 뒤 한 번(디바운스)만 최신 수로 답한다.
1449
+ const entry = group ?? { event, at: now, chatId: ctx.chatId, threadId: ctx.threadId, replyTo: ctx.messageId, count: 0 };
1450
+ entry.at = now;
1451
+ entry.count = count!;
1452
+ if (entry.timer) clearTimeout(entry.timer);
1453
+ entry.timer = setTimeout(() => {
1454
+ entry.timer = undefined;
1455
+ void this.sendMessage(entry.chatId, line(entry.count), { replyTo: entry.replyTo, threadId: entry.threadId })
1456
+ .catch((e) => this.log(`field reply failed: ${e instanceof Error ? e.message : String(e)}`));
1457
+ }, 1500);
1458
+ (entry.timer as { unref?: () => void }).unref?.();
1459
+ this.fieldGroups.set(ctx.mediaGroupId, entry);
1460
+ return true;
1461
+ }
1462
+
1350
1463
  private async handleIncoming(ctx: TgIncoming): Promise<void> {
1351
1464
  if (!this.isOwnerAllowed(ctx.userId)) {
1352
1465
  await this.refuseMessage(ctx);
1353
1466
  return;
1354
1467
  }
1468
+ // 현장 업로드 — `#현장` 캡션(또는 태그된 앨범의 나머지)의 사진·영상은 LLM 대신 현장 폴더로.
1469
+ if (await this.tryHandleFieldUpload(ctx)) return;
1470
+ if (ctx.text.startsWith('@')) {
1471
+ const reply = await handleTelegramSeatWork(ctx.text, {
1472
+ chatId: ctx.chatId, messageId: ctx.messageId, threadId: ctx.threadId, botId: ctx.botId,
1473
+ }, this.seatWorkDeps);
1474
+ if (reply !== null) {
1475
+ await this.sendMessage(ctx.chatId, reply, { replyTo: ctx.messageId, threadId: ctx.threadId });
1476
+ return;
1477
+ }
1478
+ }
1479
+ // 영상은 `#현장` 저장 전용으로만 파싱한다 — 그 밖의 영상은 종전처럼 무시한다.
1480
+ if (ctx.video && ctx.attachments.length === 0) return;
1355
1481
  // Cascade-zyu U2 — capture utterance intent at update ingress.
1356
1482
  try {
1357
1483
  const { userIntentLogger } = await import('./user-intent/index.js');
@@ -2069,6 +2195,7 @@ export class TelegramBot {
2069
2195
  interface PhotoSize { file_id: string; file_size?: number; width: number; height: number }
2070
2196
  interface VoiceLike { file_id: string; file_size?: number; mime_type?: string; duration?: number }
2071
2197
  interface DocumentLike { file_id: string; file_size?: number; mime_type?: string; file_name?: string }
2198
+ interface VideoLike { file_id: string; file_size?: number; mime_type?: string; file_name?: string; duration?: number }
2072
2199
 
2073
2200
  interface RawUpdate {
2074
2201
  update_id: number;
@@ -2083,6 +2210,8 @@ interface RawUpdate {
2083
2210
  voice?: VoiceLike;
2084
2211
  audio?: VoiceLike;
2085
2212
  document?: DocumentLike;
2213
+ video?: VideoLike;
2214
+ media_group_id?: string;
2086
2215
  reply_to_message?: { text?: string; message_id?: number };
2087
2216
  };
2088
2217
  callback_query?: {
@@ -2175,8 +2304,20 @@ export function parseUpdate(u: RawUpdate): TgIncoming | null {
2175
2304
  });
2176
2305
  }
2177
2306
 
2307
+ // 영상은 `#현장` 저장 전용 — 태그된 캡션이거나 앨범 항목일 때만 싣는다(그 밖의 영상은 종전처럼 무시).
2308
+ const video = m.video && (parseFieldCaption(m.caption) || m.media_group_id)
2309
+ ? {
2310
+ kind: 'document' as const,
2311
+ fileId: m.video.file_id,
2312
+ fileSize: m.video.file_size,
2313
+ mimeType: m.video.mime_type ?? 'video/mp4',
2314
+ fileName: m.video.file_name,
2315
+ duration: m.video.duration,
2316
+ }
2317
+ : undefined;
2318
+
2178
2319
  // Skip updates with zero signal: no text AND no attachments.
2179
- if (typeof m.text !== 'string' && attachments.length === 0) return null;
2320
+ if (typeof m.text !== 'string' && attachments.length === 0 && !video) return null;
2180
2321
 
2181
2322
  const isDm = m.chat.type === 'private';
2182
2323
  return {
@@ -2191,6 +2332,8 @@ export function parseUpdate(u: RawUpdate): TgIncoming | null {
2191
2332
  isGroup: !isDm,
2192
2333
  attachments,
2193
2334
  ...(m.reply_to_message?.text ? { replyToText: m.reply_to_message.text } : {}),
2335
+ ...(m.media_group_id ? { mediaGroupId: m.media_group_id } : {}),
2336
+ ...(video ? { video } : {}),
2194
2337
  };
2195
2338
  }
2196
2339
 
@@ -2,7 +2,7 @@
2
2
  // `elanous_skills_list` MCP tool. Mirrors the daemon-side ACP method
3
3
  // `elanous/skills/list` (src/acp/server.ts:2241) but session-agnostic so
4
4
  // the codex app-server can call it through the elanous-tools MCP server
5
- // without first opening an ACP session. Enumerates `~/.elanous/skills/*`
5
+ // without first opening an ACP session. Enumerates configured skill roots
6
6
  // and surfaces the first non-heading line of each SKILL.md as the
7
7
  // description.
8
8
  //
@@ -14,6 +14,7 @@
14
14
  import { join } from 'node:path';
15
15
  import { readdir, readFile } from 'node:fs/promises';
16
16
  import { defaultSkillDirs } from '../user-config.js';
17
+ import { SKILLS_LIST_DESCRIPTION } from '../onboarding/entry-hints.js';
17
18
  import type { LLMToolSpec } from '../llm.js';
18
19
  import type { ToolRuntime } from './types.js';
19
20
 
@@ -41,8 +42,7 @@ export interface ElanousSkillsListResult extends Record<string, unknown> {
41
42
  export function buildElanousSkillsListTool(): LLMToolSpec {
42
43
  return {
43
44
  name: 'elanous_skills_list',
44
- description:
45
- 'List installed elanous skills (sub-directories of ~/.elanous/skills with optional SKILL.md). Codex app-server callback via the elanous-tools MCP server. Read-only.',
45
+ description: SKILLS_LIST_DESCRIPTION,
46
46
  parameters: {
47
47
  type: 'object',
48
48
  properties: {