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,87 @@
1
+ #!/usr/bin/env bun
2
+ import { homedir } from 'node:os';
3
+ import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
4
+
5
+ interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string }
6
+
7
+ function firstLine(text: string): string { return text.trim().split(/\r?\n/)[0]?.trim() ?? ''; }
8
+
9
+ function reason(result: CommandResult): string {
10
+ const data = lastResult(result);
11
+ const message = [data?.error, data?.reason, data?.message, result.stderr, result.stdout].find((part) => typeof part === 'string' && part.trim());
12
+ const line = firstLine(String(message ?? `명령 실패 (rc=${result.status})`));
13
+ return /\b404\b/.test(line) ? `공급원에 판 없음 (404): ${line}` : line;
14
+ }
15
+
16
+ function quote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`; }
17
+
18
+ export function runOpsUpgrade(run: CommandRunner = runCommand) {
19
+ const context = readGraphContext();
20
+ const hosts = context.input.opsHosts;
21
+ if (hosts === undefined || (Array.isArray(hosts) && hosts.length === 0)) {
22
+ return { outcome: 'ok' as const, verdict: 'pass' as const, summary: '운영 대상 없음 — 건너뜀', hosts: [] as HostResult[] };
23
+ }
24
+ if (!Array.isArray(hosts) || !hosts.every((host) => typeof host === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(host))) throw new Error('input.opsHosts must be an array of host names');
25
+ if (context.outputs.verify?.outcome !== 'ok') throw new Error('verify must pass before ops upgrade');
26
+ const version = context.input.version;
27
+ if (nodeOutput(context, 'publish', 'tag') !== `v${version}`) throw new Error('published tag/version mismatch');
28
+ const internalDist = context.input.internalDist;
29
+ if (internalDist !== undefined && (typeof internalDist !== 'string' || !internalDist.trim())) throw new Error('input.internalDist must be a path string');
30
+ const feedPath = typeof internalDist === 'string' && internalDist.startsWith('~/') ? `${homedir()}/${internalDist.slice(2)}` : internalDist;
31
+ let feedError = '';
32
+ if (typeof feedPath === 'string') {
33
+ try {
34
+ const feed = run('bun', ['scripts/publish-internal-dist.ts', '--checkout', process.cwd(), '--out', feedPath]);
35
+ const data = lastResult(feed);
36
+ if (feed.status !== 0 || data?.ok !== true || data.version !== version) feedError = `내부 피드 발행 실패: ${feed.status === 0 && data?.ok === true ? `판 불일치 (${String(data.version)})` : reason(feed)}`;
37
+ } catch (error) { feedError = `내부 피드 발행 실패: ${firstLine(String(error))}`; }
38
+ }
39
+ const results: HostResult[] = [];
40
+ for (const host of hosts as string[]) {
41
+ if (host !== 'local' && feedError) {
42
+ results.push({ host, ok: false, before: '', after: '', skipped: '내부 피드 발행 실패', error: feedError });
43
+ continue;
44
+ }
45
+ const command = (args: string[]): CommandResult => host === 'local'
46
+ ? run('elanous', args)
47
+ : run('ssh', [host, `PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"; export PATH; elanous ${args.map(quote).join(' ')}`]);
48
+ let before = '';
49
+ let after = '';
50
+ try {
51
+ const previous = command(['--version']);
52
+ if (previous.status === 0) before = firstLine(previous.stdout);
53
+ const updated = command(['update', '--version', version, '--json', ...(context.input.opsRestart === true ? ['--restart'] : [])]);
54
+ const updateResult = lastResult(updated);
55
+ if (updated.status !== 0 || updateResult?.exitCode !== 0 || updateResult.installedVersion !== version) {
56
+ const error = updated.status === 0 && updateResult?.exitCode === 0
57
+ ? `설치판 불일치 (기대 ${version}, 실제 ${String(updateResult.installedVersion)})` : reason(updated);
58
+ results.push({ host, ok: false, before, after, error });
59
+ continue;
60
+ }
61
+ const current = command(['--version']);
62
+ if (current.status !== 0) {
63
+ results.push({ host, ok: false, before, after, error: reason(current) });
64
+ continue;
65
+ }
66
+ after = firstLine(current.stdout);
67
+ if (after.split(/\s+/)[0] !== version) {
68
+ results.push({ host, ok: false, before, after, error: `올렸는데 판이 그대로 (기대 ${version}, 실제 ${after || '없음'})` });
69
+ continue;
70
+ }
71
+ results.push({ host, ok: true, before, after });
72
+ } catch (error) {
73
+ results.push({ host, ok: false, before, after, error: firstLine(String(error)) });
74
+ }
75
+ }
76
+ const failures = results.filter((result) => !result.ok);
77
+ return { outcome: failures.length || feedError ? 'fail' as const : 'ok' as const,
78
+ verdict: failures.length || feedError ? 'fail' as const : 'pass' as const,
79
+ summary: failures.length ? `운영 판올림 실패: ${failures.map(({ host, error }) => `${host}: ${error}`).join('; ')}${feedError && !failures.some((item) => item.skipped) ? `; ${feedError}` : ''}`
80
+ : feedError || `운영 판올림 ${version} · ${results.length}대 확인`, hosts: results };
81
+ }
82
+
83
+ if (import.meta.main) {
84
+ let version = '';
85
+ try { version = readGraphContext().input.version; process.exitCode = finishNode('ops-upgrade', version, runOpsUpgrade()); }
86
+ catch (error) { process.exitCode = finishNode('ops-upgrade', version, errorResult(error)); }
87
+ }
@@ -0,0 +1,58 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { releaseLedgerRoot } from '../../src/instance/resolve.js';
4
+ import { checklistGate, type ChecklistGate } from '../../src/release-loop/checklist.js';
5
+
6
+ export type ReleaseReadiness =
7
+ | { ready: false; reason: 'already-published'; details: string }
8
+ | { ready: false; reason: 'already-running'; details: string }
9
+ | { ready: false; reason: 'checklist-blocked'; details: ChecklistGate }
10
+ | { ready: true; reason: 'ready'; details: ChecklistGate };
11
+
12
+ export interface ReleaseReadinessDeps {
13
+ ledgerRoot?: string;
14
+ checklist?: typeof checklistGate;
15
+ isPidAlive?: (pid: number) => boolean;
16
+ }
17
+
18
+ function readIfPresent(path: string): string | undefined {
19
+ try { return readFileSync(path, 'utf8'); }
20
+ catch (error) {
21
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ function isPidAlive(pid: number): boolean {
27
+ try { process.kill(pid, 0); return true; }
28
+ catch (error) {
29
+ if ((error as NodeJS.ErrnoException).code === 'ESRCH') return false;
30
+ if ((error as NodeJS.ErrnoException).code === 'EPERM') return true;
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ /** Read-only preflight; callers own acquiring and releasing run.lock around the graph. */
36
+ export function releaseReadiness(version: string, deps: ReleaseReadinessDeps = {}): ReleaseReadiness {
37
+ if (!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(version)) throw new Error(`release version must be x.y.z: ${version}`);
38
+ const dir = join(deps.ledgerRoot ?? releaseLedgerRoot(), 'release', version);
39
+ const record = readIfPresent(join(dir, 'release.json'));
40
+ if (record) {
41
+ const data = JSON.parse(record) as { publishedAt?: unknown };
42
+ if (typeof data.publishedAt === 'string' && data.publishedAt.trim()) {
43
+ return { ready: false, reason: 'already-published', details: data.publishedAt };
44
+ }
45
+ }
46
+ const lock = readIfPresent(join(dir, 'run.lock'));
47
+ if (lock) {
48
+ let pid: unknown;
49
+ try { pid = (JSON.parse(lock) as { pid?: unknown } | null)?.pid; }
50
+ catch { pid = undefined; }
51
+ if (typeof pid === 'number' && Number.isSafeInteger(pid) && pid > 0 && (deps.isPidAlive ?? isPidAlive)(pid)) {
52
+ return { ready: false, reason: 'already-running', details: String(pid) };
53
+ }
54
+ }
55
+ const gate = (deps.checklist ?? checklistGate)(version);
56
+ if (!gate.ok) return { ready: false, reason: 'checklist-blocked', details: gate };
57
+ return { ready: true, reason: 'ready', details: gate };
58
+ }
@@ -0,0 +1,47 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ /** Read per-file seconds from previous cut Pod reports; missing or damaged reports are not estimates. */
5
+ export function readFileDurations(dir: string): Map<string, number> {
6
+ const durations = new Map<string, number>();
7
+ let names: string[];
8
+ const entities: Record<string, string> = { '&amp;': '&', '&quot;': '"', '&apos;': "'", '&lt;': '<', '&gt;': '>' };
9
+ try { names = readdirSync(dir).filter((name) => name.endsWith('.junit.xml')).sort(); }
10
+ catch { return durations; }
11
+ for (const name of names) {
12
+ try {
13
+ const xml = readFileSync(join(dir, name), 'utf8');
14
+ for (const match of xml.matchAll(/<testsuite\b([^>]*)>/g)) {
15
+ const attrs = match[1]!;
16
+ const file = /\bfile="([^"]+)"/.exec(attrs)?.[1];
17
+ const rawTime = /\btime="([^"]+)"/.exec(attrs)?.[1];
18
+ if (!file || rawTime === undefined) continue;
19
+ const seconds = Number(rawTime);
20
+ if (!Number.isFinite(seconds) || seconds < 0) continue;
21
+ const path = file.replace(/&(?:amp|quot|apos|lt|gt);/g, (entity) => entities[entity]!);
22
+ durations.set(path, Math.max(durations.get(path) ?? 0, seconds));
23
+ }
24
+ } catch { /* One unreadable report must not discard the others. */ }
25
+ }
26
+ return durations;
27
+ }
28
+
29
+ /** Longest processing time first; tie-breaks use lexical paths, then the least populated lightest shard. */
30
+ export function planShards(files: string[], durations: Map<string, number>, count: number): Array<{ files: string[]; plannedSeconds: number }> {
31
+ if (!Number.isSafeInteger(count) || count < 1) throw new Error('invalid shard count');
32
+ const known = files.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
33
+ const median = known.length ? (known[Math.floor((known.length - 1) / 2)]! + known[Math.floor(known.length / 2)]!) / 2 : 1;
34
+ const weight = (file: string) => {
35
+ const value = durations.get(file);
36
+ return value !== undefined && Number.isFinite(value) && value >= 0 ? value : median;
37
+ };
38
+ const shards = Array.from({ length: Math.min(count, files.length) }, () => ({ files: [] as string[], plannedSeconds: 0 }));
39
+ for (const file of [...files].sort((a, b) => weight(b) - weight(a) || (a < b ? -1 : a > b ? 1 : 0))) {
40
+ let lightest = shards[0]!;
41
+ for (const shard of shards) if (shard.plannedSeconds < lightest.plannedSeconds
42
+ || (shard.plannedSeconds === lightest.plannedSeconds && shard.files.length < lightest.files.length)) lightest = shard;
43
+ lightest.files.push(file);
44
+ lightest.plannedSeconds += weight(file);
45
+ }
46
+ return shards.filter((shard) => shard.files.length > 0);
47
+ }
@@ -0,0 +1,93 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
4
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
5
+ import { userConfigPath } from '../../src/user-config.js';
6
+ import { checklistGate } from '../../src/release-loop/checklist.js';
7
+ import { runGraph, type GraphRunState } from '../../src/graph-runner/runner.js';
8
+
9
+ const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
10
+ const GRAPH = join(import.meta.dir, '../../graphs/release/release-loop.yaml');
11
+
12
+ export interface ReleaseLoopConfig {
13
+ gatePodPool?: string;
14
+ gatePodBunCache?: string;
15
+ gatePodShards?: number;
16
+ gatePodShardTimeoutSeconds?: number;
17
+ gateRemote?: string;
18
+ gateRemoteMirror?: string;
19
+ opsHosts?: string[];
20
+ internalDist?: string;
21
+ opsRestart?: boolean;
22
+ }
23
+
24
+ export interface ReleaseRunInput extends ReleaseLoopConfig {
25
+ version: string;
26
+ previousVersion: string;
27
+ gatePodPool: string;
28
+ }
29
+
30
+ export interface UnattendedReleaseDeps {
31
+ ledgerRoot?: string;
32
+ config?: ReleaseLoopConfig;
33
+ configPath?: string;
34
+ graph?: (path: string, options: { input: ReleaseRunInput }) => Promise<GraphRunState>;
35
+ checklist?: typeof checklistGate;
36
+ }
37
+
38
+ function compareVersions(a: string, b: string): number {
39
+ const aa = a.split('.').map(BigInt), bb = b.split('.').map(BigInt);
40
+ for (let i = 0; i < 3; i++) {
41
+ if (aa[i]! < bb[i]!) return -1;
42
+ if (aa[i]! > bb[i]!) return 1;
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ /** Only completed publications in the release ledger count as a prior release. */
48
+ export function latestPublishedPreviousVersion(version: string, ledgerRoot = releaseLedgerRoot()): string {
49
+ if (!VERSION.test(version)) throw new Error(`release version must be x.y.z: ${version}`);
50
+ const dir = join(ledgerRoot, 'release');
51
+ const versions = existsSync(dir) ? readdirSync(dir) : [];
52
+ const published = versions.filter((candidate) => {
53
+ if (!VERSION.test(candidate) || compareVersions(candidate, version) >= 0) return false;
54
+ try {
55
+ const record = JSON.parse(readFileSync(join(dir, candidate, 'release.json'), 'utf8')) as { version?: unknown; publishedAt?: unknown };
56
+ return record.version === candidate && typeof record.publishedAt === 'string' && record.publishedAt.trim().length > 0;
57
+ } catch { return false; }
58
+ });
59
+ const previous = published.sort((a, b) => compareVersions(b, a))[0];
60
+ if (!previous) throw new Error(`no published previous release before ${version} in ${dir}`);
61
+ return previous;
62
+ }
63
+
64
+ export function releaseLoopConfig(configPath = getElanousConfigDirOverride() ? join(effectiveInstanceRoot(), 'config.json') : userConfigPath()): ReleaseLoopConfig {
65
+ if (!existsSync(configPath)) return {};
66
+ const raw = JSON.parse(readFileSync(configPath, 'utf8')) as { release?: { loop?: ReleaseLoopConfig } };
67
+ return raw.release?.loop ?? {};
68
+ }
69
+
70
+ export function buildReleaseRunInput(version: string, deps: UnattendedReleaseDeps = {}): ReleaseRunInput {
71
+ const config = deps.config ?? releaseLoopConfig(deps.configPath);
72
+ const pool = config.gatePodPool;
73
+ if (typeof pool !== 'string' || !pool.trim()) throw new Error('release.loop.gatePodPool is required before graph execution');
74
+ const previousVersion = latestPublishedPreviousVersion(version, deps.ledgerRoot);
75
+ return {
76
+ ...config,
77
+ version, previousVersion,
78
+ gatePodPool: pool.trim(),
79
+ };
80
+ }
81
+
82
+ /** Fail closed at the entry boundary, before any graph node can change a release. */
83
+ export async function runUnattendedRelease(
84
+ opts: { version: string; dryRun?: boolean },
85
+ deps: UnattendedReleaseDeps = {},
86
+ ): Promise<{ input: ReleaseRunInput; dryRun: boolean; state?: GraphRunState }> {
87
+ const input = buildReleaseRunInput(opts.version, deps);
88
+ if (opts.dryRun) return { input, dryRun: true };
89
+ const gate = (deps.checklist ?? checklistGate)(opts.version);
90
+ if (!gate.ok) throw new Error(`release checklist blocked: ${[...gate.red, ...gate.undecided, ...gate.blocked].join(', ')}`);
91
+ const state = await (deps.graph ?? runGraph)(GRAPH, { input });
92
+ return { input, dryRun: false, state };
93
+ }
@@ -0,0 +1,102 @@
1
+ // Usage: bun scripts/ux/measure-first-text.ts --questions questions.json [--url http://127.0.0.1:3456] [--token-stdin] [--json]
2
+ // questions.json is a nonempty JSON array of nonempty question strings. Each question starts a fresh session.
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ export interface FirstTextMeasurement {
6
+ question: string;
7
+ firstTextMs: number;
8
+ delta: string;
9
+ }
10
+
11
+ /** Stop the clock on the first complete, nonempty text-delta SSE event, not on response headers or turn-begin. */
12
+ export async function measureFirstText(
13
+ question: string,
14
+ url: string,
15
+ options: { token?: string; fetch?: typeof fetch; now?: () => number } = {},
16
+ ): Promise<FirstTextMeasurement> {
17
+ const fetchFn = options.fetch ?? fetch;
18
+ const now = options.now ?? (() => performance.now());
19
+ const controller = new AbortController();
20
+ const started = now();
21
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
22
+ try {
23
+ const response = await fetchFn(new URL('/v1/prompt/stream', url), {
24
+ method: 'POST',
25
+ headers: {
26
+ 'content-type': 'application/json',
27
+ accept: 'text/event-stream',
28
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
29
+ },
30
+ body: JSON.stringify({ userText: question }),
31
+ signal: controller.signal,
32
+ });
33
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
34
+ if (!response.body) throw new Error('SSE response has no body');
35
+ reader = response.body.getReader();
36
+ const decoder = new TextDecoder();
37
+ let buffer = '';
38
+ while (true) {
39
+ const { done, value } = await reader.read();
40
+ if (done) break;
41
+ buffer += decoder.decode(value, { stream: true });
42
+ // SSE separators can straddle arbitrary UTF-8 chunks (including CRLF boundaries).
43
+ let boundary: RegExpExecArray | null;
44
+ while ((boundary = /\r?\n\r?\n/.exec(buffer)) !== null) {
45
+ const frame = buffer.slice(0, boundary.index);
46
+ buffer = buffer.slice(boundary.index + boundary[0].length);
47
+ const event = /^event:\s*(.*)$/m.exec(frame)?.[1]?.trim();
48
+ const data = frame.split(/\r?\n/).filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\n');
49
+ if (event === 'text-delta') {
50
+ const payload: unknown = JSON.parse(data);
51
+ const delta = payload && typeof payload === 'object' && 'delta' in payload ? payload.delta : undefined;
52
+ if (typeof delta === 'string' && delta.length > 0) {
53
+ return { question, firstTextMs: now() - started, delta };
54
+ }
55
+ }
56
+ if (event === 'error') throw new Error(`SSE error: ${data}`);
57
+ if (event === 'turn-end') throw new Error('Turn ended without a text-delta');
58
+ }
59
+ }
60
+ throw new Error('SSE stream ended without a text-delta');
61
+ } finally {
62
+ // Do not leave the daemon generating the rest of a turn after the measurement.
63
+ if (reader) await reader.cancel().catch(() => {});
64
+ controller.abort();
65
+ }
66
+ }
67
+
68
+ export async function run(argv: string[], deps: { fetch?: typeof fetch; now?: () => number; stdin?: () => Promise<string> } = {}): Promise<FirstTextMeasurement[]> {
69
+ let url = 'http://127.0.0.1:3456';
70
+ let questionsPath: string | undefined;
71
+ let json = false;
72
+ let tokenStdin = false;
73
+ for (let i = 0; i < argv.length; i++) {
74
+ const arg = argv[i];
75
+ if (arg === '--json') json = true;
76
+ else if (arg === '--token-stdin') tokenStdin = true;
77
+ else if (arg === '--url' || arg === '--questions') {
78
+ const value = argv[++i];
79
+ if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
80
+ if (arg === '--url') url = value;
81
+ else questionsPath = value;
82
+ } else throw new Error(`Unknown argument: ${arg}`);
83
+ }
84
+ if (!questionsPath) throw new Error('Usage: --questions <JSON file> [--url <base URL>] [--token-stdin] [--json]');
85
+ const questions: unknown = JSON.parse(readFileSync(questionsPath, 'utf8'));
86
+ if (!Array.isArray(questions) || !questions.length || !questions.every((q) => typeof q === 'string' && q.trim().length > 0)) {
87
+ throw new Error('--questions must be a nonempty JSON array of nonempty strings');
88
+ }
89
+ const token = tokenStdin ? (await (deps.stdin ?? (() => Bun.stdin.text()))()).trim() : undefined;
90
+ const results: FirstTextMeasurement[] = [];
91
+ for (const question of questions as string[]) {
92
+ results.push(await measureFirstText(question, url, { token, fetch: deps.fetch, now: deps.now }));
93
+ }
94
+ if (json) console.log(JSON.stringify(results));
95
+ else for (const result of results) console.log(`${result.firstTextMs.toFixed(1)} ms\t${result.question}`);
96
+ return results;
97
+ }
98
+
99
+ if (import.meta.main) {
100
+ try { await run(process.argv.slice(2)); }
101
+ catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }
102
+ }
@@ -12,7 +12,7 @@ import { Frames, launch, PUBLIC_LEAK_CHECK, sleep } from './lib/cdp.js';
12
12
  const arg = (k: string) => { const i = process.argv.indexOf(`--${k}`); return i > 0 ? process.argv[i + 1] : undefined; };
13
13
  const flag = (k: string) => process.argv.includes(`--${k}`);
14
14
  const rawUrl = arg('url'); const out = arg('out');
15
- if (!rawUrl || !out) { console.error('usage: --url <url> --out <dir> [--secs N] [--size WxH] [--mobile] [--type text] [--shot] [--token-stdin]'); process.exit(64); }
15
+ if (!rawUrl || !out) { console.error('usage: --url <url> --out <dir> [--secs N] [--size WxH] [--mobile] [--type text] [--shot] [--token-stdin] [--eval <js>]'); process.exit(64); }
16
16
  const mobile = flag('mobile');
17
17
  const [W, H] = mobile ? [390, 844] : (arg('size') ?? '1920x1080').split('x').map(Number);
18
18
  const DPR = mobile ? 3 : 1; const SECS = Number(arg('secs') ?? 20);
@@ -26,6 +26,9 @@ if (flag('token-stdin')) {
26
26
  if (temp) { await cdp.send('Page.navigate', { url: `${u.origin}/app/` }); await sleep(3000); await cdp.ev(`localStorage.setItem('elanous.daemon.token', ${JSON.stringify(temp)})`); }
27
27
  }
28
28
  await cdp.send('Page.navigate', { url: u.toString() }); await sleep(8000);
29
+ // --eval <js>: 녹화 전에 화면 상태를 맞춘다(출처 선택·토글·배너 닫기) — 기록에 남도록 그대로 찍는다.
30
+ const pre = arg('eval');
31
+ if (pre) { console.log('eval', await cdp.ev<string>(`(()=>{try{${pre};return 'ok'}catch(e){return 'error: '+e}})()`)); await sleep(4000); }
29
32
  const check = async (tag: string) => console.log(tag, await cdp.ev<string>(PUBLIC_LEAK_CHECK));
30
33
  await check('before');
31
34
  if (flag('shot')) { const f = join(out, `shot-${W}x${H}.png`); await cdp.png(f); console.log('png', f); cdp.close(); process.exit(0); }
@@ -22,17 +22,30 @@ const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
22
22
  const OUT_DIR = process.env.VFLOW_OUT ?? join(homedir(), 'docs', 'ref', 'vflow');
23
23
  const NDJSON = join(OUT_DIR, 'prompts.ndjson');
24
24
  const URLS = join(OUT_DIR, 'urls.txt');
25
- const CONCURRENCY = 4;
26
- const GAP_MS = 120;
25
+ // ⚠️ 2026-10-01 실측: 4병렬·120ms 로 전수를 돌리면 수백 건 뒤 사이트가 429 를 낸다(재수집 404건 중 158건).
26
+ // 기본을 낮추고, 429 는 «없다»가 아니라 «기다려라»로 읽는다(Retry-After 를 따른다).
27
+ const CONCURRENCY = Number(process.env.VFLOW_CONCURRENCY ?? 2);
28
+ const GAP_MS = Number(process.env.VFLOW_GAP_MS ?? 400);
29
+ const MAX_429_RETRIES = 5;
27
30
 
28
31
  const argv = process.argv.slice(2);
29
32
  const limitAt = argv.indexOf('--limit');
30
33
  const LIMIT = limitAt >= 0 ? Number(argv[limitAt + 1]) : Infinity;
31
34
 
35
+ let throttled = 0;
32
36
  async function get(url: string): Promise<string> {
33
- const r = await fetch(url, { headers: { 'User-Agent': UA } });
34
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
35
- return r.text();
37
+ for (let attempt = 0; ; attempt++) {
38
+ const r = await fetch(url, { headers: { 'User-Agent': UA } });
39
+ if (r.status === 429 && attempt < MAX_429_RETRIES) {
40
+ throttled++;
41
+ const after = Number(r.headers.get('retry-after'));
42
+ const waitMs = Number.isFinite(after) && after > 0 ? after * 1000 : 5000 * 2 ** attempt;
43
+ await new Promise((res) => setTimeout(res, waitMs));
44
+ continue;
45
+ }
46
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
47
+ return r.text();
48
+ }
36
49
  }
37
50
 
38
51
  /** 사이트맵 전수 → 영문 `/prompts/` URL. ⛔ 번역판(`/{lang}/prompts/`)은 «같은 내용»이라 뺀다. */
@@ -166,14 +179,14 @@ async function main(): Promise<void> {
166
179
  catch { failed++; return null; } // ⛔ 「없다」가 아니라 «못 받았다»
167
180
  }));
168
181
  for (const r of rows) { if (r) { appendFileSync(NDJSON, JSON.stringify(r) + '\n'); ok++; } else noLd++; }
169
- if ((i / CONCURRENCY) % 25 === 0) console.log(` … ${ok + noLd}/${todo.length} ok=${ok} 건너뜀=${noLd}`);
182
+ if ((i / CONCURRENCY) % 25 === 0) console.log(` … ${ok + noLd}/${todo.length} ok=${ok} 못받음=${failed} jsonld없음=${noLd - failed} 429대기=${throttled}`);
170
183
  }
171
184
  // ⛔ 수를 «도구가» 낸다 — 사람이 세지 않는다.
172
185
  writeFileSync(join(OUT_DIR, 'MANIFEST.txt'),
173
186
  [`source: https://vflow.live (robots: Allow: / · 2026-09-23 확인)`,
174
187
  `fetched: ${new Date().toISOString()}`,
175
188
  `urls_total: ${urls.length}`, `rows_ok: ${ok + done.size}`,
176
- `skipped_no_jsonld: ${noLd - failed}`, `failed_fetch: ${failed}`,
189
+ `skipped_no_jsonld: ${noLd - failed}`, `failed_fetch: ${failed}`, `throttled_429_retries: ${throttled}`,
177
190
  `schema: url model category slug name description prompt keywords[] datePublished author authorUrl sourcePost thumb spec{duration,camera[],lighting[],mood[],difficulty,promptLanguage,includes[]} video{url,isoDuration,resolution,aspect}`,
178
191
  `refetch: bun scripts/vflow-collect.ts`, ''].join('\n'));
179
192
  console.log(`\n ✅ ok ${ok} · 건너뜀 ${noLd - failed} · 못 받음 ${failed} → ${NDJSON}`);
@@ -0,0 +1,73 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { searchVflowReferences, type VflowQuery } from '../src/video-pipeline/vflow-reference.js';
4
+
5
+ const args = process.argv.slice(2);
6
+ let file = join(homedir(), 'docs', 'ref', 'vflow', 'prompts.ndjson');
7
+ const query: { -readonly [K in keyof VflowQuery]: VflowQuery[K] } = { limit: 5 };
8
+ let json = false;
9
+ const techniques: string[] = [];
10
+ const terms: string[] = [];
11
+
12
+ for (let i = 0; i < args.length; i++) {
13
+ const arg = args[i]!;
14
+ if (arg === '--json') { json = true; continue; }
15
+ if (['--file', '--model', '--technique', '--category', '--limit', '--camera', '--lighting', '--mood', '--max-seconds'].includes(arg)) {
16
+ const value = args[++i];
17
+ if (!value || value.startsWith('--')) {
18
+ console.error(`값이 필요한 옵션: ${arg}`);
19
+ process.exit(1);
20
+ }
21
+ switch (arg) {
22
+ case '--file': file = value; break;
23
+ case '--model': query.model = value; break;
24
+ case '--technique': techniques.push(value); break;
25
+ case '--category': query.category = value; break;
26
+ case '--camera': query.camera = value; break;
27
+ case '--lighting': query.lighting = value; break;
28
+ case '--mood': query.mood = value; break;
29
+ case '--limit': {
30
+ const parsed = Number(value);
31
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
32
+ console.error(`--limit 은 0 이상의 정수여야 합니다: ${value}`);
33
+ process.exit(1);
34
+ }
35
+ query.limit = parsed;
36
+ break;
37
+ }
38
+ case '--max-seconds': {
39
+ const parsed = Number(value);
40
+ if (!Number.isFinite(parsed) || parsed < 0 || !value.trim()) {
41
+ console.error(`--max-seconds 은 0 이상의 숫자여야 합니다: ${value}`);
42
+ process.exit(1);
43
+ }
44
+ query.maxSeconds = parsed;
45
+ break;
46
+ }
47
+ }
48
+ } else if (arg.startsWith('--')) {
49
+ console.error(`알 수 없는 옵션: ${arg}`);
50
+ process.exit(1);
51
+ } else {
52
+ terms.push(arg);
53
+ }
54
+ }
55
+ query.terms = terms;
56
+ query.techniques = techniques;
57
+ const result = searchVflowReferences(file, query);
58
+ if (!result.available) {
59
+ console.error(`입력 파일을 읽을 수 없습니다: ${file} (${result.reason})`);
60
+ process.exit(1);
61
+ }
62
+ if (json) {
63
+ console.log(JSON.stringify(result.rows.map(({ url, model, category, name, prompt, keywords, author, authorUrl, video }) =>
64
+ ({ url, model, category, name, prompt, keywords, author, authorUrl, video: video ?? null }))));
65
+ if (result.matches === 0) console.error(`일치 0 · 읽은 줄 ${result.read}`);
66
+ } else if (result.matches === 0) {
67
+ console.log(`일치 0 · 읽은 줄 ${result.read}`);
68
+ } else {
69
+ for (const row of result.rows) {
70
+ console.log(`${row.name} · ${row.model}\n${row.url}\n${row.author ?? ''}\n${row.prompt.slice(0, 240)}${row.video?.url ? `\n${row.video.url}` : ''}`);
71
+ }
72
+ }
73
+ if (result.skipped) console.error(`건너뛴 줄 ${result.skipped}`);
@@ -0,0 +1,36 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { parse as parseYaml } from 'yaml';
3
+ import type { GraphSpecLike, WalkerApi } from '../src/video-pipeline/walk-line.js';
4
+
5
+ interface TestGraph extends GraphSpecLike {
6
+ entry_node: string;
7
+ terminal_nodes: string[];
8
+ nodes: ({ node_id: string; recipe?: string; max_visits?: number })[];
9
+ }
10
+
11
+ export const readGraphSpec: WalkerApi['readGraphSpec'] = (path) => {
12
+ try { return { spec: parseYaml(readFileSync(path, 'utf8')) as TestGraph }; }
13
+ catch (e) { return { error: (e as Error).message }; }
14
+ };
15
+
16
+ export const walkGraph: WalkerApi['walkGraph'] = async (graph, step, opts) => {
17
+ const spec = graph as TestGraph;
18
+ const visits = new Map<string, number>();
19
+ const steps: { node: string; visit: number }[] = [];
20
+ let next = spec.entry_node;
21
+ while (steps.length < opts.maxSteps) {
22
+ const node = spec.nodes.find((n) => n.node_id === next);
23
+ if (!node) return { terminal: null, stopReason: 'no-node', steps };
24
+ const visit = (visits.get(next) ?? 0) + 1;
25
+ if (visit > (node.max_visits ?? 1)) return { terminal: null, stopReason: 'budget-exceeded', steps };
26
+ visits.set(next, visit);
27
+ steps.push({ node: next, visit });
28
+ if (spec.terminal_nodes.includes(next)) return { terminal: next, stopReason: 'terminal', steps };
29
+ const outcome = await step(node);
30
+ if (outcome === null) { next = opts.unobservedNode; continue; }
31
+ const edge = spec.edges.find((e) => e.from === next && (e.to !== undefined || e.map?.[outcome] !== undefined));
32
+ if (!edge) return { terminal: null, stopReason: 'no-edge', steps };
33
+ next = edge.to ?? edge.map![outcome]!;
34
+ }
35
+ return { terminal: null, stopReason: 'budget-exceeded', steps };
36
+ };
@@ -998,9 +998,8 @@ async function plan() {
998
998
  return where.length ? `${u} → 구간을 «${where.join('/')}» 까지` : `${u} → ⛔ 스핀 결손`;
999
999
  });
1000
1000
 
1001
- sayUnprobed();
1002
-
1003
1001
  if (COMPACT) {
1002
+ sayUnprobed();
1004
1003
  // ⛔ 노드별로 찍지 않는다 — encode 가 세 번 나온다. 능력 단위로 «접는다».
1005
1004
  const seen = new Map<string, { impl?: Impl; required: boolean }>();
1006
1005
  for (const o of out) for (const p of o.picks) {
@@ -1138,6 +1137,7 @@ async function plan() {
1138
1137
  return;
1139
1138
  }
1140
1139
 
1140
+ sayUnprobed();
1141
1141
  console.log(`\n🧱 추천 스택 — 구간 ${nodes[0].id} → ${nodes[nodes.length - 1].id} · 선호 ${TIER_MARK[prefer]}`);
1142
1142
  console.log(` 설정: ${LOADED.source}${MACHINE ? ` · 기계 «${MACHINE_NAME}»` : ''}`);
1143
1143
  if (MISSING.size) console.log(` ⚠️ 없다고 «가정»한 것: ${[...MISSING].join(', ')}`);
@@ -35,13 +35,14 @@ The grammar of a chapter (take it from the reference, don't improvise):
35
35
 
36
36
  The outro gives one line that sums everything up, then the CTA.
37
37
 
38
- ⛔ Every claim must trace to a merged change or a measured run, listed in `sources`. Don't write «fully autonomous», dates you can't promise, or comparisons (`docs/marketing/GUIDE-launch-tone-and-banned-claims-*`).
38
+ ⛔ Every claim must trace to a merged change or a measured run, listed in `sources`. Don't write «fully autonomous», dates you can't promise, or comparisons with other products.
39
39
 
40
40
  ## 2. Voice — `bun $SKILL/engine/vo.ts <project>/script.json`
41
41
 
42
42
  - Makes one `/with-timestamps` call per beat and writes `source/vo/<key>.mp3` and `<key>.json`, where the JSON holds per-character times.
43
43
  - A beat whose text hash is unchanged is skipped, so you can edit one line and re-run.
44
44
  - The key comes from `ELEVENLABS_API_KEY`, which the plugin connector sets (`elanous plugin credentials video-explainer`). Never print it.
45
+ - **No key** → a draft voice from macOS `say` (a Korean system voice; `EXPLAINER_SAY_VOICE` picks one). Character times are then spread over the measured length — captions still land per line, keyword timing is approximate. Use it for unattended runs and rehearsals; use ElevenLabs for anything public.
45
46
 
46
47
  ## 3. Compose — `node $SKILL/engine/build.mjs <project>/script.json`
47
48
 
@@ -84,7 +85,27 @@ The reference supplies the **grammar** (components and staging), never its colou
84
85
  - **Text**: `#F2F1EE`.
85
86
  - **Motif**: the V6 «엘랑 소용돌이» mark (`engine/brand/`), used in three places — a slow, faint rotation behind the scene, the header, and the chapter cards.
86
87
 
87
- Why these choices, what the reference does, and what we can and cannot reproduce: `내부 문서 `RESEARCH-explainer-video-reference-decomposition-2026-09-30``.
88
+ The reference is used only for its structure (header, chapter cards, a diagram that moves one step per line, keyword-timed captions). Its colours, artwork and voice are not reproduced.
89
+
90
+ ## Field reel — photos from the venue → a vertical video in about a minute
91
+
92
+ ```bash
93
+ zsh $SKILL/engine/reel.sh <folder> --title "마케터의 밤" --sub "2026.10.02 · 서울"
94
+ ```
95
+
96
+ - **Input**: a folder of phone photos (jpg · png · heic) and short clips (mp4 · mov). The default upload folder is `~/.elanous/field/<event>/`.
97
+ - **Order**: by capture time (`mdls` creation date), then by file name.
98
+ - **Captions**: optional `<folder>/captions.txt`, one line per item as `파일명 | 자막`. A photo without a line gets one line from a vision model (`codex exec -i`, all photos in parallel, 30 s cap each); if that fails it shows «현장 N». `FIELD_REEL_VISION=0` turns the vision step off.
99
+ - **Title**: `--title`, or `title.txt` through the daemon. A slug-like title (`field-2026-10-01`) is replaced by a vision headline read from the first three photos (event or company names on screen first). The date line defaults to the first photo's capture date (KST).
100
+ - **Music**: `FIELD_REEL_BGM=<file>` or `~/.cache/elanous-explainer/bgm/field.wav` — cut to length, faded in and out, leveled to about −16 LUFS. No file means no music. Use only licensed tracks (the default cache holds an Epidemic Sound download); the file is never committed.
101
+ - **Output**:
102
+ - `<folder>/reel/reel-9x16.mp4` at 1080×1920, 24–60 s.
103
+ - An intro card (kicker, mark, title word by word, red rule, date), then each item: photos for about 4–6 s. A landscape photo is shown whole over a blurred fill of itself; a portrait photo fills the frame. Each move overlaps the next with a slide and a red wipe on the cut.
104
+ - Per item a counter (`01 / 03`), the caption rising word by word, and a red rule. The V6 header, an Icarus Red progress bar, and an end card with elanous.ai.
105
+ - `--part body` renders the photos only (no cards, no music) — for an instant version that stitches pre-rendered cards around it.
106
+ - **Fitting to length**: over 60 s drops items from the middle, keeping the first and last. Under 24 s stretches the photos.
107
+ - **Measured**: 3 photos → 24 s video in 28 s end to end, of which vision 8.7 s and render 15.9 s (M-series Mac, 10-01). Earlier: 7 items → 31.2 s video in 56–83 s.
108
+ - **Before posting**: people's faces need their consent. Run the same 1 fps OCR check as any public video.
88
109
 
89
110
  ## Graph
90
111
 
@@ -96,4 +117,4 @@ Why these choices, what the reference does, and what we can and cannot reproduce
96
117
  - speech mismatch → vo;
97
118
  - bad script → script;
98
119
  - public release → needs-human.
99
- - **Not coded yet**: the `explainer-*` recipes. Until they are, you run these steps from this skill.
120
+ - **Recipes**: the `explainer-*` recipes are in the elanous core, but `graph run` does not yet accept this graph's terminal nodes. The video-explainer plugin therefore ships the skill and nodes only; run these steps from this skill. The graph comes with plugin 0.2.0.