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,117 @@
1
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { dirname, join } from 'node:path';
4
+ import { debug } from '../../debug/log.js';
5
+ import { sendOutbound } from '../../domains/outbound-alert.js';
6
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
7
+ import { jsonResponse } from './json-response.js';
8
+
9
+ export const CONSULT_REQUESTS_PATH = '/v1/consult-requests';
10
+
11
+ interface ConsultRequest {
12
+ receiptId: string;
13
+ name: string;
14
+ org: string | null;
15
+ kind: 'company' | 'personal';
16
+ interest: 'A' | 'B';
17
+ contact: string;
18
+ consent: true;
19
+ receivedAt: string;
20
+ }
21
+
22
+ export interface ConsultRequestsDeps {
23
+ root?: () => string;
24
+ now?: () => string;
25
+ send?: typeof sendOutbound;
26
+ }
27
+
28
+ function file(root: string): string {
29
+ return join(root, 'consult-requests', 'requests.jsonl');
30
+ }
31
+
32
+ function append(path: string, entry: ConsultRequest): void {
33
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
34
+ const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
35
+ const temp = `${path}.${randomUUID()}.tmp`;
36
+ try {
37
+ writeFileSync(temp, `${previous}${JSON.stringify(entry)}\n`, { mode: 0o600 });
38
+ const fd = openSync(temp, 'r');
39
+ try { fsyncSync(fd); } finally { closeSync(fd); }
40
+ renameSync(temp, path);
41
+ } catch (error) {
42
+ try { unlinkSync(temp); } catch { /* no temp file to remove */ }
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ function records(path: string): ConsultRequest[] {
48
+ try {
49
+ return readFileSync(path, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line) as ConsultRequest);
50
+ } catch (error) {
51
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ function rejected(field: string): Response {
57
+ debug.log('consult.request', 'rejected', { field });
58
+ return jsonResponse({ error: 'bad_request', field }, 400);
59
+ }
60
+
61
+ function singleLine(value: string): string {
62
+ return value.trim().replace(/[\r\n\u2028\u2029]+/g, ' ');
63
+ }
64
+
65
+ export async function handleConsultRequests(req: Request, deps: ConsultRequestsDeps = {}): Promise<Response> {
66
+ if (req.method !== 'GET' && req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
67
+ const path = file((deps.root ?? effectiveInstanceRoot)());
68
+ if (req.method === 'GET') {
69
+ const rawLimit = new URL(req.url).searchParams.get('limit');
70
+ const limit = rawLimit === null ? 20 : Number(rawLimit);
71
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) return rejected('limit');
72
+ try {
73
+ const items = records(path).reverse().slice(0, limit).map(({ receiptId, name, org, kind, interest, receivedAt }) =>
74
+ ({ receiptId, name, org, kind, interest, receivedAt }));
75
+ return jsonResponse({ items });
76
+ } catch {
77
+ return jsonResponse({ error: 'journal-unavailable' }, 503);
78
+ }
79
+ }
80
+
81
+ let body: unknown;
82
+ try { body = await req.json(); }
83
+ catch { return rejected('body'); }
84
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return rejected('body');
85
+ const input = body as Record<string, unknown>;
86
+ for (const field of ['name', 'contact'] as const) {
87
+ if (typeof input[field] !== 'string' || !input[field].trim()) return rejected(field);
88
+ }
89
+ if (input.org !== undefined && (typeof input.org !== 'string' || !input.org.trim())) return rejected('org');
90
+ if (input.kind !== 'company' && input.kind !== 'personal') return rejected('kind');
91
+ if (input.interest !== 'A' && input.interest !== 'B') return rejected('interest');
92
+ if (input.consent !== true) return rejected('consent');
93
+
94
+ const entry: ConsultRequest = {
95
+ receiptId: `R-${randomUUID()}`,
96
+ name: singleLine(input.name as string),
97
+ org: typeof input.org === 'string' ? singleLine(input.org) : null,
98
+ kind: input.kind,
99
+ interest: input.interest,
100
+ contact: (input.contact as string).trim(),
101
+ consent: true,
102
+ receivedAt: (deps.now ?? (() => new Date().toISOString()))(),
103
+ };
104
+ try { append(path, entry); }
105
+ catch {
106
+ debug.log('consult.request', 'rejected', { field: 'journal' });
107
+ return jsonResponse({ error: 'journal-unavailable' }, 503);
108
+ }
109
+ debug.log('consult.request', 'accepted', { receiptId: entry.receiptId });
110
+ try {
111
+ (deps.send ?? sendOutbound)(
112
+ `📮 상담 문의 ${entry.receiptId} · ${entry.name}(${entry.kind === 'personal' ? '개인' : (entry.org ?? '회사')}) · 관심 ${entry.interest}\n앱에서 보기`,
113
+ 'alert',
114
+ );
115
+ } catch { /* the local receipt remains accepted if delivery is unavailable */ }
116
+ return jsonResponse({ receiptId: entry.receiptId, receivedAt: entry.receivedAt }, 202);
117
+ }
@@ -66,6 +66,8 @@ export interface NexusDiscordTriggerBotOpts {
66
66
  * commands (/cc·/fork·/voice-join·…) route into the same composed
67
67
  * message pipeline. Absent ⇒ interactions ignored (legacy). */
68
68
  onInteraction?: (raw: Record<string, unknown>) => void | Promise<void>;
69
+ /** EV12c — guild text channels answered like DMs (`discord.chatChannels`). Absent ⇒ DM-only. */
70
+ guildTextChannels?: string[];
69
71
  /** Optional logger. Defaults to `[discord/trigger]` prefix. */
70
72
  log?: (msg: string) => void;
71
73
  /** Test seam — pre-built DiscordBot (factory skips construction +
@@ -156,6 +158,7 @@ export function createNexusDiscordTriggerBot(
156
158
  const bot = opts.bot ?? new DiscordBot({
157
159
  token: opts.token,
158
160
  allowedUsers: opts.allowedUsers,
161
+ ...(opts.guildTextChannels?.length ? { guildTextChannels: opts.guildTextChannels } : {}),
159
162
  // Chat path: the injected self-turn handler (M4b nexus wire) or the
160
163
  // legacy trigger-only no-op. Workflows wanting a chat reply can
161
164
  // still declare a `chatTrigger` (routes via `/v1/workflows/chat/*`).
@@ -0,0 +1,51 @@
1
+ import { extname } from 'node:path';
2
+ import { ExecRequestRunner } from '../../exec-requests/runner.js';
3
+ import { jsonResponse } from './json-response.js';
4
+
5
+ const mime: Record<string, string> = {
6
+ '.pdf': 'application/pdf', '.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm',
7
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp',
8
+ '.md': 'text/markdown; charset=utf-8', '.txt': 'text/plain; charset=utf-8',
9
+ };
10
+
11
+ export async function handleExecRequests(req: Request, runner: ExecRequestRunner): Promise<Response> {
12
+ const path = new URL(req.url).pathname;
13
+ if (path === '/v1/exec-requests') {
14
+ if (req.method === 'POST') {
15
+ let body: unknown;
16
+ try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
17
+ const text = body && typeof body === 'object' && !Array.isArray(body) ? (body as { text?: unknown }).text : undefined;
18
+ if (typeof text !== 'string' || !text.trim()) return jsonResponse({ error: 'bad_request', reason: 'text required' }, 400);
19
+ const item = runner.submit(text.trim());
20
+ return jsonResponse({ id: item.id, status: 'planning' }, 202);
21
+ }
22
+ if (req.method === 'GET') return jsonResponse({ items: runner.store.list().map(item => {
23
+ const current = runner.get(item.id) ?? item;
24
+ return { id: current.id, text: current.text, createdAt: current.createdAt, status: current.status,
25
+ seats: current.seats.map(({ seat, title, status }) => ({ seat, title, status })), resultCount: current.results.length };
26
+ }) });
27
+ return jsonResponse({ error: 'method-not-allowed' }, 405);
28
+ }
29
+ const file = /^\/v1\/exec-requests\/([^/]+)\/files\/([^/]+)$/.exec(path);
30
+ if (file) {
31
+ if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
32
+ let id: string, name: string;
33
+ try { id = decodeURIComponent(file[1]!); name = decodeURIComponent(file[2]!); }
34
+ catch { return jsonResponse({ error: 'not-found' }, 404); }
35
+ const path = runner.file(id, name);
36
+ if (!path) return jsonResponse({ error: 'not-found' }, 404);
37
+ return new Response(Bun.file(path), { headers: { 'content-type': mime[extname(path).toLowerCase()] ?? 'application/octet-stream', 'x-content-type-options': 'nosniff' } });
38
+ }
39
+ const detail = /^\/v1\/exec-requests\/([^/]+)$/.exec(path);
40
+ if (detail) {
41
+ if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
42
+ let id: string;
43
+ try { id = decodeURIComponent(detail[1]!); } catch { return jsonResponse({ error: 'not-found' }, 404); }
44
+ const item = runner.get(id);
45
+ if (!item) return jsonResponse({ error: 'not-found' }, 404);
46
+ const { seats, ...rest } = item;
47
+ return jsonResponse({ ...rest, seats: seats.map(({ seat, title, status, graphId, runId, reason }) =>
48
+ ({ seat, title, status, graphId, runId, ...(reason ? { reason } : {}) })) });
49
+ }
50
+ return jsonResponse({ error: 'not-found' }, 404);
51
+ }
@@ -0,0 +1,180 @@
1
+ // 현장 업로드 입구 — POST/GET /v1/field/uploads (owner 전용).
2
+ // 저장 규칙은 src/field/field-media.ts 가 유일한 정본이고 텔레그램 `#현장` 도 같은 함수를 쓴다.
3
+
4
+ import { debug } from '../../debug/log.js';
5
+ import { readFieldReelStatus, scheduleFieldReel, fieldReelFile, hasFieldReelFile, type FieldReelOptions } from '../../field/field-reel.js';
6
+ import { getElanousConfigDir } from '../../elanous-config-dir.js';
7
+ import {
8
+ FIELD_MAX_FILE_BYTES,
9
+ FIELD_MAX_REQUEST_BYTES,
10
+ FieldUploadError,
11
+ defaultFieldEvent,
12
+ fieldEventDir,
13
+ isFieldSlug,
14
+ listFieldMedia,
15
+ saveFieldMedia,
16
+ type FieldMediaInput,
17
+ } from '../../field/field-media.js';
18
+ import { jsonResponse } from './json-response.js';
19
+
20
+ export const FIELD_UPLOADS_PATH = '/v1/field/uploads';
21
+ export const FIELD_REEL_PATH = '/v1/field/reel';
22
+ export const FIELD_REEL_FILE_PATH = '/v1/field/reel/file';
23
+ const reelUrl = (event: string) => `${FIELD_REEL_FILE_PATH}?event=${encodeURIComponent(event)}`;
24
+
25
+ /** 멀티파트 경계·헤더 여유. */
26
+ const MULTIPART_SLACK_BYTES = 4 * 1024 * 1024;
27
+ /** JSON 본문은 base64(×4/3) 라 더 크다. */
28
+ const JSON_MAX_BODY_BYTES = Math.ceil((FIELD_MAX_REQUEST_BYTES * 4) / 3) + MULTIPART_SLACK_BYTES;
29
+ /** Bun.serve 의 `maxRequestBodySize`(기본 128 MB) — 이 입구의 JSON 상한까지 받도록 서버가 이 값을 쓴다. */
30
+ export const FIELD_SERVER_MAX_BODY_BYTES = JSON_MAX_BODY_BYTES;
31
+
32
+ interface FieldUploadsDeps {
33
+ authorize?: (request: Request) => boolean;
34
+ /** config 뿌리 — 기본 `getElanousConfigDir()`(데몬의 다른 저장소와 같은 해석). 시험은 temp 를 준다. */
35
+ rootDir?: () => string;
36
+ now?: () => Date;
37
+ reel?: FieldReelOptions;
38
+ /** 설정 `telegram.fieldDefaultEvent` — 앱이 «오늘» 행사 칸의 기본값으로 받는다(비면 field-<날짜>). */
39
+ configuredEvent?: () => string | undefined;
40
+ }
41
+
42
+ function errorResponse(error: FieldUploadError): Response {
43
+ return jsonResponse({ ok: false, error: error.code, reason: error.message }, error.status);
44
+ }
45
+
46
+ function parseCapturedAtList(values: FormDataEntryValue[]): Array<string | undefined> {
47
+ const strings = values.filter((v): v is string => typeof v === 'string');
48
+ if (strings.length === 1 && strings[0]!.trim().startsWith('[')) {
49
+ try {
50
+ const arr = JSON.parse(strings[0]!) as unknown;
51
+ if (Array.isArray(arr)) return arr.map((v) => (typeof v === 'string' || typeof v === 'number' ? String(v) : undefined));
52
+ } catch { /* 배열이 아니면 단일 값으로 */ }
53
+ }
54
+ return strings;
55
+ }
56
+
57
+ async function readMultipart(req: Request): Promise<FieldMediaInput[]> {
58
+ let form: FormData;
59
+ try { form = await req.formData(); }
60
+ catch { throw new FieldUploadError('bad-request', 'invalid multipart body'); }
61
+ const files = form.getAll('file').filter((v): v is File => typeof v !== 'string');
62
+ const captured = parseCapturedAtList(form.getAll('capturedAt'));
63
+ const captions = form.getAll('caption').filter((v): v is string => typeof v === 'string');
64
+ let total = 0;
65
+ const inputs: FieldMediaInput[] = [];
66
+ for (const [i, file] of files.entries()) {
67
+ if (file.size > FIELD_MAX_FILE_BYTES) throw new FieldUploadError('too-large', 'a file exceeds 100 MB');
68
+ total += file.size;
69
+ if (total > FIELD_MAX_REQUEST_BYTES) throw new FieldUploadError('too-large', 'the request exceeds 250 MB');
70
+ inputs.push({
71
+ originalName: file.name || 'media',
72
+ mimeType: file.type,
73
+ bytes: new Uint8Array(await file.arrayBuffer()),
74
+ ...(captured[i] ? { capturedAt: captured[i] } : {}),
75
+ ...((captions.length === 1 ? i === 0 : i < captions.length) ? { caption: captions[i] } : {}),
76
+ });
77
+ }
78
+ return inputs;
79
+ }
80
+
81
+ async function readJson(req: Request): Promise<FieldMediaInput[]> {
82
+ let body: unknown;
83
+ try { body = await req.json(); }
84
+ catch { throw new FieldUploadError('bad-request', 'JSON body required'); }
85
+ const files = body && typeof body === 'object' && !Array.isArray(body) ? (body as { files?: unknown }).files : undefined;
86
+ if (!Array.isArray(files)) throw new FieldUploadError('bad-request', 'files[] required');
87
+ return files.map((raw) => {
88
+ const f = (raw ?? {}) as Record<string, unknown>;
89
+ if (typeof f.name !== 'string' || typeof f.dataBase64 !== 'string') {
90
+ throw new FieldUploadError('bad-request', 'each file needs name and dataBase64');
91
+ }
92
+ // base64 길이로 먼저 거른다 — 파일 상한(FIELD_MAX_FILE_BYTES)을 넘는 것을 디코드하지 않는다.
93
+ if (Math.floor((f.dataBase64.length * 3) / 4) > FIELD_MAX_FILE_BYTES + 3) {
94
+ throw new FieldUploadError('too-large', 'a file exceeds 100 MB');
95
+ }
96
+ const capturedAt = typeof f.capturedAt === 'string' || typeof f.capturedAt === 'number' ? f.capturedAt : undefined;
97
+ return {
98
+ originalName: f.name,
99
+ ...(typeof f.mimeType === 'string' ? { mimeType: f.mimeType } : {}),
100
+ bytes: new Uint8Array(Buffer.from(f.dataBase64, 'base64')),
101
+ ...(capturedAt !== undefined ? { capturedAt } : {}),
102
+ ...(typeof f.caption === 'string' ? { caption: f.caption } : {}),
103
+ };
104
+ });
105
+ }
106
+
107
+ export async function handleFieldUploads(req: Request, deps: FieldUploadsDeps = {}): Promise<Response> {
108
+ const url = new URL(req.url);
109
+ if (![FIELD_UPLOADS_PATH, FIELD_REEL_PATH, FIELD_REEL_FILE_PATH].includes(url.pathname)) return jsonResponse({ error: 'not-found' }, 404);
110
+ if (!deps.authorize?.(req)) return jsonResponse({ error: 'unauthorized' }, 401);
111
+ const rootDir = (deps.rootDir ?? getElanousConfigDir)();
112
+ const event = url.searchParams.get('event') ?? '';
113
+ // event 없는 GET = 기본 행사 이름만(앱 «오늘» 행사 칸 · 텔레그램 `#현장` 과 같은 기본).
114
+ if (req.method === 'GET' && !url.searchParams.has('event')) {
115
+ const defaultEvent = defaultFieldEvent((deps.configuredEvent ?? (() => undefined))(), (deps.now ?? (() => new Date()))());
116
+ return jsonResponse({ defaultEvent });
117
+ }
118
+ if (!isFieldSlug(event)) return errorResponse(new FieldUploadError('bad-slug', 'event slug must match ^[a-z0-9][a-z0-9._-]{0,63}$'));
119
+
120
+ const dir = fieldEventDir(rootDir, event);
121
+ if (url.pathname !== FIELD_UPLOADS_PATH && req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
122
+ if (url.pathname === FIELD_REEL_PATH) {
123
+ const status = readFieldReelStatus(dir);
124
+ if (!status) return jsonResponse({ error: 'not-found' }, 404);
125
+ return jsonResponse({ event, state: status.state, items: status.items,
126
+ ...(status.seconds !== undefined ? { seconds: status.seconds } : {}),
127
+ ...(status.finishedAt ? { finishedAt: status.finishedAt } : {}),
128
+ ...(status.state === 'failed' && status.error ? { error: status.error } : {}),
129
+ ...(status.state === 'done' && hasFieldReelFile(fieldReelFile(dir)) ? { url: reelUrl(event) } : {}) });
130
+ }
131
+ if (url.pathname === FIELD_REEL_FILE_PATH) {
132
+ const status = readFieldReelStatus(dir);
133
+ const file = fieldReelFile(dir);
134
+ if (status?.state !== 'done' || !hasFieldReelFile(file)) return jsonResponse({ error: 'not-found' }, 404);
135
+ return new Response(Bun.file(file), { headers: { 'content-type': 'video/mp4' } });
136
+ }
137
+ if (req.method === 'GET') {
138
+ const files = listFieldMedia(dir);
139
+ const status = readFieldReelStatus(dir);
140
+ debug.log('field.upload', 'listed', { count: files.length });
141
+ return jsonResponse({ event, count: files.length, files,
142
+ ...(status ? { reel: { status: status.state, updatedAt: status.updatedAt,
143
+ ...(status.state === 'done' && hasFieldReelFile(fieldReelFile(dir)) ? { url: reelUrl(event) } : {}) } } : {}) });
144
+ }
145
+ if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
146
+
147
+ const device = url.searchParams.get('device') ?? '';
148
+ if (!isFieldSlug(device)) return errorResponse(new FieldUploadError('bad-slug', 'device slug must match ^[a-z0-9][a-z0-9._-]{0,63}$'));
149
+
150
+ const contentType = (req.headers.get('content-type') ?? '').toLowerCase();
151
+ const isMultipart = contentType.startsWith('multipart/form-data');
152
+ const isJson = contentType.startsWith('application/json');
153
+ if (!isMultipart && !isJson) {
154
+ return errorResponse(new FieldUploadError('unsupported-media-type', 'use multipart/form-data or application/json'));
155
+ }
156
+ const declared = Number(req.headers.get('content-length') ?? '');
157
+ const bodyCap = isJson ? JSON_MAX_BODY_BYTES : FIELD_MAX_REQUEST_BYTES + MULTIPART_SLACK_BYTES;
158
+ if (Number.isFinite(declared) && declared > bodyCap) {
159
+ debug.log('field.upload', 'refused', { reason: 'too-large', declaredBytes: declared });
160
+ return errorResponse(new FieldUploadError('too-large', 'the request exceeds 250 MB'));
161
+ }
162
+
163
+ try {
164
+ const inputs = isMultipart ? await readMultipart(req) : await readJson(req);
165
+ const caption = url.searchParams.get('caption') ?? undefined;
166
+ const result = saveFieldMedia(inputs, { rootDir, event, device, ...(caption ? { caption } : {}), ...(deps.now ? { now: deps.now } : {}) });
167
+ debug.log('field.upload', 'saved', {
168
+ surface: 'http', files: result.saved.length, bytes: result.saved.reduce((n, s) => n + s.bytes, 0), count: result.count,
169
+ });
170
+ scheduleFieldReel(result.dir, deps.reel);
171
+ return jsonResponse({ ok: true, event, saved: result.saved, count: result.count });
172
+ } catch (error) {
173
+ if (error instanceof FieldUploadError) {
174
+ debug.log('field.upload', 'refused', { surface: 'http', reason: error.code });
175
+ return errorResponse(error);
176
+ }
177
+ debug.log('field.upload', 'failed', { surface: 'http' });
178
+ throw error;
179
+ }
180
+ }
@@ -0,0 +1,140 @@
1
+ // EV10d — «게시 대기» 피드 초안: 승인 노드가 `{"kind":"feed-preview"}` 이면 그 런의 현장폴더 초안
2
+ // (`<folder>/feed/feed-draft.json`)이 정본이다(계약 = MK 10-01 · field-feed 그래프). 경로는 «런 상태의
3
+ // input.folder» 에서만 구하고, 그 폴더는 `<configDir>/field/<slug>` 꼴일 때만 받는다(realpath 로 탈출 거부).
4
+ // 사람의 «수정 저장»은 허용된 칸만 고쳐 그 파일을 덮는다 — 승인 상태는 바꾸지 않는다.
5
+ import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
6
+ import { basename, dirname, join, normalize, sep } from 'node:path';
7
+ import { fieldEventDir, isFieldSlug } from '../../field/field-media.js';
8
+ import type { GraphRunState } from '../../graph-runner/runner.js';
9
+
10
+ export interface FeedSlide { image: string; caption: string; include: boolean; [key: string]: unknown }
11
+ export interface FeedDraft {
12
+ kind: 'feed-draft';
13
+ version: 1;
14
+ revision: number;
15
+ updatedAt: string;
16
+ updatedBy: string;
17
+ slides: FeedSlide[];
18
+ caption: { hook: string; body: string };
19
+ hashtags: string[];
20
+ cover: { text: string; sub: string; image?: string; [key: string]: unknown };
21
+ location: string | null;
22
+ reel: string | null;
23
+ brand?: { name?: string; handle?: string; avatar?: string | null };
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ /** Fields a person may change from the PWA — everything else (rendered*, source, folder…) is preserved. */
28
+ export interface FeedEdit {
29
+ slides: Array<{ image: string; caption?: string; include?: boolean }>;
30
+ caption: { hook: string; body: string };
31
+ hashtags: string[];
32
+ cover: { text: string; sub: string };
33
+ location: string | null;
34
+ }
35
+
36
+ export const FEED_DRAFT_FILE = join('feed', 'feed-draft.json');
37
+
38
+ export function isFeedPreviewMessage(message: string): boolean {
39
+ try {
40
+ const parsed = JSON.parse(message) as unknown;
41
+ return !!parsed && typeof parsed === 'object' && (parsed as { kind?: unknown }).kind === 'feed-preview';
42
+ } catch { return false; }
43
+ }
44
+
45
+ function real(path: string): string | null {
46
+ try { return realpathSync(path); } catch { return null; }
47
+ }
48
+
49
+ /** The run's field folder when it is exactly `<configDir>/field/<slug>` (after resolving links), else null. */
50
+ export function feedFolderFor(state: Pick<GraphRunState, 'input'>, configDir: string): string | null {
51
+ const input = state.input as { folder?: unknown } | undefined;
52
+ const folder = typeof input?.folder === 'string' ? input.folder : null;
53
+ if (!folder) return null;
54
+ const slug = basename(folder);
55
+ if (!isFieldSlug(slug)) return null;
56
+ const resolved = real(folder);
57
+ const expected = real(fieldEventDir(configDir, slug));
58
+ if (!resolved || !expected || resolved !== expected) return null;
59
+ if (dirname(resolved) !== real(join(configDir, 'field'))) return null;
60
+ return resolved;
61
+ }
62
+
63
+ function str(value: unknown): string { return typeof value === 'string' ? value : ''; }
64
+
65
+ export function readFeedDraft(folder: string): FeedDraft | null {
66
+ try {
67
+ const raw = JSON.parse(readFileSync(join(folder, FEED_DRAFT_FILE), 'utf8')) as Partial<FeedDraft>;
68
+ if (raw.kind !== 'feed-draft' || raw.version !== 1 || !Array.isArray(raw.slides)) return null;
69
+ return raw as FeedDraft;
70
+ } catch { return null; }
71
+ }
72
+
73
+ export type FeedEditResult = { ok: true; draft: FeedDraft } | { ok: false; error: string };
74
+
75
+ /** Apply a person's edit to the current draft. Slides must be the same set of images (order may change,
76
+ * removal is `include:false`), so no new path can enter the draft. */
77
+ export function applyFeedEdit(current: FeedDraft, edit: unknown, now = new Date()): FeedEditResult {
78
+ if (!edit || typeof edit !== 'object') return { ok: false, error: 'bad_request' };
79
+ const e = edit as Partial<FeedEdit>;
80
+ if (!Array.isArray(e.slides) || !e.caption || typeof e.caption !== 'object' || !Array.isArray(e.hashtags)
81
+ || !e.cover || typeof e.cover !== 'object') return { ok: false, error: 'bad_request' };
82
+ const original = new Map(current.slides.map((slide) => [slide.image, slide]));
83
+ const seen = new Set<string>();
84
+ const slides: FeedSlide[] = [];
85
+ for (const raw of e.slides) {
86
+ const image = str((raw as { image?: unknown })?.image);
87
+ const base = original.get(image);
88
+ if (!base || seen.has(image)) return { ok: false, error: 'unknown-image' };
89
+ seen.add(image);
90
+ const caption = (raw as { caption?: unknown }).caption;
91
+ const include = (raw as { include?: unknown }).include;
92
+ slides.push({ ...base, ...(typeof caption === 'string' ? { caption } : {}), ...(typeof include === 'boolean' ? { include } : {}) });
93
+ }
94
+ if (seen.size !== original.size) return { ok: false, error: 'missing-image' };
95
+ const location = e.location === null ? null : typeof e.location === 'string' ? e.location : current.location;
96
+ return {
97
+ ok: true,
98
+ draft: {
99
+ ...current,
100
+ slides,
101
+ caption: { ...current.caption, hook: str(e.caption.hook), body: str(e.caption.body) },
102
+ hashtags: e.hashtags.filter((tag): tag is string => typeof tag === 'string').map((tag) => tag.trim()).filter(Boolean),
103
+ cover: { ...current.cover, text: str(e.cover.text), sub: str(e.cover.sub) },
104
+ location,
105
+ revision: (Number.isFinite(current.revision) ? current.revision : 0) + 1,
106
+ updatedBy: 'human',
107
+ updatedAt: now.toISOString(),
108
+ },
109
+ };
110
+ }
111
+
112
+ export function writeFeedDraft(folder: string, draft: FeedDraft): void {
113
+ const target = join(folder, FEED_DRAFT_FILE);
114
+ const temp = `${target}.tmp-${process.pid}`;
115
+ writeFileSync(temp, `${JSON.stringify(draft, null, 2)}\n`);
116
+ renameSync(temp, target);
117
+ }
118
+
119
+ const MEDIA_RE = /^(?:feed\/[^/\\]+\.(?:png|jpe?g|webp)|reel\/reel-9x16\.mp4)$/i;
120
+
121
+ /** Absolute file for a folder-relative media path, only `feed/*.png|jpg|jpeg|webp` and `reel/reel-9x16.mp4`. */
122
+ export function resolveFeedMedia(folder: string, relative: string): string | null {
123
+ if (!relative || relative.startsWith('/') || relative.includes('\\') || relative.includes('\0')) return null;
124
+ const clean = normalize(relative).split(sep).join('/');
125
+ if (clean !== relative || !MEDIA_RE.test(clean)) return null;
126
+ const file = join(folder, clean);
127
+ if (!existsSync(file)) return null;
128
+ const resolved = real(file);
129
+ const root = real(folder);
130
+ if (!resolved || !root || !resolved.startsWith(`${root}${sep}`)) return null;
131
+ return resolved;
132
+ }
133
+
134
+ export function mediaContentType(path: string): string {
135
+ const lower = path.toLowerCase();
136
+ if (lower.endsWith('.png')) return 'image/png';
137
+ if (lower.endsWith('.webp')) return 'image/webp';
138
+ if (lower.endsWith('.mp4')) return 'video/mp4';
139
+ return 'image/jpeg';
140
+ }