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,76 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
5
+
6
+ export type SeatStatus = 'waiting' | 'running' | 'done' | 'failed';
7
+ export interface ExecSeat {
8
+ seat: string;
9
+ title: string;
10
+ status: SeatStatus;
11
+ graphId: string;
12
+ runId: string;
13
+ inputs?: Record<string, unknown>;
14
+ reason?: string;
15
+ }
16
+ export interface ExecResult {
17
+ seat: string;
18
+ kind: 'report' | 'pdf' | 'video' | 'image' | 'text' | 'link';
19
+ title: string;
20
+ url: string;
21
+ sources?: Array<{ title: string; url: string }>;
22
+ }
23
+ export interface ExecRequest {
24
+ id: string;
25
+ text: string;
26
+ createdAt: string;
27
+ status: 'planning' | 'running' | 'done' | 'failed';
28
+ summary: string;
29
+ seats: ExecSeat[];
30
+ results: ExecResult[];
31
+ approvals: Array<{ graphId: string; runId: string; message: string }>;
32
+ }
33
+
34
+ const validId = (id: string) => /^[a-f0-9-]{36}$/.test(id);
35
+
36
+ export class ExecRequestStore {
37
+ readonly dir: string;
38
+ constructor(configDir = getElanousConfigDir()) { this.dir = join(configDir, 'exec-requests'); }
39
+
40
+ create(text: string): ExecRequest {
41
+ const item: ExecRequest = { id: randomUUID(), text, createdAt: new Date().toISOString(), status: 'planning', summary: '', seats: [], results: [], approvals: [] };
42
+ this.save(item);
43
+ return item;
44
+ }
45
+
46
+ get(id: string): ExecRequest | null {
47
+ if (!validId(id)) return null;
48
+ try {
49
+ const item = JSON.parse(readFileSync(join(this.dir, `${id}.json`), 'utf8')) as ExecRequest;
50
+ return item.id === id ? item : null;
51
+ } catch (error) {
52
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ list(): ExecRequest[] {
58
+ try {
59
+ return readdirSync(this.dir).filter(file => /^[a-f0-9-]{36}\.json$/.test(file))
60
+ .flatMap(file => { const item = this.get(file.slice(0, -5)); return item ? [item] : []; })
61
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
62
+ } catch (error) {
63
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ save(item: ExecRequest): void {
69
+ if (!validId(item.id)) throw new Error('invalid exec request id');
70
+ mkdirSync(this.dir, { recursive: true, mode: 0o700 });
71
+ const target = join(this.dir, `${item.id}.json`);
72
+ const temp = join(this.dir, `${item.id}.${randomUUID()}.tmp`);
73
+ writeFileSync(temp, JSON.stringify(item) + '\n', { flag: 'wx', mode: 0o600 });
74
+ renameSync(temp, target);
75
+ }
76
+ }
@@ -0,0 +1,250 @@
1
+ // 현장(field) 업로드 폴더 계약 — HTTP 입구(`/v1/field/uploads`)와 텔레그램 `#현장` 이 «같은» 저장 함수를 쓴다.
2
+ //
3
+ // 폴더 계약(2026-09-30 합의 · 소비자 = reel 파이프라인):
4
+ // <configDir>/field/<event>/<ISO 기본형 타임스탬프>-<device>-<원래 이름>
5
+ // 타임스탬프 = 클라이언트가 준 촬영 시각(있으면) · 없으면 업로드 시각 · UTC · `20261015T193012Z`(ISO 8601 기본형 —
6
+ // 파일 이름에 콜론을 넣지 않으려고 기본형을 쓴다. 사전순 = 시간순).
7
+ // 배치가 끝나면 `<folder>/.ready` 를 한 줄(= 폴더의 미디어 파일 수)로 덮어쓴다.
8
+
9
+ import { appendFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { randomUUID } from 'node:crypto';
12
+
13
+ export const FIELD_SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
14
+ export const FIELD_MAX_FILE_BYTES = 100 * 1024 * 1024; // 폰 현장 사진·짧은 영상 — 데몬 전역 요청 한도를 과하게 올리지 않으려고 낮게(2026-09-30)
15
+ export const FIELD_MAX_REQUEST_BYTES = 250 * 1024 * 1024;
16
+ export const FIELD_READY_FILE = '.ready';
17
+ const MAX_ORIGINAL_NAME = 80;
18
+
19
+ /** 허용 MIME → 대표 확장자. heif 는 heic 의 별칭으로 받는다. */
20
+ const MIME_EXT: Record<string, string> = {
21
+ 'image/jpeg': 'jpg',
22
+ 'image/jpg': 'jpg',
23
+ 'image/png': 'png',
24
+ 'image/heic': 'heic',
25
+ 'image/heif': 'heic',
26
+ 'image/webp': 'webp',
27
+ 'video/mp4': 'mp4',
28
+ 'video/quicktime': 'mov',
29
+ };
30
+ const EXT_MIME: Record<string, string> = {
31
+ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', heic: 'image/heic', heif: 'image/heif',
32
+ webp: 'image/webp', mp4: 'video/mp4', mov: 'video/quicktime',
33
+ };
34
+
35
+ export type FieldUploadErrorCode = 'bad-slug' | 'unsupported-media-type' | 'too-large' | 'no-files' | 'bad-request';
36
+
37
+ export class FieldUploadError extends Error {
38
+ constructor(readonly code: FieldUploadErrorCode, message: string) {
39
+ super(message);
40
+ this.name = 'FieldUploadError';
41
+ }
42
+ get status(): number {
43
+ if (this.code === 'unsupported-media-type') return 415;
44
+ if (this.code === 'too-large') return 413;
45
+ return 400;
46
+ }
47
+ }
48
+
49
+ export interface FieldMediaInput {
50
+ originalName: string;
51
+ mimeType?: string;
52
+ bytes: Uint8Array;
53
+ /** 촬영 시각(ISO 문자열·epoch ms). 못 읽으면 업로드 시각. */
54
+ capturedAt?: string | number | Date;
55
+ /** 이 파일에 붙일 자막. */
56
+ caption?: string;
57
+ }
58
+
59
+ export interface FieldSaveOptions {
60
+ /** config 뿌리(`~/.elanous` 또는 격리 뿌리). 폴더 = `<rootDir>/field/<event>`. */
61
+ rootDir: string;
62
+ event: string;
63
+ device: string;
64
+ now?: () => Date;
65
+ /** 사람이 쓴 한 줄(텔레그램 캡션·앱 입력칸) — 있으면 `captions.txt` 에 «<첫 파일 이름> | <글>» 로 덧붙인다(reel 자막 · MK 2026-09-30). */
66
+ caption?: string;
67
+ }
68
+
69
+ export interface FieldSaveResult {
70
+ event: string;
71
+ dir: string;
72
+ saved: Array<{ name: string; bytes: number }>;
73
+ /** 저장 «후» 폴더의 미디어 파일 수(= `.ready` 에 쓴 값). */
74
+ count: number;
75
+ }
76
+
77
+ export function isFieldSlug(value: unknown): value is string {
78
+ return typeof value === 'string' && FIELD_SLUG_RE.test(value);
79
+ }
80
+
81
+ export function fieldEventDir(rootDir: string, event: string): string {
82
+ if (!isFieldSlug(event)) throw new FieldUploadError('bad-slug', 'event slug must match ^[a-z0-9][a-z0-9._-]{0,63}$');
83
+ return join(rootDir, 'field', event);
84
+ }
85
+
86
+ function extOf(name: string): string {
87
+ const dot = name.lastIndexOf('.');
88
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
89
+ }
90
+
91
+ /** 허용 MIME 이면 정규화한 MIME, 아니면 null. MIME 이 비었거나 octet-stream 이면 확장자로 추정한다. */
92
+ export function resolveFieldMime(mimeType: string | undefined, originalName: string): string | null {
93
+ const mime = (mimeType ?? '').split(';')[0]!.trim().toLowerCase();
94
+ if (mime && mime !== 'application/octet-stream') return MIME_EXT[mime] ? mime : null;
95
+ return EXT_MIME[extOf(originalName)] ?? null;
96
+ }
97
+
98
+ /** basename · `[A-Za-z0-9._-]` 외는 `_` · 앞 점 제거 · 최대 80자(확장자 보존) · 확장자가 MIME 과 안 맞으면 붙인다. */
99
+ export function sanitizeFieldName(originalName: string, mime?: string): string {
100
+ const base = originalName.split(/[\\/]/).pop() ?? '';
101
+ let name = base.replace(/[^A-Za-z0-9._-]/g, '_').replace(/_+/g, '_').replace(/^\.+/, '');
102
+ const wantExt = mime ? MIME_EXT[mime] : undefined;
103
+ const ext = extOf(name);
104
+ if (wantExt && EXT_MIME[ext] === undefined) name = name ? `${name}.${wantExt}` : `media.${wantExt}`;
105
+ if (!name) name = 'media';
106
+ if (name.length > MAX_ORIGINAL_NAME) {
107
+ const e = extOf(name);
108
+ const suffix = e && e.length < 10 ? `.${e}` : '';
109
+ name = name.slice(0, MAX_ORIGINAL_NAME - suffix.length) + suffix;
110
+ }
111
+ return name;
112
+ }
113
+
114
+ /** `20261015T193012Z` — ISO 8601 기본형 UTC(초 단위). */
115
+ export function fieldTimestamp(capturedAt: FieldMediaInput['capturedAt'], now: Date): string {
116
+ let at: Date | null = null;
117
+ if (capturedAt instanceof Date) at = capturedAt;
118
+ else if (typeof capturedAt === 'number') at = new Date(capturedAt);
119
+ else if (typeof capturedAt === 'string' && capturedAt.trim()) {
120
+ const trimmed = capturedAt.trim();
121
+ at = /^\d{10,13}$/.test(trimmed) ? new Date(Number(trimmed) * (trimmed.length === 10 ? 1000 : 1)) : new Date(trimmed);
122
+ }
123
+ if (!at || Number.isNaN(at.getTime())) at = now;
124
+ return at.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[-:]/g, '');
125
+ }
126
+
127
+ function isMediaFile(name: string): boolean {
128
+ return !name.startsWith('.') && EXT_MIME[extOf(name)] !== undefined;
129
+ }
130
+
131
+ export function listFieldMedia(dir: string): Array<{ name: string; bytes: number }> {
132
+ let names: string[];
133
+ try { names = readdirSync(dir); }
134
+ catch (error) {
135
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
136
+ throw error;
137
+ }
138
+ const out: Array<{ name: string; bytes: number }> = [];
139
+ for (const name of names.sort()) {
140
+ if (!isMediaFile(name)) continue;
141
+ try {
142
+ const st = statSync(join(dir, name));
143
+ if (st.isFile()) out.push({ name, bytes: st.size });
144
+ } catch { /* 사이에 지워졌다 */ }
145
+ }
146
+ return out;
147
+ }
148
+
149
+ function atomicWrite(dir: string, name: string, bytes: Uint8Array | string): void {
150
+ const tmp = join(dir, `.tmp-${randomUUID()}`);
151
+ try {
152
+ writeFileSync(tmp, bytes);
153
+ renameSync(tmp, join(dir, name));
154
+ } catch (error) {
155
+ try { unlinkSync(tmp); } catch { /* 없음 */ }
156
+ throw error;
157
+ }
158
+ }
159
+
160
+ /** `.ready` 를 «지금» 폴더의 미디어 파일 수로 덮어쓴다. */
161
+ export function refreshFieldReady(dir: string): number {
162
+ const count = listFieldMedia(dir).length;
163
+ atomicWrite(dir, FIELD_READY_FILE, `${count}\n`);
164
+ return count;
165
+ }
166
+
167
+ /** 검증(전부) → 쓰기(전부, 원자적) → `.ready` 갱신. 하나라도 검증에 걸리면 아무것도 안 쓴다. */
168
+ export function saveFieldMedia(inputs: FieldMediaInput[], opts: FieldSaveOptions): FieldSaveResult {
169
+ const dir = fieldEventDir(opts.rootDir, opts.event);
170
+ if (!isFieldSlug(opts.device)) throw new FieldUploadError('bad-slug', 'device slug must match ^[a-z0-9][a-z0-9._-]{0,63}$');
171
+ if (inputs.length === 0) throw new FieldUploadError('no-files', 'at least one file is required');
172
+ const now = (opts.now ?? (() => new Date()))();
173
+ let total = 0;
174
+ const planned = inputs.map((input) => {
175
+ const mime = resolveFieldMime(input.mimeType, input.originalName);
176
+ if (!mime) throw new FieldUploadError('unsupported-media-type', 'only jpeg/png/heic/webp images and mp4/mov videos are accepted');
177
+ const size = input.bytes.byteLength;
178
+ if (size === 0) throw new FieldUploadError('bad-request', 'empty file');
179
+ if (size > FIELD_MAX_FILE_BYTES) throw new FieldUploadError('too-large', 'a file exceeds 100 MB');
180
+ total += size;
181
+ if (total > FIELD_MAX_REQUEST_BYTES) throw new FieldUploadError('too-large', 'the request exceeds 250 MB');
182
+ const name = `${fieldTimestamp(input.capturedAt, now)}-${opts.device}-${sanitizeFieldName(input.originalName, mime)}`;
183
+ return { name, bytes: input.bytes, caption: input.caption };
184
+ });
185
+
186
+ mkdirSync(dir, { recursive: true });
187
+ const taken = new Set<string>();
188
+ const saved: FieldSaveResult['saved'] = [];
189
+ for (const item of planned) {
190
+ const name = uniqueName(dir, item.name, item.bytes.byteLength, taken);
191
+ taken.add(name);
192
+ atomicWrite(dir, name, item.bytes);
193
+ saved.push({ name, bytes: item.bytes.byteLength });
194
+ }
195
+ const lines = planned.flatMap((item, i) => {
196
+ const line = normalizeFieldCaption(item.caption ?? (i === 0 ? opts.caption : undefined));
197
+ return line ? [`${saved[i]!.name} | ${line}\n`] : [];
198
+ });
199
+ if (lines.length) appendFileSync(join(dir, FIELD_CAPTIONS_FILE), lines.join(''));
200
+ const count = refreshFieldReady(dir);
201
+ return { event: opts.event, dir, saved, count };
202
+ }
203
+
204
+ /** 같은 이름·같은 크기면 재전송으로 보고 덮는다(중복 방지). 크기가 다르면 `-1`,`-2`… 를 붙인다. */
205
+ function uniqueName(dir: string, name: string, size: number, taken: Set<string>): string {
206
+ const dot = name.lastIndexOf('.');
207
+ const stem = dot > 0 ? name.slice(0, dot) : name;
208
+ const ext = dot > 0 ? name.slice(dot) : '';
209
+ for (let i = 0; ; i += 1) {
210
+ const candidate = i === 0 ? name : `${stem}-${i}${ext}`;
211
+ if (taken.has(candidate)) continue;
212
+ let existing: number | null = null;
213
+ try { existing = statSync(join(dir, candidate)).size; } catch { existing = null; }
214
+ if (existing === null || existing === size) return candidate;
215
+ }
216
+ }
217
+
218
+ // ── 텔레그램 캡션 문법 ──────────────────────────────────────────────
219
+
220
+ export const FIELD_CAPTION_TAG = '#현장';
221
+
222
+ /** 캡션에 독립 토큰 `#현장` 이 있으면 `{ event? }`, 없으면 null.
223
+ * `#현장 <slug>` — 바로 다음 토큰을 소문자로 바꿔 슬러그 규칙에 맞으면 행사 이름, 아니면 기본 행사. */
224
+ export function parseFieldCaption(caption: string | undefined | null): { event?: string; text?: string } | null {
225
+ if (!caption) return null;
226
+ const tokens = caption.trim().split(/\s+/);
227
+ const at = tokens.indexOf(FIELD_CAPTION_TAG);
228
+ if (at < 0) return null;
229
+ const next = tokens[at + 1]?.toLowerCase();
230
+ const hasEvent = !!next && isFieldSlug(next);
231
+ // 태그·행사 이름을 뺀 나머지가 «글»(자막) — 태그 앞에 쓴 글도 같이.
232
+ const rest = tokens.filter((_, i) => i !== at && !(hasEvent && i === at + 1)).join(' ');
233
+ const text = normalizeFieldCaption(rest);
234
+ return { ...(hasEvent ? { event: next } : {}), ...(text ? { text } : {}) };
235
+ }
236
+
237
+ export const FIELD_CAPTIONS_FILE = 'captions.txt';
238
+
239
+ /** 자막 한 줄 — 줄바꿈은 공백으로, `|` 는 구분자라 전각 `|` 로, 280자까지. 비면 빈 값. */
240
+ export function normalizeFieldCaption(value: string | undefined | null): string {
241
+ if (typeof value !== 'string') return '';
242
+ return value.replace(/[\r\n]+/g, ' ').replace(/\|/g, '|').replace(/\s+/g, ' ').trim().slice(0, 280);
243
+ }
244
+
245
+ /** 기본 행사 = 유효한 config 값 · 없으면 `field-<로컬 YYYY-MM-DD>`. */
246
+ export function defaultFieldEvent(configured: string | undefined, now: Date = new Date()): string {
247
+ if (configured && isFieldSlug(configured)) return configured;
248
+ const pad = (n: number) => String(n).padStart(2, '0');
249
+ return `field-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
250
+ }
@@ -0,0 +1,196 @@
1
+ // 한 행사 폴더의 업로드 디바운스와 직렬 렌더. 프로세스 안의 폴더별 단일 실행기.
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { spawn, execFileSync } from 'node:child_process';
6
+ import { debug } from '../debug/log.js';
7
+ import { listFieldMedia } from './field-media.js';
8
+
9
+ export interface FieldReelResult { ok: boolean; file?: string; seconds: number; error?: string }
10
+ export type FieldReelRunner = (dir: string, opts: { title: string; sub: string }) => Promise<FieldReelResult>;
11
+ export interface FieldReelStatus {
12
+ state: 'waiting' | 'rendering' | 'done' | 'failed';
13
+ items: number;
14
+ updatedAt: string;
15
+ startedAt?: string;
16
+ finishedAt?: string;
17
+ seconds?: number;
18
+ file?: string;
19
+ error?: string;
20
+ }
21
+ export interface FieldReelOptions {
22
+ quietMs?: number;
23
+ title?: string;
24
+ sub?: string;
25
+ runner?: FieldReelRunner;
26
+ onDone?: (result: FieldReelResult) => void | Promise<void>;
27
+ /** 같은 수신자가 앨범 항목마다 예약해도 한 렌더당 한 번만 통지한다. */
28
+ notificationKey?: string;
29
+ }
30
+
31
+ const REEL_FILE = 'reel-9x16.mp4';
32
+ interface Job {
33
+ timer?: ReturnType<typeof setTimeout>;
34
+ running: boolean;
35
+ dirty: boolean;
36
+ lastUploadAt: number;
37
+ opts: FieldReelOptions;
38
+ callbacks: Map<string, NonNullable<FieldReelOptions['onDone']>>;
39
+ }
40
+ const jobs = new Map<string, Job>();
41
+
42
+ export function fieldReelFile(dir: string): string { return join(dir, 'reel', REEL_FILE); }
43
+
44
+ /** 요청 경로용 싼 검사 — 파일이 있고 비어 있지 않다. 디코딩 검사는 렌더가 끝날 때 한 번(`isValidFieldReelFile`)만 한다. */
45
+ export function hasFieldReelFile(file: string): boolean {
46
+ try {
47
+ const stat = statSync(file);
48
+ return stat.isFile() && stat.size > 0;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /** Publish only a reel with a decodable video frame, not merely valid-looking MP4 boxes.
55
+ * 렌더 완료 때 한 번만 부른다 — 첫 영상 프레임 하나만 읽는다(`-read_intervals %+#1`) · 긴 영상도 출력이 작다. */
56
+ export function isValidFieldReelFile(file: string): boolean {
57
+ try {
58
+ if (!hasFieldReelFile(file)) return false;
59
+ const probe = JSON.parse(execFileSync('ffprobe', [
60
+ '-v', 'error', '-select_streams', 'v:0', '-read_intervals', '%+#1', '-show_streams', '-show_frames',
61
+ '-show_entries', 'stream=codec_type:frame=media_type', '-of', 'json', file,
62
+ ], { encoding: 'utf8', timeout: 30_000, maxBuffer: 2 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] })) as {
63
+ streams?: Array<{ codec_type?: string }>;
64
+ frames?: Array<{ media_type?: string }>;
65
+ };
66
+ return probe.streams?.some((stream) => stream.codec_type === 'video') === true
67
+ && probe.frames?.some((frame) => frame.media_type === 'video') === true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+ /** 상태 파일은 `reel/` «밖»에 둔다 — `reel.sh` 가 시작할 때 `reel/` 을 지우므로 안에 두면 렌더 중에 사라진다. */
73
+ export function fieldReelStatusPath(dir: string): string { return join(dir, '.reel-status.json'); }
74
+ const statusPath = fieldReelStatusPath;
75
+ function writeStatus(dir: string, status: Omit<FieldReelStatus, 'updatedAt'>): void {
76
+ mkdirSync(dir, { recursive: true });
77
+ const tmp = join(dir, `.reel-status-${randomUUID()}`);
78
+ try {
79
+ writeFileSync(tmp, JSON.stringify({ ...status, updatedAt: new Date().toISOString() }));
80
+ renameSync(tmp, statusPath(dir));
81
+ } finally {
82
+ if (existsSync(tmp)) unlinkSync(tmp);
83
+ }
84
+ }
85
+
86
+ export function readFieldReelStatus(dir: string): FieldReelStatus | null {
87
+ try { return JSON.parse(readFileSync(statusPath(dir), 'utf8')) as FieldReelStatus; }
88
+ catch (err) {
89
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
90
+ throw err;
91
+ }
92
+ }
93
+
94
+ export const defaultFieldReelRunner: FieldReelRunner = (dir, { title, sub }) => new Promise((finish) => {
95
+ const script = resolve(import.meta.dir, '../../skills/explainer-video/engine/reel.sh');
96
+ const started = Date.now();
97
+ const child = spawn('zsh', [script, dir, '--title', title, '--sub', sub], { stdio: 'ignore' });
98
+ let settled = false;
99
+ const done = (error?: string) => {
100
+ if (settled) return;
101
+ settled = true;
102
+ const file = fieldReelFile(dir);
103
+ const seconds = Math.round((Date.now() - started) / 1000);
104
+ // 디코딩 검사(ffprobe)는 `render()` 한 곳에서만 한다 — 여기선 존재만 본다(리뷰: 검사 두 번 금지).
105
+ finish(error || !hasFieldReelFile(file)
106
+ ? { ok: false, seconds, error: error ?? `invalid or missing MP4 output: ${file}` }
107
+ : { ok: true, file, seconds });
108
+ };
109
+ child.once('error', (err) => done(err.message));
110
+ child.once('close', (code) => done(code === 0 ? undefined : `reel.sh exited ${code ?? 'unknown'}`));
111
+ });
112
+
113
+ function arm(dir: string, job: Job): void {
114
+ if (job.timer) clearTimeout(job.timer);
115
+ job.timer = setTimeout(() => {
116
+ job.timer = undefined;
117
+ if (!job.running) void render(dir, job);
118
+ }, Math.max(0, (job.opts.quietMs ?? 20_000) - (Date.now() - job.lastUploadAt)));
119
+ job.timer.unref?.();
120
+ }
121
+
122
+ async function render(dir: string, job: Job): Promise<void> {
123
+ job.running = true;
124
+ job.dirty = false;
125
+ const items = listFieldMedia(dir).length;
126
+ const startedAt = new Date().toISOString();
127
+ const event = dir.split(/[\\/]/).at(-1)!;
128
+ // 현재 렌더의 수신자 스냅샷. 렌더 중 업로드의 수신자는 다음 렌더로 넘긴다.
129
+ const callbacks = job.callbacks;
130
+ const renderOpts = job.opts;
131
+ job.callbacks = new Map();
132
+ writeStatus(dir, { state: 'rendering', items, startedAt });
133
+ mkdirSync(join(dir, 'reel'), { recursive: true }); // 실행기 계약 — 산출 폴더는 있다(reel.sh 는 스스로 다시 만든다)
134
+ debug.log('field.reel', 'started', { event, items });
135
+ let result: FieldReelResult;
136
+ try {
137
+ const date = new Date();
138
+ const sub = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
139
+ let titleLines: string[] = [];
140
+ try { titleLines = readFileSync(join(dir, 'title.txt'), 'utf8').split(/\r?\n/); }
141
+ catch (err) { if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err; }
142
+ result = await (renderOpts.runner ?? defaultFieldReelRunner)(dir, {
143
+ title: titleLines[0]?.trim() || renderOpts.title || event,
144
+ sub: titleLines[1]?.trim() || renderOpts.sub || sub,
145
+ });
146
+ } catch (err) {
147
+ result = { ok: false, seconds: 0, error: err instanceof Error ? err.message : String(err) };
148
+ }
149
+ if (result.ok && (!result.file || resolve(result.file) !== fieldReelFile(dir) || !isValidFieldReelFile(result.file))) {
150
+ result = { ok: false, seconds: result.seconds, error: 'render output invalid or missing' };
151
+ }
152
+ const finishedAt = new Date().toISOString();
153
+ writeStatus(dir, { state: result.ok ? 'done' : 'failed', items, startedAt, finishedAt,
154
+ seconds: result.seconds, ...(result.ok ? { file: result.file } : { error: result.error ?? 'render failed' }) });
155
+ debug.log('field.reel', result.ok ? 'done' : 'failed', { event, items, seconds: result.seconds });
156
+ if (result.ok) {
157
+ // EV10b: 엔진이 timeline.json 에 남긴 비전 호출 수·성공·«현장 N» 강등·걸린 ms — codex 잔량과 겹치므로 관측한다(OP 10-01).
158
+ try {
159
+ const t = JSON.parse(readFileSync(join(dir, 'reel', 'timeline.json'), 'utf8')) as { vision?: Record<string, unknown>; music?: unknown };
160
+ if (t.vision) debug.log('field.reel', 'vision', { event, ...t.vision, music: t.music === true });
161
+ } catch { debug.log('field.reel', 'vision-unread', { event }); }
162
+ }
163
+ job.running = false;
164
+ if (job.dirty) {
165
+ writeStatus(dir, { state: 'waiting', items: listFieldMedia(dir).length });
166
+ arm(dir, job);
167
+ } else {
168
+ jobs.delete(dir);
169
+ }
170
+ for (const cb of callbacks.values()) {
171
+ void Promise.resolve().then(() => cb(result)).catch((err) => {
172
+ debug.log('field.reel', 'delivery-failed', { event, error: String(err) });
173
+ });
174
+ }
175
+ }
176
+
177
+ /** 저장 성공 직후 호출한다. quiet 기간의 연속 업로드를 합치고 렌더 중 변경은 뒤이어 다시 렌더한다. */
178
+ export function scheduleFieldReel(dir: string, opts: FieldReelOptions = {}): void {
179
+ dir = resolve(dir);
180
+ const event = dir.split(/[\\/]/).at(-1)!;
181
+ let job = jobs.get(dir);
182
+ if (!job) {
183
+ job = { running: false, dirty: false, lastUploadAt: Date.now(), opts, callbacks: new Map() };
184
+ jobs.set(dir, job);
185
+ }
186
+ job.opts = { ...opts, runner: opts.runner ?? job.opts.runner, quietMs: opts.quietMs ?? job.opts.quietMs };
187
+ job.lastUploadAt = Date.now();
188
+ if (opts.onDone) job.callbacks.set(opts.notificationKey ?? 'default', opts.onDone);
189
+ const items = listFieldMedia(dir).length;
190
+ if (job.running) job.dirty = true;
191
+ else {
192
+ writeStatus(dir, { state: 'waiting', items });
193
+ arm(dir, job);
194
+ }
195
+ debug.log('field.reel', 'scheduled', { event, items });
196
+ }
@@ -35,11 +35,13 @@ export const GROK_SUBSCRIPTION_BASE_URL = 'https://cli-chat-proxy.grok.com/v1';
35
35
  /** API 키 경로 베이스(토큰 과금). */
36
36
  export const GROK_API_BASE_URL = 'https://api.x.ai/v1';
37
37
 
38
- /** 프록시가 요구하는 최소 CLI 버전(실측 426 문면). 이보다 낮으면 거절된다. */
39
- export const GROK_PROXY_MIN_CLI_VERSION = '0.1.202';
40
- /** 설치본을 못 읽었을 때 신고할 값. ⛔ 하한을 쓰면 프록시가 하한을 «올리는» 날
41
- * 전부 426 이 된다 — 그래서 아래 `detectGrokCliVersion()` 이 1순위다. */
42
- const GROK_PROXY_FALLBACK_CLI_VERSION = GROK_PROXY_MIN_CLI_VERSION;
38
+ /** 프록시가 요구하는 최소 CLI 버전(실측 426 문면). 이보다 낮으면 거절된다.
39
+ * 2026-10-01 실측: «Your Grok CLI version (0.1.202) is outdated. Please update to version 1.0.13 or later». */
40
+ export const GROK_PROXY_MIN_CLI_VERSION = '1.0.13';
41
+ /** 설치본을 못 읽었을 때(grok CLI 없는 기계 · 파드) 신고할 값 — 하한이 아니라 실측된 최근 설치본.
42
+ * ⛔ 하한을 쓰면 프록시가 하한을 «올리는» 날 전부 426 이 된다(10-01 파드 자식이 0.1.202 로 전부 거절됨).
43
+ * 그래서 아래 `detectGrokCliVersion()` 이 1순위다. */
44
+ const GROK_PROXY_FALLBACK_CLI_VERSION = '1.0.41';
43
45
 
44
46
  /** 설치된 grok CLI 의 버전. ⭐ 하드코딩 하한 대신 «실제 설치본»을 신고한다.
45
47
  *
@@ -0,0 +1,33 @@
1
+ import { stripScreenAnsi } from './harness-screen.js';
2
+
3
+ export type AsideRunStatus = {
4
+ kind: 'ok' | 'not-running' | 'auth-timeout' | 'repl-error' | 'no-marker';
5
+ hint?: string;
6
+ };
7
+
8
+ export function classifyAsideRun({ stdout, stderr, exitCode, timedOut = false }: {
9
+ stdout: string;
10
+ stderr: string;
11
+ exitCode: number | null;
12
+ /** We killed `aside` at our own timeout. */
13
+ timedOut?: boolean;
14
+ }): AsideRunStatus {
15
+ const diagnosticStdout = stripScreenAnsi(stdout).split(/\r?\n/).filter((line) => {
16
+ try { JSON.parse(line.trim()); return false; } catch { return true; }
17
+ }).join('\n');
18
+ const diagnostics = `${diagnosticStdout}\n${stripScreenAnsi(stderr)}`;
19
+ if (exitCode !== 0 && /Aside isn't running/i.test(diagnostics)) {
20
+ return { kind: 'not-running', hint: 'Aside 브라우저가 안 떠 있음 — Aside 브라우저를 켜고 다시 · 또는 `aside exec --host <host>`' };
21
+ }
22
+ if (exitCode !== 0 && /daemon auth challenge/i.test(diagnostics) && /timeout|timed out/i.test(diagnostics)) {
23
+ return { kind: 'auth-timeout', hint: 'Aside 인증 요청 시간 초과 — Aside 브라우저를 켜고 다시 · 또는 `aside exec --host <host>`' };
24
+ }
25
+ // Killed by our timeout with nothing printed: aside never answered — the browser daemon is not reachable.
26
+ if (timedOut && !stripScreenAnsi(stdout).trim()) {
27
+ return { kind: 'auth-timeout', hint: 'Aside 가 응답하지 않음 — Aside 브라우저가 떠 있는지 확인하고 다시 · 또는 `aside exec --host <host>`' };
28
+ }
29
+ const marker = stripScreenAnsi(stdout).trimEnd().match(/\[(ok|error)\s*\|[^\]]+\]\s*$/);
30
+ if (marker?.[1] === 'error') return { kind: 'repl-error' };
31
+ if (marker?.[1] === 'ok') return { kind: 'ok' };
32
+ return { kind: 'no-marker' };
33
+ }