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
@@ -8,6 +8,7 @@ import { getUserConfig } from '../user-config.js';
8
8
  import { decideBudget, readBudgetInputsLive } from '../self-implement/budget-gate.js';
9
9
  import { findTrack } from './mission-phase-track.js';
10
10
  import { effectiveInstanceRoot } from '../instance/resolve.js';
11
+ import { decideShadowTrackAction, launchTrackAgentShadow, type ShadowOptions } from './track-agent-shadow.js';
11
12
 
12
13
  export type TrackAgentInput = { missionId: string; taskId: string; title: string; prompt: string; track: string };
13
14
  export type TrackAgentDecision = { action: 'say' | 'hold'; reason?: string };
@@ -25,6 +26,8 @@ type RunnerDeps = {
25
26
  session?: (persona: string) => string;
26
27
  root?: string;
27
28
  maxConcurrent?: number;
29
+ shadow?: boolean;
30
+ shadowOptions?: ShadowOptions;
28
31
  };
29
32
  const root = resolve(import.meta.dir, '../..');
30
33
  // The installed package is not a git checkout — the daemon's target repository comes first.
@@ -41,7 +44,8 @@ const runCli = async (args: string[]): Promise<{ stdout: string }> => {
41
44
  const { argv, env } = universeLaunch(args);
42
45
  return exec('bun', argv, { cwd: repoDir(), env, timeout: 120_000, maxBuffer: 1024 * 1024, encoding: 'utf8' });
43
46
  };
44
- const forbidden = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merge|force|pay|purchase|secret|credential|sudo|rm\s+-rf|config\s+set)\b|(?:^|\s)--prod\b|삭제|배포|게시|릴리스|재시작|재부팅|병합|결제|자격|비밀|강제/i;
47
+ const forbiddenActionRegex = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merge|force|pay|purchase|secret|credential|sudo|rm\s+-rf|config\s+set)\b|(?:^|\s)--prod\b|삭제|배포|게시|릴리스|재시작|재부팅|병합|결제|자격|비밀|강제/i;
48
+ export const TRACK_AGENT_FORBIDDEN_ACTION_REGEX = new RegExp(forbiddenActionRegex.source, forbiddenActionRegex.flags);
45
49
  let running = 0;
46
50
 
47
51
  function context(input: TrackAgentInput, repo: string): string {
@@ -70,6 +74,17 @@ function context(input: TrackAgentInput, repo: string): string {
70
74
  }
71
75
 
72
76
  export async function decideTrackAction(input: TrackAgentInput, deps: RunnerDeps = {}): Promise<TrackAgentDecision> {
77
+ if (!(deps.shadow ?? getUserConfig().autopilot?.trackAgent?.shadow ?? false)) {
78
+ return decideTrackActionOnce(input, deps);
79
+ }
80
+ return decideShadowTrackAction(input, (value) => decideTrackActionOnce(value, deps), {
81
+ ...deps.shadowOptions,
82
+ maxDecisionsPerDay: deps.shadowOptions?.maxDecisionsPerDay
83
+ ?? getUserConfig().autopilot?.trackAgent?.shadowMaxDecisionsPerDay ?? 20,
84
+ });
85
+ }
86
+
87
+ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps): Promise<TrackAgentDecision> {
73
88
  try {
74
89
  const prompt = context(input, deps.root ?? repoDir());
75
90
  const persona = `track-${input.track}`;
@@ -83,7 +98,7 @@ export async function decideTrackAction(input: TrackAgentInput, deps: RunnerDeps
83
98
  const values = decision as Record<string, unknown>;
84
99
  if (!['say', 'hold'].includes(String(values.action)) || (values.reason !== undefined && typeof values.reason !== 'string')
85
100
  || Object.keys(values).some((key) => !['action', 'reason'].includes(key))) throw new Error('invalid decision');
86
- if (typeof values.reason === 'string' && forbidden.test(values.reason)) throw new Error('forbidden action');
101
+ if (typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
87
102
  return decision as TrackAgentDecision;
88
103
  } catch (error) {
89
104
  log('decision-failed', { track: input.track, taskId: input.taskId, error: redactSecretText(String(error)) });
@@ -93,7 +108,7 @@ export async function decideTrackAction(input: TrackAgentInput, deps: RunnerDeps
93
108
 
94
109
  export async function executeTrackAction(decision: TrackAgentDecision, input: TrackAgentInput, deps: RunnerDeps = {}): Promise<TrackAgentResult> {
95
110
  if (decision.action !== 'say') return { status: 'held', detail: decision.reason ?? '사람 확인' };
96
- if (forbidden.test(`${input.title}\n${input.prompt}`)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
111
+ if (forbiddenActionRegex.test(`${input.title}\n${input.prompt}`)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
97
112
  if (!findTrack(input.track)) return { status: 'held', detail: '트랙 미정 — 사람 확인' };
98
113
  try {
99
114
  const checkBudget = deps.checkBudget ?? (async () => {
@@ -112,6 +127,11 @@ export async function executeTrackAction(decision: TrackAgentDecision, input: Tr
112
127
  running++;
113
128
  let held = false;
114
129
  try {
130
+ if (deps.shadow ?? getUserConfig().autopilot?.trackAgent?.shadow ?? false) {
131
+ const result = await launchTrackAgentShadow(input, deps.shadowOptions);
132
+ log('shadow-orchestrate', { track: input.track, taskId: input.taskId, status: result.status, detail: result.detail });
133
+ return result;
134
+ }
115
135
  const launch = deps.launch ?? (async (args: string[]) => {
116
136
  const { argv, env } = universeLaunch(args);
117
137
  const child = spawn('bun', argv, { cwd: repoDir(), env, stdio: 'ignore', detached: true });
@@ -0,0 +1,16 @@
1
+ export function acpServerHelpText(): string {
2
+ return `
3
+ ACP server (Agent Client Protocol)
4
+ Usage: elanous --acp-server [--transport=stdio|unix-socket|websocket] [options]
5
+ Example: elanous --acp-server --transport=websocket
6
+
7
+ --transport=stdio|unix-socket|websocket Connection transport (default: stdio)
8
+ --socket-path=<path> Unix socket path (unix-socket only)
9
+ --port=<number> Listening port (websocket only)
10
+ --host=<host> Bind host (websocket only)
11
+ --tool-cwd=<path> Default working directory for tools
12
+ --no-auth Disable websocket token authentication
13
+
14
+ Zed setup and connection check: docs/manual/MANUAL-zed-acp.md
15
+ `;
16
+ }
@@ -1,3 +1,5 @@
1
+ import { classifyShortQuestion } from '../chat/short-question.js';
2
+ import { getUserConfig } from '../user-config.js';
1
3
  import { runCoreTurn } from '../core-turn/index.js';
2
4
  import type { LLMMessage, LLMToolSpec } from '../llm.js';
3
5
  import { maybeImageBearingResult } from '../llm.js';
@@ -83,6 +85,8 @@ export function summarizeToolResult(result: unknown): string | undefined {
83
85
 
84
86
  export async function runDaemonPromptTurn(opts: {
85
87
  history: DaemonSessionHistory;
88
+ /** B1 빠른 경로 스위치 — 없으면 config `chat.fastPath`(기본 true). 시험·호출부가 못 박을 때만 준다. */
89
+ fastPathEnabled?: boolean;
86
90
  request: DaemonPromptRequest;
87
91
  /** Optional multimodal prompt blocks for the current user turn.
88
92
  * When present, history persistence keeps the ACP block shape
@@ -173,6 +177,19 @@ export async function runDaemonPromptTurn(opts: {
173
177
  reason: surfaceResolutionReason,
174
178
  hasUserText: request.userText.length > 0,
175
179
  });
180
+ const hasAttachments = (opts.promptBlocks?.length ? opts.promptBlocks : request.userContent)
181
+ ?.some((block) => block.type !== 'text') ?? false;
182
+ // SW2 스위치 `chat.fastPath`(기본 켜짐 · PWA «짧은 물음은 빠르게» 카드) — 끄면 분류하지 않는다.
183
+ const fastPathEnabled = opts.fastPathEnabled ?? (() => { try { return getUserConfig().chat?.fastPath !== false; } catch { return true; } })();
184
+ const { fast, reason } = fastPathEnabled
185
+ ? classifyShortQuestion(request.userText, { hasAttachments })
186
+ : { fast: false, reason: 'disabled' };
187
+ const activeToolSurface = fast ? undefined : toolSurface;
188
+ const toolsBefore = toolSurface && toolSurface.kind !== 'none' ? toolSurface.specs.length : 0;
189
+ const tools: LLMToolSpec[] = activeToolSurface && activeToolSurface.kind !== 'none'
190
+ ? activeToolSurface.specs
191
+ : [];
192
+ debug.log('chat.fast-path', 'decided', { fast, reason, toolsBefore, toolsAfter: tools.length });
176
193
  // Image-pipeline followup #4 (2026-05-05) — when the webterm surface
177
194
  // is active, prepend a daemon-context block ("Current ACP sessionId:
178
195
  // …" + active terminals) to the system prompt so the LLM has the
@@ -181,21 +198,18 @@ export async function runDaemonPromptTurn(opts: {
181
198
  const augmentedSystemPrompt = appendWebtermContext(
182
199
  request.effectiveSystemPrompt,
183
200
  request.sessionId,
184
- toolSurface,
201
+ activeToolSurface,
185
202
  );
186
203
  const ctrl = new AbortController();
187
204
  if (opts.signal) {
188
205
  if (opts.signal.aborted) ctrl.abort();
189
206
  else opts.signal.addEventListener('abort', () => ctrl.abort(), { once: true });
190
207
  }
191
- const tools: LLMToolSpec[] = toolSurface && toolSurface.kind !== 'none'
192
- ? toolSurface.specs
193
- : [];
194
208
  const toolCwdResolver = createToolCwdResolver({
195
- tools: toolSurface?.kind ?? 'none',
209
+ tools: activeToolSurface?.kind ?? 'none',
196
210
  ...(opts.toolCwd !== undefined ? { toolCwd: opts.toolCwd } : {}),
197
211
  });
198
- const rawDispatchTool = toolSurface && toolSurface.kind !== 'none'
212
+ const rawDispatchTool = activeToolSurface && activeToolSurface.kind !== 'none'
199
213
  ? async (name: string, args: Record<string, unknown>, perCallCtx?: { callId: string }): Promise<unknown> => {
200
214
  const dispatchCtrl = new AbortController();
201
215
  const onAbort = (): void => dispatchCtrl.abort();
@@ -204,7 +218,7 @@ export async function runDaemonPromptTurn(opts: {
204
218
  // Image-pipeline followup #1 (2026-05-05) — pass the current
205
219
  // request.sessionId through so WebTerminal* tools can auto-
206
220
  // resolve scope when the LLM omits sessionId from args.
207
- return await toolSurface.dispatch(name, args, {
221
+ return await activeToolSurface.dispatch(name, args, {
208
222
  cwd: toolCwdResolver.cwd!,
209
223
  resolveWriteCwd: toolCwdResolver.resolveWriteCwd,
210
224
  signal: dispatchCtrl.signal,
@@ -246,9 +260,13 @@ export async function runDaemonPromptTurn(opts: {
246
260
  })
247
261
  : null;
248
262
  const dispatchTool = terminal ? terminal.dispatch : rawDispatchTool;
249
- const effectiveSystemPrompt = terminal
250
- ? terminal.systemPromptParts.join('\n\n')
251
- : augmentedSystemPrompt;
263
+ const fastPathGuidance = '확인이 필요하면 "확인해 볼까요?" 한 줄로 끝낸다';
264
+ const effectiveSystemPrompt = fast
265
+ ? [augmentedSystemPrompt, fastPathGuidance].filter(Boolean).join('\n')
266
+ : terminal
267
+ ? terminal.systemPromptParts.join('\n\n')
268
+ : augmentedSystemPrompt;
269
+ const isFirstTurn = history.get(request.sessionId).length === 0;
252
270
  const messages = opts.promptBlocks && opts.promptBlocks.length > 0
253
271
  ? appendUserPromptBlocksAndBuildMessages(
254
272
  history,
@@ -262,6 +280,9 @@ export async function runDaemonPromptTurn(opts: {
262
280
  request.userText,
263
281
  effectiveSystemPrompt,
264
282
  );
283
+ if (fast && !isFirstTurn) {
284
+ messages.unshift({ role: 'system', content: fastPathGuidance });
285
+ }
265
286
  const result = await runCoreTurn({
266
287
  sessionId: request.sessionId,
267
288
  userText: request.userText,
@@ -389,7 +389,13 @@ export function toolSurface(kind: DaemonToolSurfaceKind, cfg?: import('../../use
389
389
  }
390
390
  }
391
391
  if (shared.names.has(name)) {
392
- return shared.dispatch(name, args); // L2 core + L3 finance(gated) — buildSharedAppTools 단일 dispatch
392
+ // L2 core + L3 finance(gated) — buildSharedAppTools 단일 dispatch · B5: 진행 한 줄(emitFeedback)·세션·호출 id 를 넘긴다.
393
+ return shared.dispatch(name, args, {
394
+ surface: 'tui',
395
+ ...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),
396
+ ...(ctx.toolCallId ? { toolCallId: ctx.toolCallId } : {}),
397
+ ...(ctx.emitFeedback ? { emitFeedback: ctx.emitFeedback } : {}),
398
+ });
393
399
  }
394
400
  if (name === 'Bash') {
395
401
  // PLAN-ios-rich-dev-feedback-hydrate M3 (2026-05-13) — pass
@@ -0,0 +1,84 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { codexCredentialRoot } from './codex-reset-credit-state.js';
4
+ import type { LlmPolicy } from '../policy/llm-policy.js';
5
+
6
+ interface CreditDay {
7
+ day: string;
8
+ accounts: Record<string, { dayOpen: number; balance: number }>;
9
+ }
10
+
11
+ export type CreditBalances = Readonly<Record<string, number>>;
12
+
13
+ function kstDay(now: Date): string {
14
+ return new Date(now.getTime() + 9 * 3_600_000).toISOString().slice(0, 10);
15
+ }
16
+
17
+ function dayNumber(day: string): number {
18
+ return Date.parse(`${day}T00:00:00Z`) / 86_400_000;
19
+ }
20
+
21
+ function creditDayPath(stateDir?: string): string {
22
+ return join(stateDir ?? codexCredentialRoot(), 'budget', 'codex-credit-day.json');
23
+ }
24
+
25
+ function readCreditDay(stateDir?: string): CreditDay | undefined {
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(creditDayPath(stateDir), 'utf8')) as CreditDay;
28
+ return typeof parsed.day === 'string' && parsed.accounts && typeof parsed.accounts === 'object' && !Array.isArray(parsed.accounts)
29
+ ? parsed : undefined;
30
+ } catch { return undefined; }
31
+ }
32
+
33
+ /** Only a poller's observed balance can move the opening balance; readers never modify the ledger. */
34
+ export function recordCreditBalances(balances: CreditBalances, now: Date = new Date(), stateDir?: string): void {
35
+ const day = kstDay(now);
36
+ const previous = readCreditDay(stateDir);
37
+ const accounts: CreditDay['accounts'] = previous?.day === day ? { ...previous.accounts } : {};
38
+ for (const [name, balance] of Object.entries(balances)) {
39
+ if (!Number.isFinite(balance) || balance < 0) continue;
40
+ const prior = Object.prototype.hasOwnProperty.call(accounts, name) ? accounts[name] : undefined;
41
+ const dayOpen = prior && Number.isFinite(prior.dayOpen) && Number.isFinite(prior.balance)
42
+ ? (balance > prior.balance ? balance : prior.dayOpen)
43
+ : balance;
44
+ Object.defineProperty(accounts, name, { value: { dayOpen, balance }, enumerable: true, configurable: true, writable: true });
45
+ }
46
+ const path = creditDayPath(stateDir);
47
+ mkdirSync(dirname(path), { recursive: true });
48
+ const temporary = `${path}.${process.pid}.tmp`;
49
+ writeFileSync(temporary, `${JSON.stringify({ day, accounts }, null, 2)}\n`);
50
+ renameSync(temporary, path);
51
+ }
52
+
53
+ export function creditPaceStatus({ policy, balances, now = new Date(), stateDir }: {
54
+ policy: LlmPolicy;
55
+ balances: CreditBalances;
56
+ now?: Date;
57
+ stateDir?: string;
58
+ }): { active: boolean; todaySpent: number; target: number; remaining: number; daysLeft: number; projectedAtUntil: number } {
59
+ const day = kstDay(now);
60
+ const ledger = readCreditDay(stateDir);
61
+ const observed = Object.entries(balances).filter(([, balance]) => Number.isFinite(balance) && balance >= 0);
62
+ const todaySpent = ledger?.day === day ? observed.reduce((sum, [name, balance]) => {
63
+ const open = Object.prototype.hasOwnProperty.call(ledger.accounts, name) ? ledger.accounts[name]?.dayOpen : undefined;
64
+ return sum + (typeof open === 'number' && Number.isFinite(open) ? Math.max(0, open - balance) : 0);
65
+ }, 0) : 0;
66
+ const remaining = observed.reduce((sum, [, balance]) => sum + balance, 0);
67
+ const until = policy.credits.pace?.until;
68
+ const expiry = until ? Date.parse(`${until}T15:00:00Z`) : NaN;
69
+ const daysLeft = until ? Math.max(1, dayNumber(until) - dayNumber(day) + 1) : 1;
70
+ // The poller's opening observation anchors today's allocation. Spending cannot lower it;
71
+ // a new grant or the next KST day changes the opening observation.
72
+ const startingBalance = ledger?.day === day
73
+ ? observed.reduce((sum, [name, balance]) => {
74
+ const entry = Object.prototype.hasOwnProperty.call(ledger.accounts, name) ? ledger.accounts[name] : undefined;
75
+ return sum + (entry && Number.isFinite(entry.dayOpen) ? entry.dayOpen : balance);
76
+ }, 0)
77
+ : remaining;
78
+ const target = until ? Math.ceil(startingBalance / daysLeft) : 0;
79
+ return {
80
+ active: policy.credits.codex === 'use' && !!until && now.getTime() < expiry && remaining > 0 && todaySpent < target,
81
+ todaySpent, target, remaining, daysLeft,
82
+ projectedAtUntil: Math.max(0, remaining - Math.max(0, target - todaySpent) - target * (daysLeft - 1)),
83
+ };
84
+ }
@@ -0,0 +1,154 @@
1
+ // Codex prepaid-credit plan: days left until the credits expire, the daily spend that would use the balance up,
2
+ // and what was actually spent over the last 7 days. Disk only — the balance history is appended by
3
+ // writeQuotaSignal whenever a measured balance changes; nothing here touches the network.
4
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const DAY_MS = 86_400_000;
8
+ const HISTORY_KEEP_MS = 30 * DAY_MS;
9
+ const HISTORY_FILE = 'codex-credit-history.jsonl';
10
+
11
+ export interface CreditHistoryEntry { at: string; home: string; balance: number }
12
+
13
+ export function creditHistoryPath(signalDir: string): string {
14
+ return join(signalDir, HISTORY_FILE);
15
+ }
16
+
17
+ export function readCreditHistory(signalDir: string): CreditHistoryEntry[] {
18
+ try {
19
+ const path = creditHistoryPath(signalDir);
20
+ if (!existsSync(path)) return [];
21
+ const out: CreditHistoryEntry[] = [];
22
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
23
+ if (!line.trim()) continue;
24
+ try {
25
+ const e = JSON.parse(line) as Partial<CreditHistoryEntry>;
26
+ if (typeof e.at === 'string' && typeof e.home === 'string' && typeof e.balance === 'number' && Number.isFinite(e.balance)) out.push(e as CreditHistoryEntry);
27
+ } catch { /* a torn line is skipped */ }
28
+ }
29
+ return out;
30
+ } catch { return []; }
31
+ }
32
+
33
+ /** Appends only when this home's balance changed; prunes entries older than 30 days. Fail-soft. */
34
+ export function appendCreditHistory(signalDir: string, home: string, balance: number, now: number = Date.now()): void {
35
+ try {
36
+ if (!Number.isFinite(balance)) return;
37
+ const history = readCreditHistory(signalDir);
38
+ const last = [...history].reverse().find((e) => e.home === home);
39
+ if (last && last.balance === balance) return;
40
+ mkdirSync(signalDir, { recursive: true });
41
+ const entry: CreditHistoryEntry = { at: new Date(now).toISOString(), home, balance };
42
+ const kept = history.filter((e) => now - Date.parse(e.at) <= HISTORY_KEEP_MS);
43
+ if (kept.length !== history.length) {
44
+ writeFileSync(creditHistoryPath(signalDir), [...kept, entry].map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf8');
45
+ } else {
46
+ appendFileSync(creditHistoryPath(signalDir), `${JSON.stringify(entry)}\n`, 'utf8');
47
+ }
48
+ } catch { /* observation never blocks the fetch */ }
49
+ }
50
+
51
+ /** Credits spent in the window: sum of balance drops per home (rises are top-ups and are not spend). */
52
+ export function creditSpend(history: readonly CreditHistoryEntry[], now: number, windowMs = 7 * DAY_MS): { spent: number; spanDays: number } | null {
53
+ const since = now - windowMs;
54
+ let spent = 0;
55
+ let earliest = Infinity;
56
+ const byHome = new Map<string, CreditHistoryEntry[]>();
57
+ for (const e of history) {
58
+ const at = Date.parse(e.at);
59
+ if (!Number.isFinite(at) || at > now) continue;
60
+ byHome.set(e.home, [...(byHome.get(e.home) ?? []), e]);
61
+ }
62
+ for (const entries of byHome.values()) {
63
+ entries.sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
64
+ // The last entry before the window is the baseline for the first change inside it.
65
+ let start = entries.findIndex((e) => Date.parse(e.at) >= since);
66
+ if (start === -1) continue;
67
+ if (start > 0) start -= 1;
68
+ const used = entries.slice(start);
69
+ if (used.length < 2) continue;
70
+ earliest = Math.min(earliest, Math.max(since, Date.parse(used[0]!.at)));
71
+ for (let i = 1; i < used.length; i++) {
72
+ const drop = used[i - 1]!.balance - used[i]!.balance;
73
+ if (drop > 0) spent += drop;
74
+ }
75
+ }
76
+ if (!Number.isFinite(earliest)) return null;
77
+ return { spent, spanDays: Math.max((now - earliest) / DAY_MS, 1 / 24) };
78
+ }
79
+
80
+ export interface CodexCreditPlan {
81
+ /** policy credits.pace.until, else the earliest grant expiry; null when the policy records neither. */
82
+ expiresAt: string | null;
83
+ daysLeft: number | null;
84
+ /** Sum of the known per-account balances; null when no balance is known. */
85
+ totalBalance: number | null;
86
+ unknownBalances: number;
87
+ accounts: Array<{ name: string; balance: number | null; expires: string | null }>;
88
+ /** Credits per day that would use the balance up by the expiry date. */
89
+ dailyNeeded: number | null;
90
+ /** policy credits.pace.targetPerDay — the declared pace, shown beside the computed one. */
91
+ targetPerDay: number | null;
92
+ /** Credits actually spent per day over the last 7 days (or the shorter recorded span). */
93
+ actualPerDay: number | null;
94
+ actualSpanDays: number | null;
95
+ /** policy credits.pace.why */
96
+ note: string | null;
97
+ }
98
+
99
+ export interface CreditPolicyView {
100
+ codex: string;
101
+ grants: ReadonlyArray<{ account: string; amount: number; expires: string }>;
102
+ pace?: { targetPerDay: number; until: string; why?: string };
103
+ }
104
+
105
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
106
+
107
+ export function codexCreditPlan(input: {
108
+ now: number;
109
+ balances: ReadonlyArray<{ name: string; balance?: number }>;
110
+ credits: CreditPolicyView;
111
+ history: readonly CreditHistoryEntry[];
112
+ }): CodexCreditPlan {
113
+ const { credits } = input;
114
+ const accounts = input.balances.map((a) => {
115
+ const grantDates = credits.grants.filter((g) => g.account === a.name && DATE.test(g.expires)).map((g) => g.expires).sort();
116
+ return { name: a.name, balance: typeof a.balance === 'number' && Number.isFinite(a.balance) ? a.balance : null, expires: grantDates[0] ?? null };
117
+ });
118
+ const known = accounts.map((a) => a.balance).filter((b): b is number => b !== null);
119
+ const totalBalance = known.length ? known.reduce((x, y) => x + y, 0) : null;
120
+ const earliestGrant = credits.grants.map((g) => g.expires).filter((d) => DATE.test(d)).sort()[0];
121
+ const expiresAt = credits.pace?.until && DATE.test(credits.pace.until) ? credits.pace.until : earliestGrant ?? null;
122
+ const expiry = expiresAt ? Date.parse(`${expiresAt}T23:59:59+09:00`) : NaN;
123
+ const daysLeft = Number.isFinite(expiry) ? Math.max(0, Math.ceil((expiry - input.now) / DAY_MS)) : null;
124
+ const spend = creditSpend(input.history, input.now);
125
+ return {
126
+ expiresAt,
127
+ daysLeft,
128
+ totalBalance,
129
+ unknownBalances: accounts.length - known.length,
130
+ accounts,
131
+ dailyNeeded: daysLeft && totalBalance !== null ? totalBalance / daysLeft : null,
132
+ targetPerDay: credits.pace?.targetPerDay ?? null,
133
+ actualPerDay: spend ? spend.spent / Math.min(7, spend.spanDays) : null,
134
+ actualSpanDays: spend ? Math.round(Math.min(7, spend.spanDays) * 10) / 10 : null,
135
+ note: credits.pace?.why?.trim() || null,
136
+ };
137
+ }
138
+
139
+ /** The lines both surfaces print under the policy line. */
140
+ export function formatCodexCreditPlan(plan: CodexCreditPlan): string[] {
141
+ const n = (v: number) => String(Math.round(v));
142
+ const unknown = plan.unknownBalances ? ` (+${plan.unknownBalances}개 계정 모름)` : '';
143
+ const perAccount = plan.accounts.map((a) => `${a.name} ${a.balance === null ? '?' : n(a.balance)}${a.expires ? `(~${a.expires})` : ''}`).join(' · ');
144
+ const lines = [
145
+ `크레딧 잔액 합 ${plan.totalBalance === null ? '?' : n(plan.totalBalance)}${unknown} · 만료 ${plan.expiresAt ?? '모름 (policy credits.pace.until · credits.grants)'}${perAccount ? ` · ${perAccount}` : ''}`,
146
+ ];
147
+ const days = plan.daysLeft === null ? '만료일 모름' : `만료까지 ${plan.daysLeft}일`;
148
+ const needed = plan.dailyNeeded === null ? '?' : n(plan.dailyNeeded);
149
+ const target = plan.targetPerDay === null ? '' : ` (목표 ${n(plan.targetPerDay)})`;
150
+ const actual = plan.actualPerDay === null ? '기록 없음' : `${n(plan.actualPerDay)}/일${plan.actualSpanDays !== null && plan.actualSpanDays < 7 ? ` (기록 ${plan.actualSpanDays}일)` : ''}`;
151
+ lines.push(`계산 ${days} · 하루 소진 필요 ${needed}${target} · 최근 7일 실제 ${actual}`);
152
+ if (plan.note) lines.push(`메모 ${plan.note}`);
153
+ return lines;
154
+ }
@@ -9,6 +9,7 @@
9
9
  // ⇒ 직접 부르는 쪽은 «자기가» 감싸야 한다. 여기서 삼키면 저장 실패가 영영 안 보인다.
10
10
  // ⭐ 반면 `writeQuotaSignal` 은 «판정 신호»라 조회를 막으면 안 되므로 자기가 삼킨다(아래 주석).
11
11
 
12
+ import { appendCreditHistory } from './codex-credit-plan.js';
12
13
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
13
14
  import { createHash } from 'node:crypto';
14
15
  import { homedir } from 'node:os';
@@ -233,6 +234,7 @@ export function writeQuotaSignal(
233
234
  ...(typeof credits?.hasCredits === 'boolean' ? { hasCredits: credits.hasCredits } : {}),
234
235
  };
235
236
  writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
237
+ if (next.creditsBalance !== undefined && next.measuredHome) appendCreditHistory(dirname(path), next.measuredHome, next.creditsBalance);
236
238
  } catch { /* fail-soft */ }
237
239
  }
238
240
 
@@ -49,5 +49,8 @@ export function toDevChatOpts(opts: ChatCliOpts): DevChatOpts {
49
49
  export async function runChatCliCommand(text: string, opts: ChatCliOpts, deps: ChatCliDeps): Promise<void> {
50
50
  const spec = buildChatDevSpec(text, toDevChatOpts(opts));
51
51
  const run = deps.runDevPipeline ?? runDevPipeline;
52
- await run(spec, { runChatTurn: deps.runChatTurn });
52
+ await run(spec, {
53
+ runChatTurn: deps.runChatTurn,
54
+ ...(opts.json ? { progress: (message: string) => { process.stderr.write(`${message}\n`); } } : {}),
55
+ });
53
56
  }
@@ -0,0 +1,40 @@
1
+ // B1(UX · 0.2.7) — 짧은 «물음»은 도구 없이 빠른 경로로(첫 글자 ≤2초 · 내부 문서 `EVAL-bot-chat-vs-grok-2026-09-30`).
2
+ // 허용 방식(골 ASK-bot-short-question-fast-path-b1b): 아래가 «모두» 참일 때만 fast — 하나라도 거짓이면 fast 아님(보수 쪽).
3
+ // ⓐ 한 줄 · 80자 이하 · 첨부 없음 ⓑ 인사이거나 «물음 꼴» ⓒ 요청·작업 표지 · 경로·URL·코드 꼴 · 실시간 낱말이 없다.
4
+ // 거부 목록으로 변형을 다 막을 수는 없다 — 오분류 비용은 «확인해 볼까요?» 한 줄로 막힌다(도구가 안 실릴 뿐 대화는 이어진다).
5
+ // 판정 표본 = src/chat/short-question.test.ts 의 고정 표 20개(⊕ 리뷰가 든 변형).
6
+
7
+ export interface ShortQuestionContext {
8
+ hasAttachments?: boolean;
9
+ }
10
+ export interface ShortQuestionVerdict {
11
+ fast: boolean;
12
+ reason: string;
13
+ }
14
+
15
+ const GREETING = /^(?:안녕(?:하세요|하십니까)?|반가워(?:요)?|고마워(?:요)?|감사합니다|하이|hi|hello|hey|thanks|thank you)[!?.~\s]*$/i;
16
+ // «한 줄로» 같은 짧은 꼬리를 떼고 본다.
17
+ const TAIL = /\s*(?:한\s*줄로|짧게|간단히|briefly|in one line)[.!]?\s*$/i;
18
+ const KO_QUESTION_END = /(?:\?|까|니|나요|냐|야|어때|뭐|몇|언제|어디|누구|왜|인가|일까)[?.!~]*$/;
19
+ const EN_QUESTION = /^(?:what|why|how|who|where|when|which|is|are|does|do)\b.*\?$/i;
20
+ const REQUEST_KO = /(?:해\s*줘|해\s*주세요|줘(?:요)?(?=[?.!\s]|$)|주세요|줄래|할래|볼래|줄\s*수\s*있|돌려|실행|고쳐|만들어\s*줘|찾아|검색|확인해|열어|보내|올려|지워|써\s*줘|작성해|요약해|정리해|분석해)/;
21
+ const REQUEST_EN = /\b(?:please|can you|could you|would you|run|test|tests|fix|build|open|search|check|send|create|write|deploy|install|summarize)\b/i;
22
+ const TASK = /(?:작업|파일|폴더|디렉터리|레포|저장소|커밋|브랜치|\bPR\b|코드|스크립트|테스트|빌드|배포|설치|설정|명령어|로그|버그|이슈)/i;
23
+ const PATH_OR_CODE = /(?:https?:\/\/|www\.|[\w.-]+\/[\w./-]+|\.(?:ts|tsx|js|py|md|json|yaml|swift|kt)\b|[`{};=<>])/i;
24
+ const LIVE = /(?:오늘\s*(?:날짜|며칠|무슨\s*요일)|지금\s*몇\s*시|최신|뉴스|속보|주가|시세|환율|날씨|today'?s|latest|news|stock|weather|right now)/i;
25
+
26
+ export function classifyShortQuestion(text: string, ctx: ShortQuestionContext = {}): ShortQuestionVerdict {
27
+ const q = text.trim();
28
+ if (!q) return { fast: false, reason: 'empty' };
29
+ if (ctx.hasAttachments) return { fast: false, reason: 'attachment' };
30
+ if (/[\r\n\u2028\u2029]/.test(q)) return { fast: false, reason: 'multiline' };
31
+ if ([...q].length > 80) return { fast: false, reason: 'long' };
32
+ if (PATH_OR_CODE.test(q)) return { fast: false, reason: 'path-or-code' };
33
+ if (LIVE.test(q)) return { fast: false, reason: 'live' };
34
+ if (REQUEST_KO.test(q) || REQUEST_EN.test(q)) return { fast: false, reason: 'request' };
35
+ if (TASK.test(q)) return { fast: false, reason: 'task' };
36
+ if (GREETING.test(q)) return { fast: true, reason: 'greeting' };
37
+ const core = q.replace(TAIL, '').trim();
38
+ if (KO_QUESTION_END.test(core) || EN_QUESTION.test(core)) return { fast: true, reason: 'question' };
39
+ return { fast: false, reason: 'not-a-question' };
40
+ }
@@ -393,8 +393,8 @@ tierCmd
393
393
  * When `json` is true, the function suppresses the streaming text +
394
394
  * ui.info trailer and instead emits exactly ONE JSON line on stdout
395
395
  * at end-of-turn:
396
- * {sessionId, provider, model, reply, logPath, budget, durationMs,
397
- * turnIndex, ts}
396
+ * {sessionId, provider, model, reply, finalReply, transcript, logPath,
397
+ * budget, durationMs, ts}
398
398
  * Stable shape so LLMs can self-spawn `elanous chat` for follow-ups
399
399
  * without parsing human-readable terminal output. */
400
400
  export async function runChatTurnCli(opts: {
@@ -442,12 +442,14 @@ export async function runChatTurnCli(opts: {
442
442
  dbg.setAmbientSessionId?.(session.id);
443
443
  } catch { /* debug module unavailable — fine */ }
444
444
  const replyChunks: string[] = [];
445
- // 어시스턴트 메시지 경계(도구 호출)로 자른 본문 조각 — 마지막 «비어 있지 않은» 조각이 finalReply.
445
+ // Tool calls delimit assistant messages. finalReply retains its last non-empty-message meaning.
446
446
  let segmentChunks: string[] = [];
447
447
  let lastSegment = '';
448
+ let finalMessage = '';
449
+ let sawToolCall = false;
448
450
  const closeSegment = (): void => {
449
- const text = segmentChunks.join('').trim();
450
- if (text) lastSegment = text;
451
+ finalMessage = segmentChunks.join('').trim();
452
+ if (finalMessage) lastSegment = finalMessage;
451
453
  segmentChunks = [];
452
454
  };
453
455
  if (!opts.json) {
@@ -528,15 +530,17 @@ export async function runChatTurnCli(opts: {
528
530
  userText: opts.userText,
529
531
  systemPrompt: agentSystemPrompt,
530
532
  onDelta: (d) => {
531
- if (opts.json) { replyChunks.push(d); segmentChunks.push(d); }
532
- else process.stdout.write(d);
533
+ replyChunks.push(d);
534
+ segmentChunks.push(d);
535
+ if (!opts.json && !opts.enableTools) process.stdout.write(d);
533
536
  },
534
537
  tools: cliToolSpecs,
535
538
  dispatchTool,
536
539
  ...(opts.goalLoop ? { goalLoop: true } : {}),
537
540
  onToolCall: (call) => {
538
- // 도구 호출 뒤의 본문은 «새» 어시스턴트 메시지다 — 직전 조각을 마감한다(finalReply 용).
539
- if (opts.json) closeSegment();
541
+ // The next delta belongs to the next assistant message.
542
+ sawToolCall = true;
543
+ closeSegment();
540
544
  if (!opts.json) {
541
545
  process.stdout.write(`\n ⏺ ${call.name}(${truncateArgsForLog(call.args)})\n`);
542
546
  }
@@ -555,6 +559,12 @@ export async function runChatTurnCli(opts: {
555
559
  }
556
560
  throw err;
557
561
  }
562
+ if (!sawToolCall && replyChunks.length === 0 && result.text) {
563
+ replyChunks.push(result.text);
564
+ segmentChunks.push(result.text);
565
+ if (!opts.json && !opts.enableTools) process.stdout.write(result.text);
566
+ }
567
+ closeSegment();
558
568
  if (opts.json) {
559
569
  let logPath: string | null = null;
560
570
  try {
@@ -566,11 +576,9 @@ export async function runChatTurnCli(opts: {
566
576
  sessionId: session.id,
567
577
  provider: result.provider,
568
578
  model: result.model ?? null,
569
- reply: replyChunks.join(''),
570
- // ⭐ 2026-09-23 — `reply` 는 «모든 턴·모든 목표 루프 반복»의 본문을 이어 붙인다(뜻은 그대로 둔다 · 소비자 보호).
571
- // 그래서 모델이 반복마다 최종 답을 다시 말하면 같은 문장이 여러 번 나온다(실측: kimi 3회·grok 2회).
572
- // `finalReply` = 마지막 어시스턴트 메시지 본문 — 「최종 답만」이 필요한 소비자용(뒤호환 추가 칸).
573
- finalReply: (closeSegment(), lastSegment),
579
+ reply: finalMessage,
580
+ transcript: replyChunks.join(''),
581
+ finalReply: lastSegment,
574
582
  budget: sessionBudget(session.id),
575
583
  durationMs: Date.now() - startedAt,
576
584
  logPath,
@@ -579,7 +587,8 @@ export async function runChatTurnCli(opts: {
579
587
  await writeStdoutJson(JSON.stringify(out) + '\n');
580
588
  return;
581
589
  }
582
- process.stdout.write('\n');
590
+ if (opts.enableTools) process.stdout.write(`${finalMessage}\n`);
591
+ else process.stdout.write('\n');
583
592
  ui.info(`[session ${session.id.slice(0, 8)} ${result.provider}${result.model ? '/' + result.model : ''} ${sessionBudget(session.id)}]`);
584
593
  }
585
594
 
@@ -0,0 +1,42 @@
1
+ import { decideProviderForConfig, getProviderForConfig, streamLLM, type LLMMessage } from '../llm.js';
2
+ import { getUserConfig } from '../user-config.js';
3
+ import { DEFAULT_TOKEN_BUDGET, trimToBudget } from '../tokens.js';
4
+
5
+ export interface AskBareDeps {
6
+ getUserConfig: typeof getUserConfig;
7
+ decideProviderForConfig: typeof decideProviderForConfig;
8
+ getProviderForConfig: typeof getProviderForConfig;
9
+ streamLLM: typeof streamLLM;
10
+ trimToBudget: typeof trimToBudget;
11
+ }
12
+
13
+ export async function askBare({ text, provider, model, deps }: {
14
+ text: string;
15
+ provider?: string;
16
+ model?: string;
17
+ deps: AskBareDeps;
18
+ }): Promise<{ provider: string; model: string; reply: string }> {
19
+ const messages: LLMMessage[] = [{ role: 'user', content: text }];
20
+ if (deps.trimToBudget(messages, DEFAULT_TOKEN_BUDGET).used > DEFAULT_TOKEN_BUDGET) {
21
+ throw new Error(`ask --bare: input exceeds the ${DEFAULT_TOKEN_BUDGET}-token context budget`);
22
+ }
23
+ const cfg = deps.getUserConfig();
24
+ const selectedConfig = provider === undefined ? cfg : {
25
+ ...cfg,
26
+ llm: {
27
+ ...cfg.llm,
28
+ provider: provider as typeof cfg.llm.provider,
29
+ model: model ?? (provider === cfg.llm.provider ? cfg.llm.model : undefined),
30
+ },
31
+ };
32
+ const selected = deps.decideProviderForConfig(selectedConfig, model);
33
+ const chosenProvider = deps.getProviderForConfig(selectedConfig, model);
34
+ let resolvedProvider = chosenProvider.name;
35
+ let resolvedModel = selected.model;
36
+ const reply = await deps.streamLLM(messages, () => {}, {
37
+ ...(provider === undefined ? { initialProvider: chosenProvider } : { provider: chosenProvider }),
38
+ ...(selected.model === '(none)' ? {} : { model: selected.model }),
39
+ onResolvedProvider: (name, activeModel) => { resolvedProvider = name; resolvedModel = activeModel; },
40
+ });
41
+ return { provider: resolvedProvider, model: resolvedModel, reply };
42
+ }