elanous 0.2.6 → 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 (450) hide show
  1. package/.env.example +10 -7
  2. package/README.md +4 -0
  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/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  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/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
  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/{7633.0654fe6ea259c1ee.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/{2984-977cfa339fcd65a5.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/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
  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/{91ff6dda-3b33d42d20657429.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-10ed24f413ed66e9.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-624f7a2f89b60c8f.js +1 -0
  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-55d50e9b0006a91d.js +1 -0
  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-37b8bf78a3c6eec5.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/index.html +1 -1
  123. package/apps/pwa/out/index.txt +21 -21
  124. package/apps/pwa/out/intake/index.html +1 -1
  125. package/apps/pwa/out/intake/index.txt +22 -22
  126. package/apps/pwa/out/live/index.html +1 -1
  127. package/apps/pwa/out/live/index.txt +22 -22
  128. package/apps/pwa/out/market/index.html +1 -1
  129. package/apps/pwa/out/market/index.txt +21 -21
  130. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  131. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  132. package/apps/pwa/out/morning/index.html +1 -1
  133. package/apps/pwa/out/morning/index.txt +22 -22
  134. package/apps/pwa/out/observatory/index.html +1 -1
  135. package/apps/pwa/out/observatory/index.txt +22 -22
  136. package/apps/pwa/out/reflection/index.html +1 -1
  137. package/apps/pwa/out/reflection/index.txt +22 -22
  138. package/apps/pwa/out/scheduler/index.html +1 -1
  139. package/apps/pwa/out/scheduler/index.txt +22 -22
  140. package/apps/pwa/out/sessions/index.html +1 -1
  141. package/apps/pwa/out/sessions/index.txt +21 -21
  142. package/apps/pwa/out/settings/devices/index.html +1 -1
  143. package/apps/pwa/out/settings/devices/index.txt +22 -22
  144. package/apps/pwa/out/settings/index.html +1 -1
  145. package/apps/pwa/out/settings/index.txt +22 -22
  146. package/apps/pwa/out/setup/done/index.html +1 -1
  147. package/apps/pwa/out/setup/done/index.txt +22 -22
  148. package/apps/pwa/out/setup/index.html +1 -1
  149. package/apps/pwa/out/setup/index.txt +22 -22
  150. package/apps/pwa/out/share/index.html +1 -1
  151. package/apps/pwa/out/share/index.txt +22 -22
  152. package/apps/pwa/out/showroom/index.html +1 -1
  153. package/apps/pwa/out/showroom/index.txt +21 -21
  154. package/apps/pwa/out/tasks/index.html +1 -1
  155. package/apps/pwa/out/tasks/index.txt +22 -22
  156. package/apps/pwa/out/term/index.html +1 -1
  157. package/apps/pwa/out/term/index.txt +22 -22
  158. package/apps/pwa/out/trace/index.html +1 -1
  159. package/apps/pwa/out/trace/index.txt +22 -22
  160. package/apps/pwa/out/vault/index.html +1 -1
  161. package/apps/pwa/out/vault/index.txt +22 -22
  162. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  163. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  164. package/apps/pwa/out/workflows/index.html +1 -1
  165. package/apps/pwa/out/workflows/index.txt +22 -22
  166. package/apps/pwa/out/workspace/index.html +1 -1
  167. package/apps/pwa/out/workspace/index.txt +22 -22
  168. package/apps/pwa/out/worktrees/index.html +1 -1
  169. package/apps/pwa/out/worktrees/index.txt +22 -22
  170. package/bin/elanous.cjs +107 -0
  171. package/graphs/field/field-feed.yaml +24 -0
  172. package/graphs/field/recipes.yaml +12 -0
  173. package/graphs/landing/landing-heal.yaml +3 -1
  174. package/graphs/landing/recipes.yaml +3 -0
  175. package/graphs/release/recipes.yaml +10 -0
  176. package/graphs/release/release-loop.yaml +10 -3
  177. package/package.json +5 -5
  178. package/plugins/card-followup/README.md +13 -4
  179. package/plugins/card-followup/graphs/card-followup.yaml +3 -1
  180. package/plugins/card-followup/graphs/recipes.yaml +5 -1
  181. package/plugins/card-followup/graphs/run-step.ts +122 -18
  182. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  183. package/plugins/card-followup/plugin.json +1 -1
  184. package/plugins/doc-draft/README.md +13 -0
  185. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  186. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  187. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  188. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  189. package/plugins/doc-draft/plugin.json +24 -0
  190. package/plugins/geo-check/README.md +3 -1
  191. package/plugins/geo-check/graphs/run-step.ts +24 -3
  192. package/plugins/geo-check/plugin.json +1 -1
  193. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  194. package/plugins/job-coach/README.md +1 -1
  195. package/plugins/job-coach/graphs/run-step.ts +2 -2
  196. package/plugins/job-coach/plugin.json +1 -1
  197. package/plugins/job-coach/plugin.ts +1 -1
  198. package/scripts/affinity/endcard.sh +3 -2
  199. package/scripts/affinity/reframe.sh +3 -1
  200. package/scripts/ci-public-export-leak-gate.ts +48 -0
  201. package/scripts/codex-quota-alert.ts +122 -258
  202. package/scripts/coord-tracks.json +4 -4
  203. package/scripts/field-feed/deliver-node.ts +52 -0
  204. package/scripts/field-feed/feed-node.ts +67 -0
  205. package/scripts/field-feed/lib.ts +162 -0
  206. package/scripts/field-feed/reel-node.ts +24 -0
  207. package/scripts/field-feed/start.ts +62 -0
  208. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  209. package/scripts/install.sh +92 -13
  210. package/scripts/landing-heal/clean-checkout.ts +114 -0
  211. package/scripts/landing-heal/run-mission.ts +38 -3
  212. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  213. package/scripts/public-export.ts +8 -4
  214. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  215. package/scripts/release-loop/env-known-failures.ts +31 -0
  216. package/scripts/release-loop/gate-node.ts +81 -19
  217. package/scripts/release-loop/known-issues-node.ts +159 -0
  218. package/scripts/release-loop/npm-publish-node.ts +118 -0
  219. package/scripts/release-loop/ops-upgrade-node.ts +87 -0
  220. package/scripts/release-loop/release-readiness.ts +58 -0
  221. package/scripts/release-loop/unattended-release.ts +93 -0
  222. package/scripts/ux/measure-first-text.ts +102 -0
  223. package/scripts/vflow-collect.ts +20 -7
  224. package/scripts/vflow-search.ts +73 -0
  225. package/scripts/video-free-line-test-walker.ts +36 -0
  226. package/scripts/video-pipeline.ts +2 -2
  227. package/skills/explainer-video/SKILL.md +12 -9
  228. package/skills/explainer-video/engine/build.mjs +1 -1
  229. package/skills/explainer-video/engine/reel.mjs +138 -31
  230. package/skills/explainer-video/engine/reel.sh +2 -1
  231. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  232. package/skills/grill-me/SKILL.md +1 -1
  233. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  234. package/src/a2a/server.ts +2 -0
  235. package/src/acp/elanous-aware-peers.ts +30 -0
  236. package/src/acp/server.ts +91 -15
  237. package/src/agent/monad-agent-turn.ts +25 -6
  238. package/src/agent/shared-app-tools.ts +5 -3
  239. package/src/agent-substrate/review-intent.ts +6 -2
  240. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  241. package/src/auth/github-app-token.ts +4 -1
  242. package/src/autopilot/track-agent-shadow.ts +151 -0
  243. package/src/autopilot/track-agent.ts +23 -3
  244. package/src/boot/acp-server-help.ts +16 -0
  245. package/src/boot/daemon-prompt-turn.ts +31 -10
  246. package/src/boot/daemon-tools/index.ts +7 -1
  247. package/src/budget/codex-credit-pace.ts +84 -0
  248. package/src/budget/codex-credit-plan.ts +154 -0
  249. package/src/budget/codex-reset-credit-state.ts +2 -0
  250. package/src/chat/chat-cli.ts +4 -1
  251. package/src/chat/short-question.ts +40 -0
  252. package/src/cli/agent-cli.ts +24 -15
  253. package/src/cli/ask-bare.ts +42 -0
  254. package/src/cli/cli-user-error.ts +25 -0
  255. package/src/cli/codex-credit-plan-view.ts +25 -0
  256. package/src/cli/dependence-gauge.ts +237 -0
  257. package/src/cli/doctor-cli.ts +5 -0
  258. package/src/cli/doctor-modes.ts +48 -0
  259. package/src/cli/intake-cli.ts +12 -2
  260. package/src/cli/intake-source-cli.ts +58 -0
  261. package/src/cli/machine-cli.ts +23 -0
  262. package/src/cli/msg-cli.ts +137 -0
  263. package/src/cli/plugin-cli.ts +25 -0
  264. package/src/cli/pr-cli.ts +36 -0
  265. package/src/cli/provider-cli.ts +5 -0
  266. package/src/cli/release-cli.ts +42 -1
  267. package/src/cli/release-run-if-ready.ts +48 -0
  268. package/src/cli/setup-cli.ts +27 -6
  269. package/src/cli/setup-recommend.ts +8 -0
  270. package/src/cli/usage-cli.ts +11 -1
  271. package/src/codex/setup.ts +27 -8
  272. package/src/dashboard/index.ts +1 -1
  273. package/src/discord.ts +56 -6
  274. package/src/domains/core-tools.ts +37 -14
  275. package/src/domains/outbound-alert.ts +50 -11
  276. package/src/exec-requests/planner.ts +140 -0
  277. package/src/exec-requests/runner.ts +338 -0
  278. package/src/exec-requests/store.ts +76 -0
  279. package/src/field/field-media.ts +8 -3
  280. package/src/field/field-reel.ts +196 -0
  281. package/src/grok/credential.ts +7 -5
  282. package/src/harness/aside-run-status.ts +33 -0
  283. package/src/harness/browser-verify.ts +48 -17
  284. package/src/harness/harness-cli-command.ts +25 -3
  285. package/src/harness/image-load-state.ts +21 -0
  286. package/src/harness/process-garbage.ts +45 -0
  287. package/src/index.ts +86 -14
  288. package/src/instance/leader.ts +13 -1
  289. package/src/intake-plane/collect-github.ts +5 -3
  290. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  291. package/src/intake-plane/discord-seat-work.ts +56 -0
  292. package/src/intake-plane/intake-sources.ts +202 -0
  293. package/src/intake-plane/items.ts +28 -12
  294. package/src/intake-plane/seat-doc-route.ts +207 -0
  295. package/src/intake-plane/submit-intake-work.ts +9 -1
  296. package/src/intake-plane/telegram-seat-work.ts +72 -0
  297. package/src/llm/model-defaults.ts +24 -1
  298. package/src/llm.ts +5 -4
  299. package/src/machines/machine-ledger.ts +127 -0
  300. package/src/market/pack-internal-refs.ts +28 -0
  301. package/src/market/publish.ts +31 -4
  302. package/src/maturity/feature-maturity.ts +65 -0
  303. package/src/model-tier/turn-tier.ts +24 -0
  304. package/src/msg/msg-store.ts +149 -0
  305. package/src/native-tool-catalog.ts +2 -1
  306. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  307. package/src/nexus/api/consult-requests.ts +117 -0
  308. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  309. package/src/nexus/api/exec-requests.ts +51 -0
  310. package/src/nexus/api/field-uploads.ts +40 -3
  311. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  312. package/src/nexus/api/graph-approvals.ts +71 -4
  313. package/src/nexus/api/http-server.ts +61 -7
  314. package/src/nexus/api/seat-requests.ts +254 -0
  315. package/src/nexus/api/trace-run-scan.ts +37 -0
  316. package/src/nexus/api/trace.ts +19 -3
  317. package/src/nexus/index.ts +3 -0
  318. package/src/nexus/setup-status.ts +4 -5
  319. package/src/notify/llm-budget-alert.ts +16 -10
  320. package/src/oauth/codex-account-rotation.ts +26 -4
  321. package/src/oauth/codex-account-store.ts +20 -2
  322. package/src/oauth/codex-browser-login.ts +173 -0
  323. package/src/oauth/codex.ts +4 -1
  324. package/src/oauth/fallback-chain.ts +5 -5
  325. package/src/oauth/store.ts +42 -5
  326. package/src/onboarding/continue-here.ts +29 -0
  327. package/src/onboarding/entry-hints.ts +8 -0
  328. package/src/onboarding/full-screen-io.ts +35 -3
  329. package/src/onboarding/help/skills.md +1 -1
  330. package/src/onboarding/non-interactive.ts +1 -0
  331. package/src/onboarding/subscription-hint.ts +27 -0
  332. package/src/onboarding/web-first.ts +83 -0
  333. package/src/onboarding.ts +230 -23
  334. package/src/plugins/maker/node-install.ts +92 -0
  335. package/src/plugins/maker/node-maker.ts +139 -0
  336. package/src/plugins/maker/plugin-maker.ts +19 -7
  337. package/src/policy/llm-policy.ts +268 -0
  338. package/src/prompt-library/session-guidance.ts +7 -1
  339. package/src/prompt-library/universal-preamble.ts +2 -2
  340. package/src/relay/relay-cli.ts +20 -0
  341. package/src/relay/relay-server.ts +220 -0
  342. package/src/release-loop/checklist.ts +9 -8
  343. package/src/self-dev/decompose.ts +1 -0
  344. package/src/self-dev/dev-pipeline.ts +6 -3
  345. package/src/self-dev/run-record-mirror.ts +44 -0
  346. package/src/self-dev/run-store.ts +12 -3
  347. package/src/self-implement/harness-policy-gates.ts +4 -1
  348. package/src/self-implement/report-origin.ts +12 -1
  349. package/src/session/chat.ts +17 -5
  350. package/src/skills/shared-agent-skill-roots.ts +25 -0
  351. package/src/steward/failure-streak.ts +78 -0
  352. package/src/steward/steward-cards.ts +24 -0
  353. package/src/steward/triage-budget.ts +127 -0
  354. package/src/steward/triage-plan.ts +75 -0
  355. package/src/steward/triage.ts +244 -42
  356. package/src/task-cards/card-store.ts +1 -1
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  358. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  359. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  360. package/src/telegram.ts +38 -2
  361. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  362. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  363. package/src/user-config.ts +80 -4
  364. package/src/version/packed-revision.json +1 -1
  365. package/src/video-pipeline/recipes/character.ts +42 -5
  366. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  367. package/src/video-pipeline/recipes/free-line.ts +16 -3
  368. package/src/video-pipeline/recipes/remote.ts +7 -1
  369. package/src/video-pipeline/vflow-reference.ts +109 -0
  370. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
  379. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
  448. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  449. package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
  450. /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
@@ -0,0 +1,162 @@
1
+ // Field feed — shared helpers for the graph nodes (graphs/field/field-feed.yaml · EV10c/EV10d).
2
+ // The draft file `<folder>/feed/feed-draft.json` is the single source of truth: the feed node writes it, the PWA
3
+ // «게시 대기» card overwrites it when a person edits, and the deliver node reads it «after» approval.
4
+ import { existsSync, readFileSync, readdirSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
5
+ import { basename, dirname, extname, join, resolve } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { spawn, spawnSync } from 'node:child_process';
8
+
9
+ export type FeedSlide = { image: string; source: string; caption: string; include: boolean; renderedCaption?: string /* `N/M:caption` as last drawn */ };
10
+ export type FeedDraft = {
11
+ kind: 'feed-draft'; version: 1; revision: number; updatedAt: string; updatedBy: 'graph' | 'human';
12
+ graphId: 'field-feed'; runId: string | null; folder: string;
13
+ brand: { name: string; handle: string; avatar: string | null };
14
+ event: { title: string; date: string };
15
+ cover: { text: string; sub: string; image: string; renderedText?: string; renderedSub?: string; renderedSource?: string };
16
+ slides: FeedSlide[];
17
+ caption: { hook: string; body: string };
18
+ hashtags: string[];
19
+ location: string | null;
20
+ reel: string | null;
21
+ };
22
+
23
+ export const ENGINE = resolve(import.meta.dir, '../../skills/explainer-video/engine');
24
+ export const CACHE = process.env.EXPLAINER_CACHE ?? join(homedir(), '.cache', 'elanous-explainer');
25
+ const IMG = ['.jpg', '.jpeg', '.png', '.heic', '.webp'];
26
+
27
+ /** Graph context: `ELANOUS_GRAPH_CONTEXT` is a JSON string or a path to one. CLI `--folder` wins for manual runs. */
28
+ export function readContext(): { folder: string; runId: string | null } {
29
+ const at = process.argv.indexOf('--folder');
30
+ let folder = at >= 0 ? process.argv[at + 1] : undefined;
31
+ let runId: string | null = null;
32
+ const raw = process.env.ELANOUS_GRAPH_CONTEXT;
33
+ if (raw) {
34
+ const ctx = JSON.parse(raw.trim().startsWith('{') ? raw : readFileSync(raw, 'utf8')) as { input?: { folder?: unknown }; runId?: unknown };
35
+ if (!folder && typeof ctx.input?.folder === 'string') folder = ctx.input.folder;
36
+ if (typeof ctx.runId === 'string') runId = ctx.runId;
37
+ }
38
+ if (!folder) throw new Error('input.folder 필요(현장 폴더 절대 경로)');
39
+ folder = resolve(folder.replace(/^~(?=\/)/, homedir()));
40
+ if (!existsSync(folder)) throw new Error(`현장 폴더 없음: ${folder}`);
41
+ return { folder, runId: runId ?? process.env.ELANOUS_GRAPH_RUN_ID ?? null };
42
+ }
43
+
44
+ export const draftPath = (folder: string) => join(folder, 'feed', 'feed-draft.json');
45
+
46
+ export function readDraft(folder: string): FeedDraft | null {
47
+ try { return JSON.parse(readFileSync(draftPath(folder), 'utf8')) as FeedDraft; } catch { return null; }
48
+ }
49
+
50
+ export function writeDraft(folder: string, draft: FeedDraft): void {
51
+ const path = draftPath(folder);
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ const tmp = `${path}.${process.pid}.tmp`;
54
+ writeFileSync(tmp, JSON.stringify(draft, null, 2) + '\n');
55
+ renameSync(tmp, path);
56
+ }
57
+
58
+ /** Field uploads are named `20261002T193012Z-<device>-<name>` (capture time, UTC) — same order as the reel. */
59
+ export function listPhotos(folder: string): string[] {
60
+ const stamp = (f: string) => { const m = /^(\d{8}T\d{6}Z)-/.exec(f); return m ? m[1]! : `~${f}`; };
61
+ return readdirSync(folder).filter((f) => IMG.includes(extname(f).toLowerCase()))
62
+ .sort((a, b) => (stamp(a) < stamp(b) ? -1 : stamp(a) > stamp(b) ? 1 : a < b ? -1 : a > b ? 1 : 0));
63
+ }
64
+
65
+ export function kstDate(photos: string[]): string {
66
+ const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})/.exec(photos[0] ?? '');
67
+ if (!m) return '';
68
+ const d = new Date(Date.UTC(+m[1]!, +m[2]! - 1, +m[3]!, +m[4]!) + 9 * 3600e3);
69
+ return `${d.getUTCFullYear()}.${String(d.getUTCMonth() + 1).padStart(2, '0')}.${String(d.getUTCDate()).padStart(2, '0')}`;
70
+ }
71
+
72
+ /** One codex vision/text call — the same CLI the reel and card-followup use. Empty string on any failure. */
73
+ export function ask(prompt: string, images: string[], ms = 45000): Promise<string> {
74
+ const bin = process.env.FIELD_REEL_CODEX_BIN || 'codex';
75
+ const args = ['exec', '--skip-git-repo-check', '-c', 'model_reasoning_effort=low', prompt, ...images.flatMap((im) => ['-i', im])];
76
+ return new Promise((done) => {
77
+ let buf = ''; let settled = false;
78
+ const finish = (v: string) => { if (!settled) { settled = true; done(v); } };
79
+ let child;
80
+ try { child = spawn(bin, args, { stdio: ['ignore', 'pipe', 'ignore'] }); } catch { finish(''); return; }
81
+ const timer = setTimeout(() => { child.kill('SIGKILL'); finish(''); }, ms);
82
+ child.stdout.on('data', (d) => { buf += d; });
83
+ child.on('error', () => { clearTimeout(timer); finish(''); });
84
+ child.on('close', (code) => { clearTimeout(timer); finish(code === 0 ? buf : ''); });
85
+ });
86
+ }
87
+
88
+ const CHROME = process.env.FIELD_FEED_CHROME
89
+ ?? ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/usr/bin/chromium', '/usr/bin/google-chrome']
90
+ .find((p) => existsSync(p));
91
+
92
+ /** HTML page → PNG at 1080×1350 with headless Chrome. */
93
+ export function screenshot(html: string, out: string): void {
94
+ if (!CHROME) throw new Error('Chrome 없음 — FIELD_FEED_CHROME 로 지정');
95
+ const page = out.replace(/\.png$/, '.html');
96
+ writeFileSync(page, html);
97
+ const r = spawnSync(CHROME, ['--headless=new', '--disable-gpu', '--hide-scrollbars', '--force-device-scale-factor=1',
98
+ '--window-size=1080,1350', '--virtual-time-budget=3000', '--allow-file-access-from-files', `--screenshot=${out}`, `file://${page}`],
99
+ { encoding: 'utf8', timeout: 60000 });
100
+ if (r.status !== 0 || !existsSync(out)) throw new Error(`screenshot ${basename(out)}: ${(r.stderr || '').slice(0, 200)}`);
101
+ }
102
+
103
+ export function markSvg(size: number): string {
104
+ const paths = [...readFileSync(join(ENGINE, 'brand', 'elanous-mark-on-dark.svg'), 'utf8').matchAll(/<path d="([^"]*)"/g)].map((m) => m[1]);
105
+ return `<svg width="${size}" height="${size}" viewBox="150 150 724 724">${paths.map((d) => `<path d="${d}" fill="#F2F1EE"/>`).join('')}<circle cx="512" cy="512" r="76" fill="#E95047"/></svg>`;
106
+ }
107
+
108
+ const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
109
+ const fonts = () => ['Bold:700', 'ExtraBold:800', 'Black:900'].map((w) => {
110
+ const [name, weight] = w.split(':');
111
+ return `@font-face{font-family:"Pretendard";src:url("file://${join(CACHE, 'fonts', `Pretendard-${name}.woff2`)}") format("woff2");font-weight:${weight}}`;
112
+ }).join('');
113
+ const base = `*{margin:0;padding:0;box-sizing:border-box}html,body{width:1080px;height:1350px;overflow:hidden;background:#000}
114
+ body{position:relative;font-family:"Pretendard",sans-serif;color:#F2F1EE;word-break:keep-all}
115
+ .ph{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
116
+ .top{position:absolute;left:56px;top:52px;display:flex;align-items:center;gap:16px;font-weight:800;font-size:30px;text-shadow:0 2px 10px rgba(0,0,0,.6)}
117
+ .top b{color:#E95047}
118
+ .url{position:absolute;right:56px;bottom:48px;font-weight:700;font-size:26px;color:rgba(242,241,238,.7)}`;
119
+
120
+ export function coverHtml(photo: string, title: string, sub: string): string {
121
+ return `<!doctype html><html lang="ko"><head><meta charset="utf-8"><style>${fonts()}${base}
122
+ .shade{position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.92) 0%,rgba(0,0,0,.55) 45%,rgba(0,0,0,.25) 100%)}
123
+ .box{position:absolute;left:72px;right:72px;bottom:150px}
124
+ .kick{font-weight:800;font-size:30px;letter-spacing:.26em;color:#E95047}
125
+ h1{margin-top:22px;font-weight:900;font-size:104px;line-height:1.1;letter-spacing:-.03em}
126
+ .rule{margin-top:34px;width:150px;height:9px;background:#E95047}
127
+ p{margin-top:26px;font-weight:700;font-size:40px;color:rgba(242,241,238,.75)}
128
+ </style></head><body><img class="ph" src="file://${esc(photo)}"/><div class="shade"></div>
129
+ <div class="top">${markSvg(54)}<span>Elanous <b>·</b> 현장 스케치</span></div>
130
+ <div class="box"><div class="kick">현장 스케치</div><h1>${esc(title)}</h1><div class="rule"></div><p>${esc(sub)}</p></div>
131
+ <div class="url">elanous.ai</div></body></html>`;
132
+ }
133
+
134
+ export function slideHtml(photo: string, caption: string, no: number, total: number): string {
135
+ return `<!doctype html><html lang="ko"><head><meta charset="utf-8"><style>${fonts()}${base}
136
+ .shade{position:absolute;left:0;right:0;bottom:0;height:46%;background:linear-gradient(to top,rgba(0,0,0,.86),rgba(0,0,0,0))}
137
+ .box{position:absolute;left:72px;right:72px;bottom:120px}
138
+ .no{font-weight:800;font-size:32px;letter-spacing:.12em;color:#E95047}.no i{font-style:normal;color:rgba(242,241,238,.6)}
139
+ .cap{margin-top:16px;font-weight:900;font-size:68px;line-height:1.2;letter-spacing:-.03em;text-shadow:0 3px 16px rgba(0,0,0,.6)}
140
+ .rule{margin-top:26px;width:110px;height:8px;background:#E95047}
141
+ </style></head><body><img class="ph" src="file://${esc(photo)}"/><div class="shade"></div>
142
+ <div class="top">${markSvg(48)}<span>Elanous</span></div>
143
+ <div class="box"><div class="no">${String(no).padStart(2, '0')} <i>/ ${String(total).padStart(2, '0')}</i></div><div class="cap">${esc(caption)}</div><div class="rule"></div></div>
144
+ <div class="url">elanous.ai</div></body></html>`;
145
+ }
146
+
147
+ /** Last JSON object in a model reply, or null. */
148
+ export function lastJson(text: string): Record<string, unknown> | null {
149
+ // Try objects from the last «{» backwards; for each, the shortest closing «}» that parses.
150
+ for (let start = text.lastIndexOf('{'); start >= 0; start = text.lastIndexOf('{', start - 1)) {
151
+ for (let end = text.indexOf('}', start); end >= 0; end = text.indexOf('}', end + 1)) {
152
+ try {
153
+ const value = JSON.parse(text.slice(start, end + 1)) as unknown;
154
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>;
155
+ } catch { /* longer */ }
156
+ }
157
+ if (start === 0) break;
158
+ }
159
+ return null;
160
+ }
161
+
162
+ export const result = (data: Record<string, unknown>) => console.log(JSON.stringify({ outcome: 'ok', ...data }));
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bun
2
+ // reel node — 데몬이 업로드 뒤 이미 렌더했으면(.reel-status.json done ⊕ 파일 있음) 그대로 쓰고, 아니면 같은 엔진(reel.sh)으로 한 번.
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { ENGINE, readContext, result } from './lib.js';
7
+
8
+ try {
9
+ const { folder } = readContext();
10
+ const file = join(folder, 'reel', 'reel-9x16.mp4');
11
+ let state = '';
12
+ try { state = (JSON.parse(readFileSync(join(folder, '.reel-status.json'), 'utf8')) as { state?: string }).state ?? ''; } catch { /* 없음 */ }
13
+ if (state === 'rendering') throw new Error('데몬이 아직 렌더 중이다 — 끝난 뒤 다시');
14
+ if (state === 'done' && existsSync(file) && existsSync(join(folder, 'reel', 'timeline.json'))) {
15
+ result({ reel: file, reused: true });
16
+ } else {
17
+ const started = Date.now();
18
+ const r = spawnSync('zsh', [join(ENGINE, 'reel.sh'), folder], { encoding: 'utf8', timeout: 540000 });
19
+ if (r.status !== 0 || !existsSync(file)) throw new Error(`reel.sh 실패: ${(r.stderr || r.stdout || '').slice(-300)}`);
20
+ result({ reel: file, reused: false, ms: Date.now() - started });
21
+ }
22
+ } catch (error) {
23
+ console.log(JSON.stringify({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) }));
24
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bun
2
+ // 현장 피드 시작 — `bun scripts/field-feed/start.ts <현장폴더>`
3
+ // ① graphs/field/field-feed.yaml 런을 띄운다(reel → feed → post 에서 «게시 대기»로 멈춘다)
4
+ // ② 멈췄으면 «결정 대기자»를 떼어 띄운다: 15초마다 `graph run --resume <runId>` — 결정이 기록되기 전엔 같은 자리에 다시 멈추고,
5
+ // «최종 게시»(approved)가 기록되면 deliver 까지, 거절이면 failed 로 끝난다. 48시간 뒤 스스로 그만둔다.
6
+ // `--wait <runId>` 는 그 대기자 자신(직접 부르지 않는다).
7
+ import { spawn, spawnSync } from 'node:child_process';
8
+ import { existsSync, mkdirSync, openSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { join, resolve } from 'node:path';
11
+ import { debug } from '../../src/debug/log.js';
12
+
13
+ const ROOT = resolve(import.meta.dir, '../..');
14
+ const GRAPH = join(ROOT, 'graphs', 'field', 'field-feed.yaml');
15
+ const CLI = join(ROOT, 'bin', 'elanous.mjs');
16
+ type State = { runId?: string; status?: string; pending?: { decision?: string } };
17
+
18
+ function graphRun(args: string[]): State {
19
+ const r = spawnSync(process.execPath, [CLI, 'graph', 'run', GRAPH, '--json', ...args], { cwd: ROOT, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 });
20
+ const line = r.stdout.trim().split('\n').reverse().find((l) => l.startsWith('{'));
21
+ if (!line) throw new Error(`graph run: 결과 없음 rc=${r.status} ${r.stderr.slice(-300)}`);
22
+ return JSON.parse(line) as State;
23
+ }
24
+
25
+ const argv = process.argv.slice(2);
26
+ const waitAt = argv.indexOf('--wait');
27
+ if (waitAt >= 0) {
28
+ const runId = argv[waitAt + 1]!;
29
+ const deadline = Date.now() + 48 * 3600e3;
30
+ let state: State = { status: 'awaiting-approval' };
31
+ while (Date.now() < deadline) {
32
+ await Bun.sleep(15000);
33
+ try { state = graphRun(['--resume', runId]); } catch (error) {
34
+ debug.log('field.feed', 'resume-error', { runId, error: error instanceof Error ? error.message : String(error) });
35
+ continue;
36
+ }
37
+ if (state.status !== 'awaiting-approval') break;
38
+ }
39
+ debug.log('field.feed', 'wait-ended', { runId, status: state.status ?? 'unknown', timedOut: state.status === 'awaiting-approval' });
40
+ console.log(`field-feed ${runId}: ${state.status}`);
41
+ process.exit(0);
42
+ }
43
+
44
+ const folderArg = argv.find((a) => !a.startsWith('--'));
45
+ if (!folderArg) { console.error('usage: bun scripts/field-feed/start.ts <현장폴더>'); process.exit(2); }
46
+ const folder = resolve(folderArg.replace(/^~(?=\/)/, homedir()));
47
+ if (!existsSync(folder)) { console.error(`현장 폴더 없음: ${folder}`); process.exit(2); }
48
+ const started = Date.now();
49
+ const state = graphRun(['--input', JSON.stringify({ folder })]);
50
+ const seconds = Math.round((Date.now() - started) / 1000);
51
+ debug.log('field.feed', 'started', { runId: state.runId, status: state.status, seconds });
52
+ if (state.status === 'awaiting-approval' && state.runId) {
53
+ const logDir = join(folder, 'feed');
54
+ mkdirSync(logDir, { recursive: true });
55
+ const out = openSync(join(logDir, 'wait.log'), 'a');
56
+ const child = spawn(process.execPath, [import.meta.path, '--wait', state.runId], { cwd: ROOT, detached: true, stdio: ['ignore', out, out] });
57
+ child.unref();
58
+ console.log(`field-feed ${state.runId}: 게시 대기 (${seconds}s) · 초안 ${join(folder, 'feed', 'feed-draft.json')} · 결정 대기자 pid ${child.pid}`);
59
+ } else {
60
+ console.log(`field-feed ${state.runId}: ${state.status} (${seconds}s)`);
61
+ if (state.status !== 'done') process.exitCode = 1;
62
+ }
@@ -0,0 +1,6 @@
1
+ {"url":"https://example.test/shot-a","model":"seedance-2-0","category":"camera","name":"Crash Zoom Macro","description":"A toy car approaches the lens","prompt":"Crash zoom into a miniature car, then hold a macro detail on the painted door.","keywords":["crash zoom","macro"],"author":"Aster","authorUrl":"https://example.test/aster"}
2
+ {"url":"https://example.test/shot-b","model":"seedance-2-0","category":"camera","name":"Quiet Crash Zoom","description":"A bell at a crossing","prompt":"A crash zoom toward a distant bell in the rain.","keywords":["crash zoom"],"author":"Beryl","authorUrl":"https://example.test/beryl"}
3
+ {"url":"https://example.test/shot-c","model":"seedance-2-0","category":"motion","name":"Wide Speed Ramp","description":"A paper kite travels across the sky","prompt":"A paper kite turns slowly before a speed ramp across the horizon.","keywords":["speed ramp"],"author":"Cedar","authorUrl":"https://example.test/cedar"}
4
+ {"url":"https://example.test/shot-d","model":"grok-imagine","category":"camera","name":"Crash Zoom Macro","description":"A tiny clock on the desk","prompt":"Crash zoom onto a tiny clock, revealing macro gears.","keywords":["crash zoom","macro"],"author":"Dara","authorUrl":"https://example.test/dara"}
5
+ {"url":"https://example.test/shot-e","model":"grok-imagine","category":"motion","name":"Whip Pan","description":"A painted room","prompt":"Whip pan past the window to reveal a blue wall. The illustrated cardboard room is full of handmade clocks, chalk drawings, paper boats, folded maps, model trees, painted stones, colorful blocks, and strings of origami stars. The camera circles the toy table slowly while the paper figures stand still. ENDING_SENTINEL","keywords":["whip pan"],"author":"Eden","authorUrl":"https://example.test/eden"}
6
+ {broken json
@@ -11,7 +11,7 @@ set -euo pipefail
11
11
 
12
12
  usage() {
13
13
  cat <<'EOF'
14
- Usage: bash scripts/install.sh [--prefix PATH] [--source PATH.tgz|URL] [--no-modify-path] [--no-bootstrap-bun] [--help]
14
+ Usage: bash scripts/install.sh [--prefix PATH] [--source PATH.tgz|URL] [--no-modify-path] [--no-bootstrap-bun] [--no-setup] [--yes] [--allow-downgrade] [--help]
15
15
  curl -fsSL https://github.com/ElanvitalAI/elanous/releases/latest/download/install.sh | bash
16
16
 
17
17
  Install elanous without contacting a package registry.
@@ -23,6 +23,9 @@ Install elanous without contacting a package registry.
23
23
  (or set $ELANOUS_INSTALL_SOURCE; without either, fetch the verified latest release)
24
24
  --no-modify-path do not append the elanous PATH block to a shell startup file
25
25
  --no-bootstrap-bun fail instead of installing bun with its official installer when bun is missing
26
+ --no-setup do not start first-time setup in this window after installing (interactive terminals only)
27
+ --yes do not ask before updating an existing installation
28
+ --allow-downgrade allow installing a version older than the one already installed
26
29
  --help, -h show this help
27
30
  EOF
28
31
  }
@@ -35,6 +38,9 @@ PREFIX="${ELANOUS_INSTALL_PREFIX:-${XDG_DATA_HOME:-${HOME:?HOME is required}/.lo
35
38
  SOURCE="${ELANOUS_INSTALL_SOURCE:-}"
36
39
  MODIFY_PATH=1
37
40
  BOOTSTRAP_BUN=1
41
+ RUN_SETUP=1
42
+ ASSUME_YES=0
43
+ ALLOW_DOWNGRADE=0
38
44
 
39
45
  while [ "$#" -gt 0 ]; do
40
46
  case "$1" in
@@ -47,10 +53,30 @@ while [ "$#" -gt 0 ]; do
47
53
  SOURCE="$2"; shift 2 ;;
48
54
  --no-modify-path) MODIFY_PATH=0; shift ;;
49
55
  --no-bootstrap-bun) BOOTSTRAP_BUN=0; shift ;;
56
+ --no-setup) RUN_SETUP=0; shift ;;
57
+ --yes|-y) ASSUME_YES=1; shift ;;
58
+ --allow-downgrade) ALLOW_DOWNGRADE=1; shift ;;
50
59
  *) echo "⛔ unknown argument: $1" >&2; usage >&2; exit 2 ;;
51
60
  esac
52
61
  done
53
62
 
63
+ # INST1 — one language per run: English by default, Korean when the locale is Korean (override: ELANOUS_INSTALL_LANG=en|ko).
64
+ case "${ELANOUS_INSTALL_LANG:-${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}}" in
65
+ ko*) INSTALL_LANG=ko ;;
66
+ *) INSTALL_LANG=en ;;
67
+ esac
68
+ t() { if [ "$INSTALL_LANG" = ko ]; then printf '%s' "$2"; else printf '%s' "$1"; fi; }
69
+
70
+ # Interactive = a person is at a terminal. `curl … | bash` gives us the script on stdin, so look at
71
+ # stdout and /dev/tty, not stdin (rustup does the same). CI never counts as interactive.
72
+ # ELANOUS_INSTALL_INTERACTIVE=0|1 overrides the probe (tests, wrappers).
73
+ INTERACTIVE=0
74
+ if [ -n "${ELANOUS_INSTALL_INTERACTIVE:-}" ]; then
75
+ [ "$ELANOUS_INSTALL_INTERACTIVE" = 1 ] && INTERACTIVE=1
76
+ elif [ -z "${CI:-}" ] && [ -t 1 ] && { : </dev/tty; } 2>/dev/null; then
77
+ INTERACTIVE=1
78
+ fi
79
+
54
80
  # bun 이 없으면 공식 설치기로 먼저 깐다 — «sh 한 번이면 알아서» (claude·grok 설치기와 같은 기대).
55
81
  # ⛔ 끄면(--no-bootstrap-bun) 공식 설치 명령을 안내하고 rc 127 로 멈춘다.
56
82
  # 🩸 2026-09-24 빈 VM 실측: 비대화 셸(ssh 명령·크론)은 ~/.bun/bin 이 PATH 에 없어 «이미 깐» bun 을 못 보고 또 설치했다.
@@ -295,6 +321,30 @@ fi
295
321
  PACKAGE_VERSION="$(tar -xzOf "$INSTALL_TARBALL" package/package.json 2>/dev/null | bun -e 'const t=await Bun.stdin.text(); try { process.stdout.write(String(JSON.parse(t).version ?? "")) } catch {}' || true)"
296
322
  [ -n "$PACKAGE_VERSION" ] || { echo "⛔ package version missing in tarball: $INSTALL_TARBALL" >&2; exit 1; }
297
323
  case "$PACKAGE_VERSION" in */*|*..*) echo "⛔ unsafe package version: $PACKAGE_VERSION" >&2; exit 1 ;; esac
324
+
325
+ # INST1 — an existing installation is never moved silently: no downgrade without --allow-downgrade
326
+ # (10-01: a running mbp went 0.2.7 → 0.2.6 unnoticed), and an interactive re-run asks before updating.
327
+ INSTALLED_VERSION=""
328
+ if [ -f "$PREFIX/install.json" ]; then
329
+ INSTALLED_VERSION="$(bun -e 'try { const j=JSON.parse(await Bun.file(process.argv.at(-1)).text()); process.stdout.write(String(j.version ?? "")) } catch {}' "$PREFIX/install.json" || true)"
330
+ fi
331
+ if [ -n "$INSTALLED_VERSION" ]; then
332
+ ORDER="$(bun -e 'const [a,b]=process.argv.slice(-2); try { process.stdout.write(String(Bun.semver.order(a,b))) } catch { process.stdout.write("0") }' "$PACKAGE_VERSION" "$INSTALLED_VERSION" || echo 0)"
333
+ if [ "$ORDER" = "-1" ] && [ "$ALLOW_DOWNGRADE" -eq 0 ]; then
334
+ echo "⛔ $(t "elanous $INSTALLED_VERSION is installed; $PACKAGE_VERSION is older, so nothing was changed. To go back on purpose, rerun with --allow-downgrade." "elanous $INSTALLED_VERSION 이(가) 설치돼 있고 $PACKAGE_VERSION 은(는) 더 낮은 판이라 아무것도 바꾸지 않았습니다. 일부러 내리려면 --allow-downgrade 를 붙여 다시 실행하세요.")" >&2
335
+ exit 3
336
+ fi
337
+ if [ "$INTERACTIVE" -eq 1 ] && [ "$ASSUME_YES" -eq 0 ]; then
338
+ printf '%s ' "$(t "elanous $INSTALLED_VERSION is already installed. Update to $PACKAGE_VERSION? [Y/n]" "elanous $INSTALLED_VERSION 이(가) 이미 설치돼 있습니다. $PACKAGE_VERSION 로 업데이트할까요? [Y/n]")"
339
+ if [ -n "${ELANOUS_INSTALL_ANSWER+x}" ]; then ANSWER="$ELANOUS_INSTALL_ANSWER"; else ANSWER=""; read -r ANSWER </dev/tty || ANSWER=""; fi
340
+ echo ""
341
+ case "$ANSWER" in
342
+ n|N|no|NO|No|아니오|아니요)
343
+ echo "$(t "Left elanous $INSTALLED_VERSION as it is." "elanous $INSTALLED_VERSION 을(를) 그대로 두었습니다.")"
344
+ exit 0 ;;
345
+ esac
346
+ fi
347
+ fi
298
348
  VERSION_NAME="$PACKAGE_VERSION${VERSION_SUFFIX:-}"
299
349
  VERSION_DIR="$PREFIX/versions/$VERSION_NAME"
300
350
  mkdir -p "$VERSION_DIR"
@@ -312,15 +362,25 @@ printf '#!/bin/sh\nexec "%s" x node-gyp@latest "$@"\n' "$BUN_EXEC" > "$NATIVE_SH
312
362
  chmod +x "$NATIVE_SHIM/node-gyp"
313
363
  if ! (cd "$VERSION_DIR" && PATH="$NATIVE_SHIM:$PATH" bun add --no-save --offline "$INSTALL_TARBALL" >/dev/null 2>&1); then
314
364
  echo "dependencies not in the local bun cache — fetching them from the npm registry" >&2
315
- (cd "$VERSION_DIR" && PATH="$NATIVE_SHIM:$PATH" bun add --no-save "$INSTALL_TARBALL")
365
+ # bun prints «Blocked N postinstalls» and progress noise here — keep it out of the screen unless this step fails.
366
+ ADD_LOG="$(mktemp "${TMPDIR:-/tmp}/elanous-bun-add.XXXXXX")"
367
+ ADD_RC=0
368
+ (cd "$VERSION_DIR" && PATH="$NATIVE_SHIM:$PATH" bun add --no-save "$INSTALL_TARBALL") >"$ADD_LOG" 2>&1 || ADD_RC=$?
369
+ if [ "$ADD_RC" -ne 0 ]; then
370
+ tail -n 20 "$ADD_LOG" >&2
371
+ rm -f "$ADD_LOG"; rm -rf "$NATIVE_SHIM"
372
+ exit "$ADD_RC"
373
+ fi
374
+ rm -f "$ADD_LOG"
316
375
  fi
317
376
  rm -rf "$NATIVE_SHIM"
318
377
  ln -sfn "versions/$VERSION_NAME" "$PREFIX/current"
319
378
  mkdir -p "$PREFIX/bin"
320
379
  # 절대 bun 경로를 설치 때 박고 엔트리는 current 를 거친다 — cron/systemd 의 짧은 PATH 와 판 전환 모두 지원.
321
380
  WRAPPER="$PREFIX/bin/.elanous-$$"
322
- printf '#!/bin/sh\n# elanous-wrapper\nif [ ! -x %s ]; then\n printf '\''bun 을 찾을 수 없습니다: %%s — 설치기를 다시 실행하세요\\n'\'' %s >&2\n exit 127\nfi\nexec %s %s "$@"\n' \
323
- "$(shell_quote "$BUN_EXEC")" "$(shell_quote "$BUN_EXEC")" "$(shell_quote "$BUN_EXEC")" \
381
+ BUN_MISSING_MSG="$(t 'bun not found: %s — run the installer again' 'bun 을 찾을 수 없습니다: %s — 설치기를 다시 실행하세요')"
382
+ printf '#!/bin/sh\n# elanous-wrapper\nif [ ! -x %s ]; then\n printf '\''%s\\n'\'' %s >&2\n exit 127\nfi\nexec %s %s "$@"\n' \
383
+ "$(shell_quote "$BUN_EXEC")" "$BUN_MISSING_MSG" "$(shell_quote "$BUN_EXEC")" "$(shell_quote "$BUN_EXEC")" \
324
384
  "$(shell_quote "$PREFIX/current/node_modules/elanous/bin/elanous.mjs")" > "$WRAPPER"
325
385
  chmod +x "$WRAPPER"
326
386
  mv -f "$WRAPPER" "$PREFIX/bin/elanous"
@@ -329,7 +389,7 @@ ELN_ON_PATH="$(command -v eln 2>/dev/null || true)"
329
389
  if { [ -n "$ELN_ON_PATH" ] && [ "$ELN_ON_PATH" != "$ELN" ]; } ||
330
390
  { { [ -e "$ELN" ] || [ -L "$ELN" ]; } &&
331
391
  { [ -L "$ELN" ] || [ ! -f "$ELN" ] || [ "$(sed -n '2p' "$ELN")" != '# elanous-wrapper' ]; }; }; then
332
- echo '⚠ eln: 이미 다른 명령이 있어 만들지 않았다 — elanous 로 쓰십시오'
392
+ echo "⚠ eln: $(t 'already used by another program, so it was not created — run elanous instead.' '이미 다른 프로그램이 eln 을 써서 만들지 않았습니다 — elanous 로 실행하세요.')"
333
393
  ELN_AVAILABLE=0
334
394
  else
335
395
  cp "$PREFIX/bin/elanous" "$ELN"
@@ -368,21 +428,26 @@ if [ "$MODIFY_PATH" -eq 1 ] && [ -n "${LOGIN_STARTUP:-}" ] && ! grep -Fqx "$MARK
368
428
  printf '\n%s\n%s\n%s\n' "$MARKER_START" "$PATH_LINE" "$MARKER_END" >> "$LOGIN_STARTUP"
369
429
  fi
370
430
 
371
- echo "Installed elanous $VERSION at $PREFIX/bin/elanous"
431
+ echo "$(t "Installed elanous $VERSION at $PREFIX/bin/elanous" "elanous $VERSION 을(를) 설치했습니다: $PREFIX/bin/elanous")"
372
432
 
373
433
  # ── 다음 걸음 (2026-09-23 · Phase 3 「사람 손」) ─────────────────────────
374
434
  # ⛔ 설치가 끝나도 «무엇을 더 쳐야 하나»를 안 말하면, 빠뜨린 손이 나중에 «다른 원인의 얼굴»로 나타난다
375
435
  # (예: 로그인 누락이 quota-exhausted 로 분류됐다 — 09-21 실측). 그래서 «지금 상태»로 계산해 말한다.
376
436
  # provider 설정은 «안» 적는다 — 빈 config(auto)는 로그인만 있으면 런타임이 codex 로 고른다(#19950).
437
+ # --no-setup keeps the prompt above but never starts setup.
438
+ START_SETUP=$INTERACTIVE
439
+ [ "$RUN_SETUP" -eq 1 ] || START_SETUP=0
440
+
377
441
  echo ""
378
- echo "Next:"
442
+ echo "$(t 'Next:' '다음:')"
379
443
  STEP=1
444
+ # INST1 order: new shell → elanous (first-time setup, which also signs in) → missing tools → harness say.
380
445
  case ":$PATH:" in
381
446
  *":$PREFIX/bin:"*) ;;
382
- *) if [ "$MODIFY_PATH" -eq 1 ]; then echo " $STEP) open a new shell (or: source $STARTUP)"; else echo " $STEP) add $PREFIX/bin to PATH"; fi; STEP=$((STEP + 1)) ;;
447
+ *) if [ "$MODIFY_PATH" -eq 1 ]; then echo " $STEP) $(t "open a new shell (or: source $STARTUP)" "새 셸을 여세요(또는: source $STARTUP)")"; else echo " $STEP) $(t "add $PREFIX/bin to PATH" "$PREFIX/bin 을 PATH 에 넣으세요")"; fi; STEP=$((STEP + 1)) ;;
383
448
  esac
384
- if ! grep -q '"openai-codex"' "$HOME/.elanous/auth.json" 2>/dev/null; then
385
- echo " $STEP) elanous login openai-codex # ChatGPT subscription (device code, no API key)"; STEP=$((STEP + 1))
449
+ if [ "$START_SETUP" -eq 0 ]; then
450
+ echo " $STEP) $(t "Run: $PREFIX/bin/elanous # first-time setup (in a new shell: elanous)" "실행: $PREFIX/bin/elanous # 첫 설정(새 셸에서는: elanous)")"; STEP=$((STEP + 1))
386
451
  fi
387
452
  # 🆕 2026-09-24 빈 VM 실측: 하니스(codex 백엔드)는 Codex CLI 를 자식으로 띄우는데 빈 기계엔 codex·node 가 «둘 다» 없었다.
388
453
  # ⚠️ 아래 설치 줄은 빈 기계에서 아직 «안 쟀다» — `elanous doctor` 의 codex 줄이 판정한다.
@@ -440,8 +505,22 @@ elif ! gh auth status >/dev/null 2>&1; then
440
505
  echo " $STEP) gh auth login # the harness opens pull requests with it"; STEP=$((STEP + 1))
441
506
  fi
442
507
  if [ "$ELN_AVAILABLE" -eq 1 ]; then
443
- echo " $STEP) eln harness say \"<one line of what you want>\" (or: elanous harness say)"
508
+ echo " $STEP) $(t 'eln harness say "<one line of what you want>" (or: elanous harness say)' 'eln harness say "<하고 싶은 일 한 줄>" (또는: elanous harness say)')"
444
509
  else
445
- echo " $STEP) elanous harness say \"<one line of what you want>\""
510
+ echo " $STEP) $(t 'elanous harness say "<one line of what you want>"' 'elanous harness say "<하고 싶은 일 한 줄>"')"
511
+ fi
512
+ echo " $(t 'check anytime: elanous setup --non-interactive' '언제든 점검: elanous setup --non-interactive')"
513
+
514
+ # INST2 — finish in the same window: start first-time setup with the absolute wrapper (no source, no new shell).
515
+ if [ "$START_SETUP" -eq 1 ]; then
516
+ echo ""
517
+ echo "$(t 'Starting first-time setup… (skip it next time with --no-setup)' '첫 설정을 시작합니다… (다음에 건너뛰려면 --no-setup)')"
518
+ case ":$PATH:" in
519
+ *":$PREFIX/bin:"*) ;;
520
+ *) echo " $(t "(new shells will find elanous on PATH; this window uses $PREFIX/bin/elanous)" "(새 셸에서는 PATH 로 elanous 를 찾습니다 · 이 창은 $PREFIX/bin/elanous 를 씁니다)")" ;;
521
+ esac
522
+ if [ -n "${ELANOUS_INSTALL_SETUP_EXEC:-}" ]; then
523
+ exec "$ELANOUS_INSTALL_SETUP_EXEC" "$PREFIX/bin/elanous"
524
+ fi
525
+ exec "$PREFIX/bin/elanous" </dev/tty
446
526
  fi
447
- echo " check anytime: elanous setup --non-interactive"
@@ -0,0 +1,114 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { repoRoot } from '../../src/domains/schedule-registry.js';
6
+
7
+ export interface CheckoutStep { name: string; ok: boolean; seconds: number; tail: string }
8
+ export interface CheckoutResult { pr: number; mergeSha: string; ok: boolean; steps: CheckoutStep[] }
9
+ export interface CheckoutDeps {
10
+ git: (args: string[], options: { cwd: string; timeout: number }) => string;
11
+ run: (args: string[], options: { cwd: string; timeout: number; env?: NodeJS.ProcessEnv }) => string;
12
+ now: () => number;
13
+ }
14
+
15
+ const LIMIT_MS = 600_000;
16
+ const tail = (text: string): string => text.trimEnd().split('\n').slice(-20).join('\n');
17
+ const validPath = (file: string): boolean => file.length > 0 && !file.startsWith('/') && !file.startsWith('-') && !file.split('/').includes('..');
18
+ const errorOutput = (error: unknown): string => {
19
+ if (!error || typeof error !== 'object') return String(error);
20
+ const e = error as { stdout?: string | Buffer; stderr?: string | Buffer; message?: string };
21
+ return [e.stdout, e.stderr, e.message].filter(Boolean).map(String).join('\n') || String(error);
22
+ };
23
+ const defaultDeps: CheckoutDeps = {
24
+ git: (args, options) => execFileSync('git', args, { ...options, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024 }),
25
+ run: (args, options) => {
26
+ const result = spawnSync('bun', args, { ...options, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
27
+ if (result.error || result.status !== 0) throw { stdout: result.stdout, stderr: result.stderr, message: result.error?.message ?? `bun exit ${result.status}` };
28
+ return `${result.stdout}${result.stderr}`;
29
+ },
30
+ now: Date.now,
31
+ };
32
+
33
+ /** A single merge commit, with installed dependencies owned by its detached worktree. */
34
+ export function verifyCleanCheckout({ pr, mergeSha, files, deps = defaultDeps }: {
35
+ pr: number; mergeSha: string; files: string[]; deps?: CheckoutDeps;
36
+ }): CheckoutResult {
37
+ const steps: CheckoutStep[] = [];
38
+ const root = repoRoot();
39
+ const start = deps.now();
40
+ const deadline = start + LIMIT_MS;
41
+ const workspace = mkdtempSync(join(tmpdir(), 'landing-heal-clean-'));
42
+ const checkout = join(workspace, 'checkout');
43
+ let ok = true;
44
+ const step = (name: string, command: (timeout: number) => string): boolean => {
45
+ const began = deps.now();
46
+ try {
47
+ const remaining = deadline - began;
48
+ if (remaining <= 0) throw new Error('clean checkout 600s timeout');
49
+ const out = command(remaining);
50
+ steps.push({ name, ok: true, seconds: (deps.now() - began) / 1000, tail: tail(out) });
51
+ return true;
52
+ } catch (error) {
53
+ steps.push({ name, ok: false, seconds: (deps.now() - began) / 1000, tail: tail(errorOutput(error)) });
54
+ ok = false;
55
+ return false;
56
+ }
57
+ };
58
+ try {
59
+ if (step('worktree add', (timeout) => {
60
+ const commit = `${mergeSha}^{commit}`;
61
+ const git = (args: string[]) => {
62
+ const remaining = Math.min(timeout, deadline - deps.now());
63
+ if (remaining <= 0) throw new Error('clean checkout 600s timeout');
64
+ return deps.git(args, { cwd: root, timeout: remaining });
65
+ };
66
+ try {
67
+ git(['cat-file', '-e', commit]);
68
+ } catch (error) {
69
+ if (deadline - deps.now() <= 0) throw error;
70
+ git(['fetch', '--no-tags', 'origin', mergeSha]);
71
+ git(['cat-file', '-e', commit]);
72
+ }
73
+ return git(['worktree', 'add', '--detach', checkout, mergeSha]);
74
+ })) {
75
+ if (step('bun install --frozen-lockfile', (timeout) => deps.run(['install', '--frozen-lockfile'], { cwd: checkout, timeout }))) {
76
+ const changedTests = files.filter((file) => /\.(?:test|spec)\.[^/]+$/.test(file));
77
+ const unsupported = changedTests.filter((file) => !validPath(file) || !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file));
78
+ const tests = [...new Set(changedTests.filter((file) => validPath(file)))];
79
+ if (unsupported.length) {
80
+ step('select changed tests', () => { throw new Error(`unsupported changed test files: ${unsupported.join(', ')}`); });
81
+ } else if (tests.length) {
82
+ step('bun test', (timeout) => {
83
+ const out = deps.run(['test', ...tests], { cwd: checkout, timeout });
84
+ const count = /Ran (\d+) tests? across (\d+) files?/.exec(out);
85
+ if (!count || Number(count[1]) === 0) throw new Error(`bun test did not run any tests: ${tail(out)}`);
86
+ if (Number(count[2]) !== tests.length) throw new Error(`bun test ran ${count[2]} of ${tests.length} changed test files: ${tail(out)}`);
87
+ return out;
88
+ });
89
+ } else if (files.some((file) => validPath(file) && /\.tsx?$/.test(file))) {
90
+ // ci-typecheck-changed.ts rejects --base: its supported base selector is TSC_BASE_REF.
91
+ step('typecheck changed', (timeout) => deps.run(['run', 'scripts/ci-typecheck-changed.ts'], {
92
+ cwd: checkout, timeout, env: { ...process.env, TSC_BASE_REF: `${mergeSha}^`, GITHUB_BASE_REF: '' },
93
+ }));
94
+ } else {
95
+ step('select changed TypeScript', () => { throw new Error('no existing changed .ts files to verify'); });
96
+ }
97
+ }
98
+ }
99
+ } finally {
100
+ // Cleanup has its own budget: exhausting the PR deadline must not skip removal.
101
+ const cleanupStart = deps.now();
102
+ try {
103
+ const out = deps.git(['worktree', 'remove', '--force', checkout], { cwd: root, timeout: 30_000 });
104
+ steps.push({ name: 'worktree remove', ok: true, seconds: (deps.now() - cleanupStart) / 1000, tail: tail(out) });
105
+ } catch (error) {
106
+ steps.push({ name: 'worktree remove', ok: false, seconds: (deps.now() - cleanupStart) / 1000, tail: tail(errorOutput(error)) });
107
+ ok = false;
108
+ }
109
+ try { rmSync(workspace, { recursive: true, force: true }); }
110
+ catch (error) { steps.push({ name: 'remove temporary folder', ok: false, seconds: 0, tail: tail(errorOutput(error)) }); ok = false; }
111
+ if (existsSync(workspace)) { steps.push({ name: 'remove temporary folder', ok: false, seconds: 0, tail: 'temporary folder still exists' }); ok = false; }
112
+ }
113
+ return { pr, mergeSha, ok, steps };
114
+ }