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,159 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { emitNodeResult, readGraphContext, runCommand, type CommandRunner, type GraphContext } from './node-verdict.js';
5
+
6
+ const FALLBACK = 'Known issues have been reported for this release.';
7
+ // No fix schedule is promised here — the inputs do not carry one (review R2).
8
+ const INTRO = 'The following issues are known in this release.';
9
+ // A known-issues bullet describes a problem that is still there; «fixed / resolved» wording turns it into a false claim.
10
+ const resolved = /\b(?:fixed|resolved|no longer|has been addressed|is addressed|was addressed)\b/i;
11
+ const unsafe = /[^\x20-\x7e]|#\d+|\/Users\/|\.test\.ts\b|\[(?:TC|MK|OP|UX|S|T|F|O)\]|\b(?:TC|MK|OP|UX)\s+track\b|\b(?:PR|pull request)\s*\d+\b|\b(?:scripts|src|test)\/\S+|\bfile:\/\/|(?<![\w:/])\/(?!\/)[\w.-]+(?:\/[\w.-]+)*|\b[A-Z]:\\(?:[^\s\\]+\\)*[^\s\\]+|\b(?:RFC|HANDOFF|MANUAL)-/i;
12
+
13
+ type Issue = { id: string; title?: string; evidence?: string; note?: string };
14
+
15
+ function issues(value: unknown, kind: 'checklist' | 'accepted'): Issue[] {
16
+ if (value === undefined) return [];
17
+ if (!Array.isArray(value) || !value.every((item) => item && typeof item === 'object'
18
+ && typeof item.id === 'string' && item.id.trim()
19
+ && (kind === 'checklist' ? typeof item.title === 'string' && typeof item.evidence === 'string' : typeof item.note === 'string'))) {
20
+ throw new Error(`invalid ${kind} known issues`);
21
+ }
22
+ return value as Issue[];
23
+ }
24
+
25
+ function generatedBullets(raw: string, sourceIds: string[]): string[] {
26
+ const envelope: unknown = JSON.parse(raw.trim());
27
+ const reply = envelope && typeof envelope === 'object' && !Array.isArray(envelope) && 'reply' in envelope
28
+ ? envelope.reply : envelope;
29
+ if (typeof reply !== 'string') throw new Error('empty ask reply');
30
+ const entries: unknown = JSON.parse(reply.replace(/^```(?:json)?\s*\n|\n```\s*$/g, '').trim());
31
+ if (!Array.isArray(entries) || entries.length !== sourceIds.length || new Set(sourceIds).size !== sourceIds.length) {
32
+ throw new Error('known issues response does not cover every input ID');
33
+ }
34
+ const byId = new Map<string, string>();
35
+ for (const entry of entries) {
36
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)
37
+ || typeof entry.id !== 'string' || typeof entry.bullet !== 'string'
38
+ || byId.has(entry.id) || !sourceIds.includes(entry.id)) {
39
+ throw new Error('known issues response does not cover every input ID');
40
+ }
41
+ byId.set(entry.id, entry.bullet);
42
+ }
43
+ if (sourceIds.some((id) => !byId.has(id))) throw new Error('known issues response does not cover every input ID');
44
+ return sourceIds.map((id) => byId.get(id)!);
45
+ }
46
+
47
+ function coversIssue(bullet: string, issue: Issue, others: Issue[]): boolean {
48
+ const source = issue.title ?? issue.note ?? '';
49
+ const words = source.toLowerCase().match(/[a-z]{5,}/g) ?? [];
50
+ const otherWords = new Set(others.flatMap((other) => ((other.title ?? other.note ?? '').toLowerCase().match(/[a-z]{5,}/g) ?? []).map((word) => word.slice(0, 5))));
51
+ const publicWords = new Set((bullet.toLowerCase().match(/[a-z]{5,}/g) ?? []).map((word) => word.slice(0, 5)));
52
+ const distinctive = [...new Set(words.map((word) => word.slice(0, 5)))].filter((word) => !otherWords.has(word));
53
+ const negative = /\b(?:not|never|no|cannot|can't|doesn't|don't|won't|fails? to|isn't|aren't)\b/i;
54
+ if (negative.test(source) && !negative.test(bullet)) return false;
55
+ // A source with no English words to compare (e.g. a Korean checklist title) is covered structurally —
56
+ // one bullet per input ID — instead of blocking the release (review R2).
57
+ if (words.length === 0) return true;
58
+ return distinctive.length > 0 && distinctive.filter((word) => publicWords.has(word)).length >= Math.min(issue.note === undefined ? 1 : 2, distinctive.length);
59
+ }
60
+
61
+ function cleanBullet(text: string): string | null {
62
+ const line = text.trim();
63
+ if (!line || /^[-*]\s/.test(line) || unsafe.test(line) || /[{}<>\[\]`]/.test(line) || (line.match(/[.!?](?:\s|$)/g) ?? []).length > 2) return null;
64
+ return line;
65
+ }
66
+
67
+ function command(run: CommandRunner, program: string, args: string[], cwd?: string): string {
68
+ const result = run(program, args, cwd);
69
+ if (result.status !== 0) throw new Error(`${program} ${args[0]} failed: ${result.stderr || result.stdout}`);
70
+ return result.stdout;
71
+ }
72
+
73
+ export function runKnownIssues(context: GraphContext = readGraphContext(), run: CommandRunner = runCommand) {
74
+ const version = context.input.version;
75
+ const worktree = context.outputs.docs?.worktree;
76
+ if (typeof worktree !== 'string' || !worktree || !existsSync(worktree)) throw new Error('docs 워크트리 없음');
77
+ if (context.outputs.docs?.branch !== `release-docs/${version}`) throw new Error('docs branch/version mismatch');
78
+ const notes = join(worktree, 'release/public/docs/releases', `${version}.md`);
79
+ if (!existsSync(notes)) throw new Error(`release notes missing: ${notes}`);
80
+ const checklist = issues(context.outputs['checklist-gate']?.knownIssues, 'checklist');
81
+ const accepted = issues(context.input.acceptedRegressions, 'accepted');
82
+ const sources = { checklist: checklist.length, accepted: accepted.length };
83
+ if (!checklist.length && !accepted.length) return { outcome: 'ok' as const, verdict: 'pass' as const, summary: '알려진 문제 없음', count: 0, bullets: [] as string[], sources, fallbackUsed: false };
84
+
85
+ let bullets: string[];
86
+ let fallbackUsed = false;
87
+ const allIssues = [...checklist, ...accepted];
88
+ const sourceIds = allIssues.map((issue) => issue.id);
89
+ const prompt = `Write public English release-note bullets for these known issues. Describe only user-visible impact; include a one-line workaround when available. No internal names, test paths, track names, PR numbers, or people. Each bullet must have at most two sentences. Return ONLY a JSON array with exactly one object {"id":"input ID","bullet":"English public sentence"} for each input, using each input ID exactly once. Do not include IDs in bullet text. Input: ${JSON.stringify({ checklist, acceptedRegressions: accepted })}`;
90
+ let response: ReturnType<CommandRunner>;
91
+ try {
92
+ response = run('elanous', ['--test', 'ask', '--json', prompt]);
93
+ } catch {
94
+ response = { status: 1, stdout: '', stderr: '' };
95
+ }
96
+ if (response.status !== 0) {
97
+ bullets = [FALLBACK];
98
+ fallbackUsed = true;
99
+ } else {
100
+ try {
101
+ bullets = generatedBullets(response.stdout, sourceIds).map((line, index) => {
102
+ const safe = cleanBullet(line);
103
+ if (!safe) {
104
+ fallbackUsed = true;
105
+ return FALLBACK;
106
+ }
107
+ if (resolved.test(safe)) throw new Error(`bullet for ${sourceIds[index]} claims the issue is resolved`);
108
+ if (!coversIssue(safe, allIssues[index]!, allIssues.filter((_, otherIndex) => otherIndex !== index))) {
109
+ throw new Error(`bullet does not identify input ${sourceIds[index]}`);
110
+ }
111
+ return safe;
112
+ });
113
+ if (new Set(bullets).size !== bullets.length && bullets.some((bullet) => bullet !== FALLBACK)) {
114
+ throw new Error('known issues response repeats an issue instead of covering each input');
115
+ }
116
+ bullets = [...new Set(bullets)];
117
+ } catch (error) {
118
+ throw new Error(`known issues coverage not verified: ${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ }
121
+ const original = readFileSync(notes, 'utf8');
122
+ const heading = /^## Known issues\s*$/m;
123
+ const match = heading.exec(original);
124
+ let updated: string;
125
+ if (match) {
126
+ const start = match.index + match[0].length;
127
+ const next = /^## /gm;
128
+ next.lastIndex = start;
129
+ const end = next.exec(original)?.index ?? original.length;
130
+ const section = original.slice(start, end);
131
+ const additions = bullets.filter((bullet) => !section.split(/\r?\n/).some((line) => line.trim() === `- ${bullet}`));
132
+ const before = original.slice(0, end);
133
+ updated = additions.length ? `${before}${before.endsWith('\n') ? '' : '\n'}${additions.map((bullet) => `- ${bullet}`).join('\n')}\n\n${original.slice(end)}` : original;
134
+ } else {
135
+ updated = `${original.trimEnd()}\n\n## Known issues\n\n${INTRO}\n\n${bullets.map((bullet) => `- ${bullet}`).join('\n')}\n`;
136
+ }
137
+ if (updated !== original) {
138
+ writeFileSync(notes, updated);
139
+ command(run, 'git', ['add', '--', `release/public/docs/releases/${version}.md`], worktree);
140
+ command(run, 'git', ['commit', '-m', `docs: known issues for ${version}`], worktree);
141
+ }
142
+ // Push whenever the branch is ahead of its remote — a rerun after a failed push finds the bullets already in the file (review R2).
143
+ command(run, 'git', ['fetch', 'origin', `release-docs/${version}`], worktree);
144
+ const ahead = Number(command(run, 'git', ['rev-list', '--count', `origin/release-docs/${version}..HEAD`], worktree).trim());
145
+ if (!Number.isFinite(ahead)) throw new Error('could not compare release-docs branch with its remote');
146
+ if (ahead > 0) command(run, 'git', ['push', 'origin', `release-docs/${version}`], worktree);
147
+ return { outcome: 'ok' as const, verdict: 'pass' as const, summary: `known issues: ${bullets.length}`, count: bullets.length, bullets, sources, fallbackUsed };
148
+ }
149
+
150
+ if (import.meta.main) {
151
+ try {
152
+ const result = runKnownIssues();
153
+ emitNodeResult(result);
154
+ process.exitCode = 0;
155
+ } catch (error) {
156
+ emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: error instanceof Error ? error.message : String(error), count: 0, bullets: [], sources: { checklist: 0, accepted: 0 }, fallbackUsed: false });
157
+ process.exitCode = 1;
158
+ }
159
+ }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bun
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { debug } from '../../src/debug/log.js';
7
+ import { sendOutbound } from '../../src/domains/outbound-alert.js';
8
+ import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
9
+ import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
10
+
11
+ type Result = { outcome: 'ok' | 'fail'; verdict: 'pass' | 'fail'; summary: string; npm?: 'published' | 'already-published' | 'staged-not-visible'; humanAction?: 'npm-support' };
12
+ type Exec = (command: string, args: string[], env?: NodeJS.ProcessEnv) => { status: number | null; stdout: string; stderr: string };
13
+ type Registry = (url: string) => Promise<{ status: number; body: unknown }>;
14
+ export interface NpmPublishDeps {
15
+ exec?: Exec;
16
+ registry?: Registry;
17
+ sleep?: (ms: number) => Promise<void>;
18
+ stateDir?: string;
19
+ alert?: (text: string, kind: string) => boolean;
20
+ log?: (version: string, waitedMinutes: number) => void;
21
+ }
22
+
23
+ const exec: Exec = (command, args, env) => {
24
+ const result = spawnSync(command, args, { encoding: 'utf8', env: env ?? process.env, maxBuffer: 1024 * 1024, timeout: 300_000 });
25
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? String(result.error ?? '') };
26
+ };
27
+ const registry: Registry = async (url) => {
28
+ const response = await fetch(url, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(15_000) });
29
+ return { status: response.status, body: response.ok ? await response.json() : null };
30
+ };
31
+ const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
32
+ const PACKAGE_URL = 'https://registry.npmjs.org/elanous';
33
+
34
+ function redact(text: string, token: string): string {
35
+ return token ? text.replaceAll(token, '[REDACTED]') : text;
36
+ }
37
+ function reason(error: unknown, token: string): string {
38
+ return redact(error instanceof Error ? error.message : String(error), token).replace(/[\r\n]+/g, ' ').slice(0, 500);
39
+ }
40
+ function metadata(value: unknown): { name?: string; version?: string } {
41
+ if (!value || typeof value !== 'object') return {};
42
+ return value as { name?: string; version?: string };
43
+ }
44
+
45
+ export async function runNpmPublish(context: GraphContext = readGraphContext(), deps: NpmPublishDeps = {}): Promise<Result> {
46
+ const version = context.input.version;
47
+ let token = '';
48
+ try {
49
+ if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before npm publish');
50
+ const stateDir = deps.stateDir ?? effectiveInstanceRoot();
51
+ const candidate = join(stateDir, 'release', version, 'prepared', 'dist', 'elanous.tgz');
52
+ const run = deps.exec ?? exec;
53
+ const readRegistry = deps.registry ?? registry;
54
+ const wait = deps.sleep ?? sleep;
55
+ const waitMinutes = context.input.npmWaitMinutes ?? 40;
56
+ if (typeof waitMinutes !== 'number' || !Number.isFinite(waitMinutes) || waitMinutes < 0 || waitMinutes > 40) throw new Error('input.npmWaitMinutes must be between 0 and 40');
57
+ const archive = run('tar', ['-xOzf', candidate, 'package/package.json']);
58
+ if (archive.status !== 0) throw new Error(`npm archive invalid: ${archive.stderr}`);
59
+ const pkg = metadata(JSON.parse(archive.stdout));
60
+ if (pkg.name !== 'elanous' || pkg.version !== version) throw new Error('npm archive package name/version mismatch');
61
+ const inspect = async (): Promise<{ visible: boolean; tagged: boolean }> => {
62
+ const [manifest, tags] = await Promise.all([readRegistry(`${PACKAGE_URL}/${version}`), readRegistry(PACKAGE_URL)]);
63
+ if (manifest.status !== 200 && manifest.status !== 404) throw new Error(`npm registry version lookup failed (${manifest.status})`);
64
+ if (tags.status !== 200) throw new Error(`npm registry tags lookup failed (${tags.status})`);
65
+ const latest = tags.body && typeof tags.body === 'object' && 'dist-tags' in tags.body
66
+ ? (tags.body as { 'dist-tags'?: { latest?: unknown } })['dist-tags']?.latest : undefined;
67
+ if (manifest.status === 200 && (metadata(manifest.body).name !== 'elanous' || metadata(manifest.body).version !== version))
68
+ throw new Error('npm registry version metadata mismatch');
69
+ return { visible: manifest.status === 200, tagged: manifest.status === 200 && latest === version };
70
+ };
71
+ const before = await inspect();
72
+ if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with latest tag` };
73
+ if (!before.visible) {
74
+ const tokenFile = context.input.npmTokenFile ?? join(stateDir, 'secrets', 'npm-token');
75
+ if (typeof tokenFile !== 'string' || !tokenFile.trim()) throw new Error('input.npmTokenFile must be a path');
76
+ token = readFileSync(tokenFile, 'utf8').trim();
77
+ if (!token) throw new Error('npm token file empty');
78
+ const dir = mkdtempSync(join(tmpdir(), 'release-npmrc-'));
79
+ let published;
80
+ try {
81
+ const npmrc = join(dir, 'npmrc');
82
+ writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}\n`, { mode: 0o600, flag: 'wx' });
83
+ published = run('npm', ['publish', candidate, '--ignore-scripts', '--tag', 'latest', '--access', 'public'], { ...process.env, NPM_CONFIG_USERCONFIG: npmrc });
84
+ } finally { rmSync(dir, { recursive: true, force: true }); }
85
+ const output = `${published.stdout}\n${published.stderr}`;
86
+ const staged = published.status !== 0 && /\bE409\b/i.test(output) && /previously staged/i.test(output);
87
+ if (published.status !== 0 && !staged) throw new Error(`npm publish failed (rc=${published.status}): ${redact(output, token)}`);
88
+ }
89
+ const polls = Math.ceil(waitMinutes * 60 / 30);
90
+ let last = before;
91
+ for (let i = 0; i <= polls; i++) {
92
+ last = await inspect();
93
+ if (last.tagged) return { outcome: 'ok', verdict: 'pass', npm: before.visible ? 'already-published' : 'published', summary: `npm ${version} visible with latest tag` };
94
+ if (i < polls) await wait(Math.min(30_000, waitMinutes * 60_000 - i * 30_000));
95
+ }
96
+ if (last.visible) throw new Error(`npm ${version} visible but dist-tags.latest did not reach ${version} within ${waitMinutes} minutes`);
97
+ try { (deps.log ?? ((v, minutes) => debug.log('release-loop.npm', 'staged-not-visible', { version: v, waitedMinutes: minutes })))(version, waitMinutes); } catch { /* observation must not mask npm's staged state */ }
98
+ try { (deps.alert ?? sendOutbound)(`npm ${version} 가 스테이징에 멈춤 — 계정 소유자 확인 필요`, 'alert'); } catch { /* alert failure must not block docs */ }
99
+ return { outcome: 'ok', verdict: 'pass', npm: 'staged-not-visible', humanAction: 'npm-support', summary: 'npm 스테이징 — 레지스트리에 아직 없음' };
100
+ } catch (error) {
101
+ return { outcome: 'fail', verdict: 'fail', summary: `npm ${version} failed: ${reason(error, token)}` };
102
+ }
103
+ }
104
+
105
+ if (import.meta.main) {
106
+ let version = '';
107
+ try {
108
+ const context = readGraphContext();
109
+ version = context.input.version;
110
+ const result = await runNpmPublish(context);
111
+ debug.log('release-loop.npm', 'result', { version, outcome: result.outcome, npm: result.npm });
112
+ emitNodeResult(result);
113
+ process.exitCode = result.outcome === 'ok' ? 0 : 1;
114
+ } catch {
115
+ emitNodeResult({ outcome: 'fail', verdict: 'fail', summary: 'npm publish context unavailable' });
116
+ process.exitCode = 1;
117
+ }
118
+ }
@@ -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,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
+ }