elanous 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (479) hide show
  1. package/.env.example +10 -7
  2. package/README.md +9 -4
  3. package/apps/pwa/out/404/index.html +1 -1
  4. package/apps/pwa/out/404.html +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 2375-cb6debea56cce4fb.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/{1751.50b96ea9c37b49b6.js → 2415.ed6428ff334c7ce2.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/{4daa2798-1363d725c79f3593.js → 2ea96958-e8c7e112de50b067.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  18. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.js → 4323.c5dfb6f503304775.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.js → 5080-1613752891fcadb4.js} +3 -3
  24. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{5348-bbf838e119661eee.js → 5791-bb82743fa6299562.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 6062-73e68343a9814f6a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/{4440-18bb160d95819f4e.js → 6090-85a3f05f954e5697.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/{f6f3850d-7f324c6566002113.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-f89e9a303515a033.js → page-10ed24f413ed66e9.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/board/{page-4f403cd060442c95.js → page-211810b6c93ab66a.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-0ac5d053f0444d21.js → page-624f7a2f89b60c8f.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/morning/{page-bccce0babceae30f.js → page-55d50e9b0006a91d.js} +1 -1
  68. package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/{webpack-2bf59c720fe9542c.js → webpack-e5bea36f183b25e6.js} +1 -1
  95. package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
  96. package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
  97. package/apps/pwa/out/approvals/index.html +1 -1
  98. package/apps/pwa/out/approvals/index.txt +22 -22
  99. package/apps/pwa/out/autopilot/index.html +1 -1
  100. package/apps/pwa/out/autopilot/index.txt +22 -22
  101. package/apps/pwa/out/board/index.html +1 -1
  102. package/apps/pwa/out/board/index.txt +22 -22
  103. package/apps/pwa/out/botlab/index.html +1 -1
  104. package/apps/pwa/out/botlab/index.txt +21 -21
  105. package/apps/pwa/out/bots/index.html +1 -1
  106. package/apps/pwa/out/bots/index.txt +21 -21
  107. package/apps/pwa/out/chat/index.html +1 -1
  108. package/apps/pwa/out/chat/index.txt +21 -21
  109. package/apps/pwa/out/consult/index.html +1 -0
  110. package/apps/pwa/out/consult/index.txt +32 -0
  111. package/apps/pwa/out/control/index.html +1 -1
  112. package/apps/pwa/out/control/index.txt +22 -22
  113. package/apps/pwa/out/design-check/index.html +1 -1
  114. package/apps/pwa/out/design-check/index.txt +22 -22
  115. package/apps/pwa/out/editor/index.html +1 -1
  116. package/apps/pwa/out/editor/index.txt +22 -22
  117. package/apps/pwa/out/exec/index.html +1 -0
  118. package/apps/pwa/out/exec/index.txt +32 -0
  119. package/apps/pwa/out/field/index.html +1 -0
  120. package/apps/pwa/out/field/index.txt +32 -0
  121. package/apps/pwa/out/healthz +1 -1
  122. package/apps/pwa/out/icons/elanous-16.png +0 -0
  123. package/apps/pwa/out/icons/elanous-180.png +0 -0
  124. package/apps/pwa/out/icons/elanous-192.png +0 -0
  125. package/apps/pwa/out/icons/elanous-32.png +0 -0
  126. package/apps/pwa/out/icons/elanous-512.png +0 -0
  127. package/apps/pwa/out/index.html +1 -1
  128. package/apps/pwa/out/index.txt +21 -21
  129. package/apps/pwa/out/intake/index.html +1 -1
  130. package/apps/pwa/out/intake/index.txt +22 -22
  131. package/apps/pwa/out/live/index.html +1 -1
  132. package/apps/pwa/out/live/index.txt +22 -22
  133. package/apps/pwa/out/market/index.html +1 -1
  134. package/apps/pwa/out/market/index.txt +21 -21
  135. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  136. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  137. package/apps/pwa/out/morning/index.html +1 -1
  138. package/apps/pwa/out/morning/index.txt +22 -22
  139. package/apps/pwa/out/observatory/index.html +1 -1
  140. package/apps/pwa/out/observatory/index.txt +22 -22
  141. package/apps/pwa/out/reflection/index.html +1 -1
  142. package/apps/pwa/out/reflection/index.txt +22 -22
  143. package/apps/pwa/out/scheduler/index.html +1 -1
  144. package/apps/pwa/out/scheduler/index.txt +22 -22
  145. package/apps/pwa/out/sessions/index.html +1 -1
  146. package/apps/pwa/out/sessions/index.txt +21 -21
  147. package/apps/pwa/out/settings/devices/index.html +1 -1
  148. package/apps/pwa/out/settings/devices/index.txt +22 -22
  149. package/apps/pwa/out/settings/index.html +1 -1
  150. package/apps/pwa/out/settings/index.txt +22 -22
  151. package/apps/pwa/out/setup/done/index.html +1 -1
  152. package/apps/pwa/out/setup/done/index.txt +22 -22
  153. package/apps/pwa/out/setup/index.html +1 -1
  154. package/apps/pwa/out/setup/index.txt +22 -22
  155. package/apps/pwa/out/share/index.html +1 -1
  156. package/apps/pwa/out/share/index.txt +22 -22
  157. package/apps/pwa/out/showroom/index.html +1 -1
  158. package/apps/pwa/out/showroom/index.txt +21 -21
  159. package/apps/pwa/out/tasks/index.html +1 -1
  160. package/apps/pwa/out/tasks/index.txt +22 -22
  161. package/apps/pwa/out/term/index.html +1 -1
  162. package/apps/pwa/out/term/index.txt +22 -22
  163. package/apps/pwa/out/trace/index.html +1 -1
  164. package/apps/pwa/out/trace/index.txt +22 -22
  165. package/apps/pwa/out/vault/index.html +1 -1
  166. package/apps/pwa/out/vault/index.txt +22 -22
  167. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  168. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  169. package/apps/pwa/out/workflows/index.html +1 -1
  170. package/apps/pwa/out/workflows/index.txt +22 -22
  171. package/apps/pwa/out/workspace/index.html +1 -1
  172. package/apps/pwa/out/workspace/index.txt +22 -22
  173. package/apps/pwa/out/worktrees/index.html +1 -1
  174. package/apps/pwa/out/worktrees/index.txt +22 -22
  175. package/bin/elanous.cjs +107 -0
  176. package/graphs/field/field-feed.yaml +24 -0
  177. package/graphs/field/recipes.yaml +12 -0
  178. package/graphs/landing/landing-heal.yaml +3 -1
  179. package/graphs/landing/recipes.yaml +3 -0
  180. package/graphs/release/recipes.yaml +13 -0
  181. package/graphs/release/release-loop.yaml +13 -4
  182. package/package.json +5 -5
  183. package/plugins/card-followup/README.md +42 -0
  184. package/plugins/card-followup/examples/card-sample.json +7 -0
  185. package/plugins/card-followup/graphs/card-followup.yaml +18 -0
  186. package/plugins/card-followup/graphs/recipes.yaml +16 -0
  187. package/plugins/card-followup/graphs/run-step.ts +276 -0
  188. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  189. package/plugins/card-followup/plugin.json +25 -0
  190. package/plugins/doc-draft/README.md +13 -0
  191. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  192. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  193. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  194. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  195. package/plugins/doc-draft/plugin.json +24 -0
  196. package/plugins/geo-check/README.md +15 -0
  197. package/plugins/geo-check/examples/brand-sample.json +8 -0
  198. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  199. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  200. package/plugins/geo-check/graphs/run-step.ts +198 -0
  201. package/plugins/geo-check/plugin.json +24 -0
  202. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  203. package/plugins/job-coach/README.md +31 -1
  204. package/plugins/job-coach/graphs/run-step.ts +2 -2
  205. package/plugins/job-coach/plugin.json +1 -1
  206. package/plugins/job-coach/plugin.ts +1 -1
  207. package/scripts/affinity/endcard.sh +3 -2
  208. package/scripts/affinity/reframe.sh +3 -1
  209. package/scripts/ci-public-export-leak-gate.ts +48 -0
  210. package/scripts/codex-quota-alert.ts +122 -258
  211. package/scripts/coord-tracks.json +4 -4
  212. package/scripts/daemon-port-baseline.txt +1 -2
  213. package/scripts/field-feed/deliver-node.ts +52 -0
  214. package/scripts/field-feed/feed-node.ts +67 -0
  215. package/scripts/field-feed/lib.ts +162 -0
  216. package/scripts/field-feed/reel-node.ts +24 -0
  217. package/scripts/field-feed/start.ts +62 -0
  218. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  219. package/scripts/install.sh +92 -13
  220. package/scripts/landing-heal/clean-checkout.ts +114 -0
  221. package/scripts/landing-heal/run-mission.ts +38 -3
  222. package/scripts/lib/android-test-device.ts +32 -0
  223. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  224. package/scripts/public-export.ts +8 -4
  225. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  226. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  227. package/scripts/release-loop/env-known-failures.ts +31 -0
  228. package/scripts/release-loop/gate-diff.ts +29 -0
  229. package/scripts/release-loop/gate-node.ts +124 -29
  230. package/scripts/release-loop/known-issues-node.ts +159 -0
  231. package/scripts/release-loop/npm-publish-node.ts +118 -0
  232. package/scripts/release-loop/ops-upgrade-node.ts +87 -0
  233. package/scripts/release-loop/release-readiness.ts +58 -0
  234. package/scripts/release-loop/shard-plan.ts +47 -0
  235. package/scripts/release-loop/unattended-release.ts +93 -0
  236. package/scripts/ux/measure-first-text.ts +102 -0
  237. package/scripts/ux/record-page.ts +4 -1
  238. package/scripts/vflow-collect.ts +20 -7
  239. package/scripts/vflow-search.ts +73 -0
  240. package/scripts/video-free-line-test-walker.ts +36 -0
  241. package/scripts/video-pipeline.ts +2 -2
  242. package/skills/explainer-video/SKILL.md +24 -3
  243. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
  244. package/skills/explainer-video/engine/build.mjs +2 -2
  245. package/skills/explainer-video/engine/reel.mjs +235 -0
  246. package/skills/explainer-video/engine/reel.sh +9 -0
  247. package/skills/explainer-video/engine/vo.ts +58 -16
  248. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  249. package/skills/grill-me/SKILL.md +1 -1
  250. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  251. package/src/a2a/server.ts +2 -0
  252. package/src/acp/elanous-aware-peers.ts +30 -0
  253. package/src/acp/server.ts +91 -15
  254. package/src/agent/monad-agent-turn.ts +25 -6
  255. package/src/agent/shared-app-tools.ts +5 -3
  256. package/src/agent-substrate/review-intent.ts +6 -2
  257. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  258. package/src/auth/github-app-token.ts +4 -1
  259. package/src/autopilot/mission-origin.ts +20 -4
  260. package/src/autopilot/mission-phase-track.ts +11 -1
  261. package/src/autopilot/track-agent-shadow.ts +151 -0
  262. package/src/autopilot/track-agent.ts +26 -6
  263. package/src/autopilot/track-courier.ts +2 -2
  264. package/src/boot/acp-server-help.ts +16 -0
  265. package/src/boot/daemon-prompt-turn.ts +31 -10
  266. package/src/boot/daemon-tools/index.ts +7 -1
  267. package/src/budget/codex-credit-pace.ts +84 -0
  268. package/src/budget/codex-credit-plan.ts +154 -0
  269. package/src/budget/codex-reset-credit-state.ts +2 -0
  270. package/src/chat/chat-cli.ts +4 -1
  271. package/src/chat/short-question.ts +40 -0
  272. package/src/cli/agent-cli.ts +24 -15
  273. package/src/cli/ask-bare.ts +42 -0
  274. package/src/cli/cli-user-error.ts +25 -0
  275. package/src/cli/codex-credit-plan-view.ts +25 -0
  276. package/src/cli/dependence-gauge.ts +237 -0
  277. package/src/cli/doctor-cli.ts +5 -0
  278. package/src/cli/doctor-modes.ts +48 -0
  279. package/src/cli/intake-cli.ts +12 -2
  280. package/src/cli/intake-source-cli.ts +58 -0
  281. package/src/cli/machine-cli.ts +23 -0
  282. package/src/cli/msg-cli.ts +137 -0
  283. package/src/cli/nexus-ios-bind.ts +25 -8
  284. package/src/cli/phone-link.ts +117 -0
  285. package/src/cli/plugin-cli.ts +52 -0
  286. package/src/cli/pr-cli.ts +36 -0
  287. package/src/cli/private-ledger-commands.ts +31 -0
  288. package/src/cli/provider-cli.ts +5 -0
  289. package/src/cli/release-cli.ts +56 -12
  290. package/src/cli/release-run-if-ready.ts +48 -0
  291. package/src/cli/setup-cli.ts +27 -6
  292. package/src/cli/setup-recommend.ts +8 -0
  293. package/src/cli/usage-cli.ts +11 -1
  294. package/src/codex/setup.ts +27 -8
  295. package/src/dashboard/index.ts +1 -1
  296. package/src/discord.ts +56 -6
  297. package/src/domains/core-tools.ts +37 -14
  298. package/src/domains/outbound-alert.ts +55 -16
  299. package/src/exec-requests/planner.ts +140 -0
  300. package/src/exec-requests/runner.ts +338 -0
  301. package/src/exec-requests/store.ts +76 -0
  302. package/src/field/field-media.ts +250 -0
  303. package/src/field/field-reel.ts +196 -0
  304. package/src/grok/credential.ts +7 -5
  305. package/src/harness/aside-run-status.ts +33 -0
  306. package/src/harness/browser-verify.ts +48 -17
  307. package/src/harness/harness-cli-command.ts +25 -3
  308. package/src/harness/image-load-state.ts +21 -0
  309. package/src/harness/process-garbage.ts +45 -0
  310. package/src/index.ts +104 -38
  311. package/src/instance/leader.ts +13 -1
  312. package/src/intake-plane/collect-github.ts +5 -3
  313. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  314. package/src/intake-plane/discord-seat-work.ts +56 -0
  315. package/src/intake-plane/intake-sources.ts +202 -0
  316. package/src/intake-plane/items.ts +28 -12
  317. package/src/intake-plane/seat-doc-route.ts +207 -0
  318. package/src/intake-plane/submit-intake-work.ts +9 -1
  319. package/src/intake-plane/telegram-seat-work.ts +72 -0
  320. package/src/llm/model-defaults.ts +24 -1
  321. package/src/llm.ts +5 -4
  322. package/src/machines/machine-ledger.ts +127 -0
  323. package/src/market/pack-internal-refs.ts +28 -0
  324. package/src/market/publish.ts +31 -4
  325. package/src/maturity/feature-maturity.ts +65 -0
  326. package/src/model-tier/turn-tier.ts +24 -0
  327. package/src/msg/msg-store.ts +149 -0
  328. package/src/native-tool-catalog.ts +2 -1
  329. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  330. package/src/nexus/api/consult-requests.ts +117 -0
  331. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  332. package/src/nexus/api/exec-requests.ts +51 -0
  333. package/src/nexus/api/field-uploads.ts +180 -0
  334. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  335. package/src/nexus/api/graph-approvals.ts +120 -4
  336. package/src/nexus/api/harness-api.ts +219 -5
  337. package/src/nexus/api/http-server.ts +68 -5
  338. package/src/nexus/api/seat-requests.ts +254 -0
  339. package/src/nexus/api/trace-run-scan.ts +37 -0
  340. package/src/nexus/api/trace.ts +19 -3
  341. package/src/nexus/index.ts +3 -0
  342. package/src/nexus/setup-status.ts +4 -5
  343. package/src/notify/llm-budget-alert.ts +125 -0
  344. package/src/oauth/codex-account-rotation.ts +26 -4
  345. package/src/oauth/codex-account-store.ts +20 -2
  346. package/src/oauth/codex-browser-login.ts +173 -0
  347. package/src/oauth/codex.ts +4 -1
  348. package/src/oauth/fallback-chain.ts +5 -5
  349. package/src/oauth/store.ts +42 -5
  350. package/src/onboarding/continue-here.ts +29 -0
  351. package/src/onboarding/entry-hints.ts +8 -0
  352. package/src/onboarding/full-screen-io.ts +35 -3
  353. package/src/onboarding/help/skills.md +1 -1
  354. package/src/onboarding/non-interactive.ts +1 -0
  355. package/src/onboarding/subscription-hint.ts +27 -0
  356. package/src/onboarding/web-first.ts +83 -0
  357. package/src/onboarding.ts +230 -23
  358. package/src/plugins/maker/node-install.ts +92 -0
  359. package/src/plugins/maker/node-maker.ts +139 -0
  360. package/src/plugins/maker/plugin-maker.ts +220 -0
  361. package/src/policy/llm-policy.ts +268 -0
  362. package/src/prompt-library/session-guidance.ts +7 -1
  363. package/src/prompt-library/universal-preamble.ts +2 -2
  364. package/src/relay/channel.ts +184 -0
  365. package/src/relay/mux.ts +182 -0
  366. package/src/relay/relay-cli.ts +20 -0
  367. package/src/relay/relay-server.ts +220 -0
  368. package/src/relay/test-vectors.json +29 -0
  369. package/src/release-loop/checklist.ts +36 -9
  370. package/src/seat-address/seat-address.ts +34 -0
  371. package/src/self-dev/decompose.ts +1 -0
  372. package/src/self-dev/dev-pipeline.ts +6 -3
  373. package/src/self-dev/run-record-mirror.ts +44 -0
  374. package/src/self-dev/run-store.ts +12 -3
  375. package/src/self-implement/harness-policy-gates.ts +4 -1
  376. package/src/self-implement/report-origin.ts +12 -1
  377. package/src/self-implement/run-ledger-compact.ts +1 -1
  378. package/src/session/chat.ts +17 -5
  379. package/src/session/index.ts +23 -10
  380. package/src/skills/shared-agent-skill-roots.ts +25 -0
  381. package/src/steward/failure-streak.ts +78 -0
  382. package/src/steward/steward-cards.ts +24 -0
  383. package/src/steward/triage-budget.ts +127 -0
  384. package/src/steward/triage-plan.ts +75 -0
  385. package/src/steward/triage.ts +244 -42
  386. package/src/task-cards/card-store.ts +1 -1
  387. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  388. package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
  389. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  390. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  391. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  392. package/src/telegram.ts +145 -2
  393. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  394. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  395. package/src/user-config.ts +84 -4
  396. package/src/version/packed-revision.json +1 -1
  397. package/src/video-pipeline/recipes/character.ts +42 -5
  398. package/src/video-pipeline/recipes/explainer.ts +213 -0
  399. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  400. package/src/video-pipeline/recipes/free-line.ts +16 -3
  401. package/src/video-pipeline/recipes/remote.ts +7 -1
  402. package/src/video-pipeline/vflow-reference.ts +109 -0
  403. package/src/video-pipeline/walk-line.ts +3 -2
  404. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
  411. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
  419. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
  477. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  478. package/apps/pwa/out/_next/static/ktMqfrBbf8BMGac0jqLrb/_buildManifest.js +0 -1
  479. /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
@@ -0,0 +1,254 @@
1
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { dirname, join } from 'node:path';
4
+ import { debug } from '../../debug/log.js';
5
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
6
+ import { submitIntakeWork } from '../../intake-plane/submit-intake-work.js';
7
+ import { parseSeatAddress, resolveSeat } from '../../seat-address/seat-address.js';
8
+ import trackData from '../../../scripts/coord-tracks.json';
9
+ import { jsonResponse } from './json-response.js';
10
+
11
+ export const SEAT_REQUESTS_PATH = '/v1/seat-requests';
12
+
13
+ interface SeatRequestItem {
14
+ receiptId: string;
15
+ seat: string;
16
+ text: string;
17
+ queuedAt: string;
18
+ status: 'queued';
19
+ }
20
+
21
+ interface PendingRequest {
22
+ status: 'pending';
23
+ key: string;
24
+ seat: string;
25
+ text: string;
26
+ queuedAt: string;
27
+ ref: string;
28
+ }
29
+
30
+ interface RejectedRequest extends Omit<PendingRequest, 'status'> {
31
+ status: 'rejected';
32
+ }
33
+
34
+ type RecordEntry = (SeatRequestItem & { key: string }) | PendingRequest | RejectedRequest;
35
+
36
+ export interface SeatRequestsDeps {
37
+ root?: () => string;
38
+ submit?: typeof submitIntakeWork;
39
+ now?: () => string;
40
+ append?: (path: string, entry: RecordEntry) => void;
41
+ }
42
+
43
+ const seats = trackData.tracks.filter((entry): entry is typeof entry & { title: string } => 'title' in entry)
44
+ .map(({ id, title }) => ({ id, title }));
45
+ const inFlight = new Map<string, { payload: string; task: Promise<Response> }>();
46
+
47
+ function unknownSeat(): Response {
48
+ debug.log('seat-address.pwa', 'rejected', { reason: 'unknown-seat' });
49
+ return jsonResponse({ error: 'unknown-seat', seats }, 400);
50
+ }
51
+
52
+ function file(root: string): string {
53
+ return join(root, 'seat-requests', 'requests.jsonl');
54
+ }
55
+
56
+ function append(path: string, entry: RecordEntry): void {
57
+ mkdirSync(dirname(path), { recursive: true });
58
+ // Atomically replace the journal so a failed write cannot leave a truncated JSON line.
59
+ const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
60
+ const temp = `${path}.${randomUUID()}.tmp`;
61
+ try {
62
+ writeFileSync(temp, `${previous}${JSON.stringify(entry)}\n`);
63
+ const fd = openSync(temp, 'r');
64
+ try { fsyncSync(fd); } finally { closeSync(fd); }
65
+ renameSync(temp, path);
66
+ } catch (error) {
67
+ try { unlinkSync(temp); } catch { /* no temp file to remove */ }
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ function records(path: string): Map<string, RecordEntry> {
73
+ let data: string;
74
+ try { data = readFileSync(path, 'utf8'); }
75
+ catch (error) {
76
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' && !existsSync(path)) data = '';
77
+ else throw error;
78
+ }
79
+ const entries = new Map<string, RecordEntry>();
80
+ for (const line of data.split('\n')) {
81
+ if (!line) continue;
82
+ const entry = JSON.parse(line) as RecordEntry;
83
+ if (typeof entry.key !== 'string' || typeof entry.seat !== 'string' || typeof entry.text !== 'string'
84
+ || typeof entry.queuedAt !== 'string' || !['pending', 'queued', 'rejected'].includes(entry.status)
85
+ || (entry.status === 'queued' && typeof entry.receiptId !== 'string')
86
+ || (entry.status !== 'queued' && typeof entry.ref !== 'string')) {
87
+ throw new Error('invalid seat request journal');
88
+ }
89
+ entries.set(entry.key, entry);
90
+ }
91
+ const dir = join(dirname(path), 'outcomes');
92
+ let names: string[];
93
+ try { names = readdirSync(dir); }
94
+ catch (error) {
95
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return entries;
96
+ throw error;
97
+ }
98
+ for (const name of names) {
99
+ if (!/^[a-f0-9]{64}\.json$/.test(name)) continue;
100
+ const item = JSON.parse(readFileSync(join(dir, name), 'utf8')) as RecordEntry;
101
+ if (item.key !== name.slice(0, -5) || (item.status !== 'queued' && item.status !== 'rejected')) {
102
+ throw new Error('invalid seat request outcome');
103
+ }
104
+ const current = entries.get(item.key);
105
+ if (current?.status === 'queued') continue;
106
+ if (current?.status === 'pending') {
107
+ if (item.status === 'rejected' && current.ref !== item.ref) continue;
108
+ if (item.status === 'queued' && current.queuedAt !== item.queuedAt) continue;
109
+ }
110
+ entries.set(item.key, item);
111
+ }
112
+ return entries;
113
+ }
114
+
115
+ function outcomeFile(path: string, key: string): string {
116
+ return join(dirname(path), 'outcomes', `${key}.json`);
117
+ }
118
+
119
+ function persistOutcome(path: string, entry: RecordEntry, write: typeof append): void {
120
+ try {
121
+ write(path, entry);
122
+ const sidecar = outcomeFile(path, entry.key);
123
+ if (existsSync(sidecar)) unlinkSync(sidecar);
124
+ } catch {
125
+ // The primary journal may be unavailable while the outcome is already committed
126
+ // to the intake queue. Keep a durable sidecar rather than a process-local receipt.
127
+ const target = outcomeFile(path, entry.key);
128
+ mkdirSync(dirname(target), { recursive: true });
129
+ const temp = `${target}.${randomUUID()}.tmp`;
130
+ try {
131
+ writeFileSync(temp, JSON.stringify(entry));
132
+ const fd = openSync(temp, 'r');
133
+ try { fsyncSync(fd); } finally { closeSync(fd); }
134
+ renameSync(temp, target);
135
+ } catch (error) {
136
+ try { unlinkSync(temp); } catch { /* no temp file */ }
137
+ throw error;
138
+ }
139
+ }
140
+ }
141
+
142
+ function receipt(item: SeatRequestItem): Response {
143
+ return jsonResponse({ receiptId: item.receiptId, seat: item.seat, queuedAt: item.queuedAt }, 202);
144
+ }
145
+
146
+ function unavailable(): Response {
147
+ debug.log('seat-address.pwa', 'rejected', { reason: 'journal-unavailable' });
148
+ return jsonResponse({ error: 'enqueue-failed' }, 503);
149
+ }
150
+
151
+ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps = {}): Promise<Response> {
152
+ const path = file((deps.root ?? effectiveInstanceRoot)());
153
+ if (req.method === 'GET') {
154
+ const params = new URL(req.url).searchParams;
155
+ const requestedSeat = params.get('seat');
156
+ const seat = requestedSeat ? resolveSeat(requestedSeat) : undefined;
157
+ if (requestedSeat && !seat) return unknownSeat();
158
+ const rawLimit = params.get('limit');
159
+ const limit = rawLimit === null ? 20 : Number(rawLimit);
160
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) return jsonResponse({ error: 'bad_request' }, 400);
161
+ try {
162
+ const items = [...records(path).values()].filter((entry): entry is RecordEntry & SeatRequestItem => entry.status === 'queued')
163
+ .reverse().filter((item) => !seat || item.seat === seat.id).slice(0, limit)
164
+ .map(({ receiptId, seat: id, text, queuedAt, status }) => ({ receiptId, seat: id, text, queuedAt, status }));
165
+ return jsonResponse({ items, seats });
166
+ } catch { return unavailable(); }
167
+ }
168
+ if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
169
+
170
+ let body: unknown;
171
+ try { body = await req.json(); }
172
+ catch { return jsonResponse({ error: 'bad_request' }, 400); }
173
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
174
+ const input = body as Record<string, unknown>;
175
+ if (typeof input.text !== 'string' || !input.text.trim()
176
+ || (input.seat !== undefined && typeof input.seat !== 'string')) {
177
+ debug.log('seat-address.pwa', 'rejected', { reason: 'invalid-body' });
178
+ return jsonResponse({ error: 'bad_request' }, 400);
179
+ }
180
+ // An implicit address must start the text; parseSeatAddress also accepts later lines.
181
+ const parsed = input.text.startsWith('@') ? parseSeatAddress(input.text) : null;
182
+ const prefix = parsed ? `@${parsed.seats.join(',')}` : '';
183
+ const firstAddress = parsed && input.text.startsWith(prefix)
184
+ && (input.text.length === prefix.length || /[ \t\r\n]/.test(input.text[prefix.length]!)) ? parsed : null;
185
+ const address = input.seat ?? (firstAddress?.seats.length === 1 ? firstAddress.seats[0] : undefined);
186
+ const seat = address ? resolveSeat(address) : undefined;
187
+ if (!seat) return unknownSeat();
188
+ const text = input.seat === undefined ? firstAddress!.body.trim() : input.text.trim();
189
+ if (!text) {
190
+ debug.log('seat-address.pwa', 'rejected', { reason: 'empty-text', seat: seat.id });
191
+ return jsonResponse({ error: 'bad_request' }, 400);
192
+ }
193
+ debug.log('seat-address.pwa', 'parsed', { seat: seat.id, textLength: text.length });
194
+ const now = (deps.now ?? (() => new Date().toISOString()))();
195
+ const header = req.headers.get('idempotency-key');
196
+ if (header !== null && (!header.trim() || header.length > 200)) return jsonResponse({ error: 'bad_request' }, 400);
197
+ const payload = `${seat.id}\0${text}`;
198
+ const key = header === null ? createHash('sha256').update(randomUUID()).digest('hex')
199
+ : createHash('sha256').update(`client\0${header}`).digest('hex');
200
+ const lock = `${path}\0${key}`;
201
+ const pending = inFlight.get(lock);
202
+ if (pending) {
203
+ if (pending.payload !== payload) return jsonResponse({ error: 'idempotency-conflict' }, 409);
204
+ return pending.task.then((response) => response.clone());
205
+ }
206
+ // Install the lock before starting disk or queue work; a second caller must
207
+ // wait for the first instead of independently submitting the same key.
208
+ let start!: () => void;
209
+ const gate = new Promise<void>((resolve) => { start = resolve; });
210
+ const task = (async (): Promise<Response> => {
211
+ await gate;
212
+ let existing: RecordEntry | undefined;
213
+ try { existing = records(path).get(key); }
214
+ catch { return unavailable(); }
215
+ if (existing) {
216
+ if (existing.seat !== seat.id || existing.text !== text) return jsonResponse({ error: 'idempotency-conflict' }, 409);
217
+ if (existing.status === 'queued') return receipt(existing);
218
+ }
219
+ const intent: PendingRequest = existing?.status === 'pending'
220
+ ? existing : { key, seat: seat.id, text, queuedAt: now, status: 'pending', ref: `pwa:${randomUUID()}` };
221
+ if (existing?.status !== 'pending') {
222
+ try {
223
+ (deps.append ?? append)(path, intent);
224
+ if (existing?.status === 'rejected') {
225
+ const sidecar = outcomeFile(path, key);
226
+ if (existsSync(sidecar)) unlinkSync(sidecar);
227
+ }
228
+ } catch { return unavailable(); }
229
+ }
230
+ let result: Awaited<ReturnType<typeof submitIntakeWork>>;
231
+ try {
232
+ result = await (deps.submit ?? submitIntakeWork)({
233
+ text: `@${seat.title ?? seat.id} ${text}`,
234
+ track: 'graph',
235
+ origin: { kind: 'external', ledgerSource: 'pwa', provider: 'other', ref: intent.ref, reportTo: { channel: 'pwa' } },
236
+ });
237
+ } catch { return unavailable(); }
238
+ if (!result.ok || result.track !== 'graph') {
239
+ debug.log('seat-address.pwa', 'rejected', { reason: result.ok ? 'unexpected-track' : result.reason, seat: seat.id });
240
+ try { persistOutcome(path, { ...intent, status: 'rejected' }, deps.append ?? append); }
241
+ catch { return unavailable(); }
242
+ return unavailable();
243
+ }
244
+ const item: RecordEntry & SeatRequestItem = { key, receiptId: result.acceptanceId, seat: seat.id, text, queuedAt: intent.queuedAt, status: 'queued' };
245
+ try { persistOutcome(path, item, deps.append ?? append); }
246
+ catch { return unavailable(); }
247
+ debug.log('seat-address.pwa', 'enqueued', { seat: seat.id, receiptId: item.receiptId });
248
+ return receipt(item);
249
+ })();
250
+ inFlight.set(lock, { payload, task });
251
+ start();
252
+ try { return await task; }
253
+ finally { inFlight.delete(lock); }
254
+ }
@@ -0,0 +1,37 @@
1
+ import { statSync } from 'node:fs';
2
+ import type { LogInstanceView } from '../../mss/logging/instance-registry.js';
3
+ import type { LogStore } from '../../mss/logging/log-store.js';
4
+
5
+ /** Bounded, read-only fallback when a run's ledger is not on this host. */
6
+ export function scanStoresForRun(
7
+ runId: string,
8
+ views: readonly LogInstanceView[],
9
+ open: (view: LogInstanceView) => Pick<LogStore, 'queryTraceRun' | 'close'> | null,
10
+ opts: { sinceMs?: number; untilMs?: number; maxStores?: number; deadlineMs?: number; now?: () => number } = {},
11
+ ): { universe?: string; checked: number; truncated: boolean; unresolved?: boolean } {
12
+ const now = opts.now ?? Date.now;
13
+ const deadline = now() + (opts.deadlineMs ?? 3000);
14
+ const maxStores = opts.maxStores ?? 200;
15
+ const mtime = (view: LogInstanceView): number => {
16
+ try { return statSync(view.dbPath).mtimeMs; } catch { return -Infinity; }
17
+ };
18
+ const candidates = views.map((view) => ({ view, at: view.dbExists ? mtime(view) : -Infinity }))
19
+ .sort((a, b) => (b.at === a.at ? a.view.name.localeCompare(b.view.name) : b.at - a.at));
20
+ let checked = 0;
21
+ let unresolved = false;
22
+ for (const { view } of candidates) {
23
+ if (checked >= maxStores || now() >= deadline) return { checked, truncated: true };
24
+ checked++;
25
+ let store: ReturnType<typeof open> = null;
26
+ try {
27
+ if (!view.dbExists) continue;
28
+ store = open(view);
29
+ if (!store) { unresolved = true; continue; }
30
+ if (store.queryTraceRun(runId, { sinceMs: opts.sinceMs, untilMs: opts.untilMs, limit: 1 }).length) {
31
+ return { universe: view.name, checked, truncated: false };
32
+ }
33
+ } catch { unresolved = true; }
34
+ finally { try { store?.close(); } catch { /* read-only handle cleanup is best effort */ } }
35
+ }
36
+ return { checked, truncated: false, ...(unresolved ? { unresolved } : {}) };
37
+ }
@@ -8,6 +8,7 @@ import { activeStoreNames, parseSinceParam, type LogFabricDeps, registeredLogSto
8
8
  import { checkAuth, type MetaApiOpts } from './meta-api.js';
9
9
  import { jsonResponse } from './json-response.js';
10
10
  import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
11
+ import { scanStoresForRun } from './trace-run-scan.js';
11
12
 
12
13
  export type TraceLevel = 'L0' | 'L1' | 'L2' | 'L3';
13
14
  export interface TraceEvent {
@@ -346,7 +347,8 @@ export function handleTrace(req: Request, opts: MetaApiOpts, deps: TraceDeps = {
346
347
  }
347
348
  if (!opened) return jsonResponse({ ok: false, error: 'log-store-unavailable', failedStores: failures }, 503);
348
349
  const runId = url.searchParams.get('runId');
349
- let resolution: { universe?: string; checked: number } | undefined;
350
+ let resolution: { universe?: string; checked: number; truncated?: boolean; unresolved?: boolean } | undefined;
351
+ let resolvedFrom: 'ledger' | 'log-scan' | undefined;
350
352
  if ((parsed.level === 'L2' || parsed.level === 'L3') && runId && !rows.some((row) => matches(row, url)) && !requestedUniverse) {
351
353
  // A store with no log db at all («has no log store yet» — e.g. the daemon's own tree-derived universe) cannot
352
354
  // hold the run, so it must not block the ledger lookup. A store that exists but failed to open/read still does:
@@ -356,6 +358,20 @@ export function handleTrace(req: Request, opts: MetaApiOpts, deps: TraceDeps = {
356
358
  try { resolution = findRunUniverse(runId, names, deps); }
357
359
  catch (error) { return jsonResponse({ ok: false, error: 'run-universe-unavailable', reason: error instanceof Error ? error.message : String(error) }, 503); }
358
360
  try { debug.log('nexus.trace', 'run-universe-resolved', { runId, universe: resolution.universe ?? 'not-found', checked: resolution.checked }); } catch { /* observation must not block reads */ }
361
+ if (resolution.universe) resolvedFrom = 'ledger';
362
+ else {
363
+ let views: LogInstanceView[];
364
+ try { views = (deps.instances ?? readLogInstances)(); }
365
+ catch (error) { return jsonResponse({ ok: false, error: 'run-universe-unavailable', reason: error instanceof Error ? error.message : String(error) }, 503); }
366
+ resolution = scanStoresForRun(runId, views, (view) => {
367
+ const store = (deps.openRemoteStore ?? ((v: LogInstanceView) => LogStore.openReadOnly(v.dbPath)))(view);
368
+ if (!store) return null;
369
+ return { queryTraceRun: (id, query) => store.queryTraceRun(id, query),
370
+ close: () => (deps.closeRemoteStore ?? ((s: LogStore) => s.close()))(store) };
371
+ }, { sinceMs: parsed.from, untilMs: parsed.to });
372
+ if (resolution.universe) resolvedFrom = 'log-scan';
373
+ try { debug.log('nexus.trace', 'run-universe-scanned', { runId, universe: resolution.universe ?? (resolution.truncated || resolution.unresolved ? 'unresolved' : 'not-found'), checked: resolution.checked, truncated: resolution.truncated }); } catch { /* observation must not block reads */ }
374
+ }
359
375
  if (resolution.universe && !names.includes(resolution.universe)) {
360
376
  const name = resolution.universe;
361
377
  names.push(name);
@@ -373,9 +389,9 @@ export function handleTrace(req: Request, opts: MetaApiOpts, deps: TraceDeps = {
373
389
  }
374
390
  }
375
391
  const filtered = rows.filter((row) => matches(row, url)).sort(sortRows);
376
- const trace = buildTrace(filtered.slice(0, parsed.limit), parsed.level, filtered.length > parsed.limit || candidateLimitReached || failures.length > 0);
392
+ const trace = buildTrace(filtered.slice(0, parsed.limit), parsed.level, filtered.length > parsed.limit || candidateLimitReached || failures.length > 0 || resolution?.truncated === true);
377
393
  try { (deps.queryLog ?? ((details) => debug.log('logs.trace', 'query', details)))({ stores: names, level: parsed.level, count: trace.events.length, truncated: trace.truncated, mode: 'rows', runs: trace.nodes.filter((node) => node.level === 'L1' && node.count > 0).length }); } catch { /* query logging must not block reads */ }
378
- return jsonResponse({ ok: true, ...trace, stores: names, registeredStores: registeredStoreCount(deps), ...(resolution ? resolution.universe ? { resolvedFrom: 'ledger' as const, runUniverse: resolution.universe } : { runUniverse: 'not-found' as const, checked: resolution.checked } : {}), ...(failures.length ? { failedStores: failures } : {}) }, 200);
394
+ return jsonResponse({ ok: true, ...trace, stores: names, registeredStores: registeredStoreCount(deps), ...(resolution ? resolution.universe ? { resolvedFrom, runUniverse: resolution.universe, ...(resolvedFrom === 'log-scan' ? { checked: resolution.checked } : {}) } : { runUniverse: resolution.truncated || resolution.unresolved ? 'unresolved' as const : 'not-found' as const, checked: resolution.checked } : {}), ...(failures.length ? { failedStores: failures } : {}) }, 200);
379
395
  } finally { closeStores(stores, deps); }
380
396
  }
381
397
 
@@ -2626,9 +2626,12 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
2626
2626
  getBot: () => getBotRef.current,
2627
2627
  allowedUsers: [...cfg.discord.allowedUsers],
2628
2628
  });
2629
+ const chatChannels = cfg.discord.chatChannels ?? [];
2630
+ debug.log('nexus.discord', 'chat-channels', { count: chatChannels.length });
2629
2631
  const handle = createNexusDiscordTriggerBot({
2630
2632
  token: discordToken,
2631
2633
  allowedUsers: [...cfg.discord.allowedUsers],
2634
+ ...(chatChannels.length ? { guildTextChannels: chatChannels } : {}),
2632
2635
  dispatch: (event) => workflowDaemon!.dispatchDiscord(event),
2633
2636
  onMessage: composedOnMessage,
2634
2637
  onInteraction: async (raw) => {
@@ -13,6 +13,7 @@ import type { UserConfig as NexusUserConfig } from './config/types.js';
13
13
  import { resolveLaunchdEnvironment } from './install/launchd.js';
14
14
  import { resolveSystemdEnvironment } from './install/systemd.js';
15
15
  import { resolvePwaStaticDir } from './static-dir-resolve.js';
16
+ import { SKILLS_STEP_COMMAND, unattendedSetupHint } from '../onboarding/entry-hints.js';
16
17
 
17
18
  export type SetupItemId = 'llm' | 'pwa-build' | 'channel-bot' | 'skill-dirs' | 'os-install';
18
19
 
@@ -97,8 +98,6 @@ export interface SetupCheckOpts {
97
98
  decideProviderForConfig?: typeof decideProviderForConfig;
98
99
  }
99
100
 
100
- const UNATTENDED_SETUP_HINT = 'unattended: `elanous setup --non-interactive --config <ans.json>`';
101
-
102
101
  function hasText(value: unknown): boolean {
103
102
  return typeof value === 'string' && value.trim().length > 0;
104
103
  }
@@ -146,7 +145,7 @@ function checkLlm(
146
145
  id: 'llm',
147
146
  label: 'LLM provider',
148
147
  passed,
149
- hint: `run \`elanous setup llm\` or interactive \`elanous nexus\`; ${UNATTENDED_SETUP_HINT}`,
148
+ hint: `run \`elanous onboarding llm\` or interactive \`elanous nexus\`; ${unattendedSetupHint()}`,
150
149
  ...(detailProvider && detailProvider !== 'none' ? {
151
150
  detail: `provider=${detailProvider}${credential ? ` · credential=${credential}` : ''}`,
152
151
  } : {}),
@@ -173,7 +172,7 @@ function checkChannelBot(cfg: NexusUserConfig): SetupItem {
173
172
  id: 'channel-bot',
174
173
  label: 'Channel bot',
175
174
  passed: hasText(telegram) || hasText(discord),
176
- hint: `run \`elanous nexus channel-bot setup telegram|discord\`; ${UNATTENDED_SETUP_HINT}`,
175
+ hint: `run \`elanous nexus channel-bot setup telegram|discord\`; ${unattendedSetupHint()}`,
177
176
  };
178
177
  }
179
178
 
@@ -187,7 +186,7 @@ function checkSkillDirs(cfg: MainUserConfig, exists: (path: string) => boolean):
187
186
  label: 'Skill dirs',
188
187
  passed: existingDirs.length > 0,
189
188
  hint: missingDirs.length > 0
190
- ? `create the missing skill directories or choose an already-existing skill directory; ${UNATTENDED_SETUP_HINT}`
189
+ ? `create the missing skill directories or choose an already-existing skill directory; run \`${SKILLS_STEP_COMMAND}\`; ${unattendedSetupHint()}`
191
190
  : '',
192
191
  ...(dirs.length > 0 ? {
193
192
  detail: missingDirs.length > 0 ? `${detail} · missing: ${missingDirs.join(', ')}` : detail,
@@ -0,0 +1,125 @@
1
+ // NT1 — LLM 예산 알림 «모양» (UX · 대표 2026-09-30 22:30 재요청 · OP 결정 B6 ⊕ NT1).
2
+ // 숫자·정책은 TC 의 B6 로더가 채운다(`내부 문서 `RFC-llm-policy-single-file-2026-09-30``) — 이 파일은 문구만 정한다.
3
+ //
4
+ // 규칙 셋(대표 가 받은 알림이 어긴 것들):
5
+ // ① 한 알림 = 한 결정·한 행동 — 맨 윗줄이 «무엇이 바뀌었나», 마지막 줄이 «할 일».
6
+ // ② 바뀐 게 없으면 침묵 — 같은 상태(key)면 send=false.
7
+ // ③ 모순 금지 — «한도 참»과 «크레딧으로 진행 중»을 한 알림에 같이 쓰지 않는다. 상태 하나만 고른다.
8
+ // ⊕ 내부 낱말 0 — «자»·«판정»·계정 내부 이름 나열을 쓰지 않는다(계정은 «계정 N개»로 센다).
9
+
10
+ export interface LlmBudgetAccount {
11
+ name: string;
12
+ /** 구독 잔량 %(0~100). 모르면 undefined. */
13
+ subscriptionRemainingPct?: number;
14
+ /** 이 계정의 크레딧 잔액. */
15
+ credits?: number;
16
+ resetInHours?: number;
17
+ }
18
+
19
+ export interface LlmBudgetSnapshot {
20
+ /** ISO 시각(Asia/Seoul 로 날짜를 자른다). */
21
+ at: string;
22
+ credits: {
23
+ total: number;
24
+ usedToday: number;
25
+ /** 하루 목표 소진량(정책 `credits.pace`). */
26
+ paceTarget: number;
27
+ /** ISO 날짜 — 크레딧이 사라지는 날. */
28
+ expiresAt?: string;
29
+ /** 정책이 «크레딧 먼저»인가(`until` 전). */
30
+ useFirst?: boolean;
31
+ };
32
+ accounts: LlmBudgetAccount[];
33
+ selected: { account?: string; reason: 'subscription' | 'credits' | 'fallback' | 'none' };
34
+ fallback?: { provider: string; remainingPct?: number };
35
+ }
36
+
37
+ export interface LlmBudgetAlert {
38
+ send: boolean;
39
+ /** 상태 열쇠 — 다음 호출의 prevKey 로 넘긴다. */
40
+ key: string;
41
+ text: string;
42
+ }
43
+
44
+ const EXPIRY_WARN_DAYS = 7;
45
+ /** 공개 알림에 표시할 수 있는 폴백 제공자명. 현재 폴백 체인의 외부 제공자는 grok 뿐이다. */
46
+ const PUBLIC_FALLBACK_PROVIDERS = new Set(['grok']);
47
+ /** 이 시각(서울) 뒤에야 «오늘 페이스가 느리다»를 말한다 — 아침에 0 인 건 정상이다. */
48
+ const PACE_CHECK_HOUR = 18;
49
+
50
+ const n = (v: number) => Math.round(v).toLocaleString('ko-KR');
51
+
52
+ function seoulParts(iso: string): { date: string; hour: number } {
53
+ const d = new Date(iso);
54
+ const f = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hourCycle: 'h23' });
55
+ const p = Object.fromEntries(f.formatToParts(d).map((x) => [x.type, x.value]));
56
+ return { date: `${p.year}-${p.month}-${p.day}`, hour: Number(p.hour) };
57
+ }
58
+
59
+ function daysBetween(fromIso: string, toIso: string): number {
60
+ const a = Date.parse(seoulParts(fromIso).date);
61
+ const b = Date.parse(seoulParts(toIso).date);
62
+ return Math.round((b - a) / 86_400_000);
63
+ }
64
+
65
+ function mmdd(iso: string): string {
66
+ const { date } = seoulParts(iso);
67
+ return `${date.slice(5, 7)}-${date.slice(8, 10)}`;
68
+ }
69
+
70
+ function creditsLine(s: LlmBudgetSnapshot): string {
71
+ const parts = [`오늘 ${n(s.credits.usedToday)} / 목표 ${n(s.credits.paceTarget)}`, `남은 크레딧 ${n(s.credits.total)}`];
72
+ if (s.credits.expiresAt) parts.push(`${mmdd(s.credits.expiresAt)} 만료`);
73
+ return parts.join(' · ');
74
+ }
75
+
76
+ /** 지금 상태 하나를 고른다(위에서부터 먼저 맞는 것). 아무것도 아니면 침묵. */
77
+ export function buildLlmBudgetAlert(now: LlmBudgetSnapshot, prevKey?: string): LlmBudgetAlert {
78
+ const { date, hour } = seoulParts(now.at);
79
+ const accounts = now.accounts.length;
80
+ let key = 'ok';
81
+ let lines: string[] = [];
82
+
83
+ const expiryDays = now.credits.expiresAt ? daysBetween(now.at, now.credits.expiresAt) : undefined;
84
+
85
+ if (now.selected.reason === 'none') {
86
+ key = 'blocked';
87
+ lines = [
88
+ '⛔ LLM 을 쓸 곳이 없어 작업이 멈췄습니다.',
89
+ `구독 계정 ${accounts}개 한도 · 크레딧·대체 모델도 못 씁니다.`,
90
+ '할 일: 크레딧 사용을 허락하거나 대체 모델 키를 확인해 주세요.',
91
+ ];
92
+ } else if (now.selected.reason === 'fallback') {
93
+ const soonest = now.accounts.map((a) => a.resetInHours)
94
+ .filter((h): h is number => typeof h === 'number' && Number.isFinite(h) && h > 0)
95
+ .sort((a, b) => a - b)[0];
96
+ const candidate = now.fallback?.provider?.trim();
97
+ const provider = candidate && PUBLIC_FALLBACK_PROVIDERS.has(candidate) ? candidate : undefined;
98
+ key = `fallback:${provider || 'unknown'}`;
99
+ lines = [
100
+ `⚠️ codex 를 못 써서 ${provider ? `${provider} 로` : '대체 모델로'} 일하고 있습니다${typeof now.fallback?.remainingPct === 'number' ? `(남은 ${Math.round(now.fallback.remainingPct)}%)` : ''}.`,
101
+ soonest !== undefined ? `codex 는 ${Math.ceil(soonest)}시간 뒤 돌아옵니다.` : 'codex 가 언제 돌아오는지 아직 모릅니다.',
102
+ '할 일: 없음 — 참고만 하세요.',
103
+ ];
104
+ } else if (expiryDays !== undefined && expiryDays >= 0 && expiryDays <= EXPIRY_WARN_DAYS && now.credits.total > 0) {
105
+ key = `expiry:${expiryDays}`;
106
+ lines = [
107
+ `💳 크레딧 ${n(now.credits.total)} 이 ${expiryDays === 0 ? '오늘' : `${expiryDays}일 뒤`} 사라집니다.`,
108
+ creditsLine(now),
109
+ '할 일: 만료 전에 크레딧 사용을 확인해 주세요.',
110
+ ];
111
+ } else if (now.credits.useFirst && now.credits.total > 0 && (expiryDays === undefined || expiryDays >= 0) && hour >= PACE_CHECK_HOUR && now.credits.usedToday < now.credits.paceTarget * 0.5) {
112
+ // 하루 한 번만 — 날짜를 열쇠에 넣는다.
113
+ key = `pace-behind:${date}`;
114
+ lines = [
115
+ now.selected.reason === 'credits'
116
+ ? '💳 크레딧을 먼저 쓰는 중인데 오늘 쓴 양이 목표의 절반이 안 됩니다.'
117
+ : '💳 크레딧을 먼저 쓰라는 방침인데 오늘은 구독 계정이 먼저 쓰였습니다.',
118
+ creditsLine(now),
119
+ now.selected.reason === 'credits' ? '할 일: 없음 — 맡긴 일이 적었던 날입니다.' : '할 일: 없음 — 기술 담당이 순서를 고치는 중입니다.',
120
+ ];
121
+ }
122
+
123
+ const send = key !== 'ok' && key !== prevKey;
124
+ return { send, key, text: send ? lines.join('\n') : '' };
125
+ }
@@ -47,6 +47,8 @@ type RotationReason =
47
47
  | 'no-candidate'
48
48
  /** 찼고 구독 잔량이 남은 계정도 없지만 대표 가 크레딧 사용을 허가했다 — 지금 계정에 머물러 선불 크레딧으로 계속(grok 폴백 안 함). */
49
49
  | 'credits-allowed'
50
+ /** 오늘 크레딧 몫을 먼저 쓰기 위해 찬 계정으로 이동하거나 머문다. */
51
+ | 'credit-pace'
50
52
  /** 넘겼다. */
51
53
  | 'rotated';
52
54
 
@@ -101,6 +103,7 @@ interface RotationInput {
101
103
  /** 지금 계정의 크레딧 잔액·보유(모르면 없다). */
102
104
  readonly currentCreditBalance?: number;
103
105
  readonly currentHasCredits?: boolean;
106
+ readonly creditPace?: { active: boolean };
104
107
  }
105
108
 
106
109
  /**
@@ -182,6 +185,23 @@ export function decideCodexRotation(input: RotationInput): RotationDecision {
182
185
  });
183
186
  if (input.explicit) return { reason: 'explicit', candidateCount, accountThresholds };
184
187
  if (!input.enabled) return { reason: 'disabled', candidateCount, accountThresholds, ...(input.disabledProvenance ? { disabledProvenance: input.disabledProvenance } : {}) };
188
+ if (input.creditPace?.active) {
189
+ const self = input.candidates.find((candidate) => candidate.name === input.current.name);
190
+ const eligible = (name: string, balance: number | undefined, hasCredits: boolean | undefined, home: string | undefined): boolean =>
191
+ !!home?.trim() && typeof balance === 'number' && Number.isFinite(balance) && balance > 0
192
+ && hasCredits !== false && accountThresholds.some((row) => row.name === name
193
+ && (row.status === 'reached' || row.status === 'threshold-reached'));
194
+ const currentBalance = input.currentCreditBalance ?? self?.creditBalance;
195
+ const currentHas = input.currentHasCredits ?? self?.hasCredits;
196
+ const to = otherCandidates.filter((candidate) => eligible(candidate.name, candidate.creditBalance, candidate.hasCredits, candidate.home))
197
+ .sort((a, b) => (b.creditBalance! - a.creditBalance!) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))[0];
198
+ if (eligible(input.current.name, currentBalance, currentHas, self?.home ?? input.current.home)) {
199
+ return { reason: 'credit-pace', candidateCount, accountThresholds };
200
+ }
201
+ if (to) {
202
+ return { reason: 'credit-pace', candidateCount, accountThresholds, to };
203
+ }
204
+ }
185
205
  const currentUsageUnknown = input.currentReached === undefined && input.currentUsedPercent == null;
186
206
  if (input.currentReached !== true && !currentUsageUnknown && accountThresholds[0]?.status !== 'threshold-reached') {
187
207
  return { reason: 'not-reached', candidateCount, accountThresholds };
@@ -271,7 +291,7 @@ export function observeRotation(decision: RotationDecision, from: string): void
271
291
  ...(decision.reason === 'disabled' && decision.disabledProvenance
272
292
  ? { disabledProvenance: decision.disabledProvenance }
273
293
  : {}),
274
- }, { level: decision.reason === 'rotated' || decision.reason === 'reset-credit-unknown' ? 'warn' : 'debug' });
294
+ }, { level: decision.reason === 'rotated' || decision.reason === 'reset-credit-unknown' || (decision.reason === 'credit-pace' && decision.to) ? 'warn' : 'debug' });
275
295
  for (const account of decision.accountThresholds ?? []) {
276
296
  debug.log('oauth.codex-account', 'rotation-account-threshold', account);
277
297
  }
@@ -279,7 +299,8 @@ export function observeRotation(decision: RotationDecision, from: string): void
279
299
  if (decision.reason !== 'explicit' && decision.reason !== 'disabled') {
280
300
  const fromRow = decision.accountThresholds?.find((a) => a.name === from);
281
301
  const toCredit = decision.to?.creditBalance;
282
- const why = typeof toCredit === 'number'
302
+ const why = decision.reason === 'credit-pace' ? '오늘 크레딧 몫을 먼저 쓴다'
303
+ : typeof toCredit === 'number'
283
304
  // 금액은 문장에 넣지 않는다 — 판단 사유는 Live·Trace 카드에 글자로 뜨고 공개 캡처가 문장 속 숫자를 못 가린다(09-28 🅣).
284
305
  ? `구독 한도 전부 참 · ${decision.to!.name} 크레딧이 가장 많다`
285
306
  : fromRow ? `${from} ${fromRow.usedPercent ?? '?'}% · 임계 ${fromRow.thresholdPercent}%` : `판정 ${decision.reason}`;
@@ -287,7 +308,8 @@ export function observeRotation(decision: RotationDecision, from: string): void
287
308
  kind: 'ROUTE',
288
309
  what: decision.to ? `codex 계정 ${from} → ${decision.to.name}` : `codex 계정 ${from} 유지 (${decision.reason})`,
289
310
  reason: why,
290
- purpose: decision.reason === 'credits-allowed' ? '한도가 찼다 — 정책상 크레딧으로 계속'
311
+ purpose: decision.reason === 'credit-pace' ? '하루 크레딧 몫을 채운 뒤 구독 순서로 복귀'
312
+ : decision.reason === 'credits-allowed' ? '한도가 찼다 — 정책상 크레딧으로 계속'
291
313
  : typeof toCredit === 'number' ? '크레딧을 세 계정에 고르게 · 한 계정이 먼저 마르지 않게' : '구독 한도 보존 · 끊김 없이 계속',
292
314
  target: decision.to?.name ?? from,
293
315
  refs: { account: decision.to?.name ?? from },
@@ -301,7 +323,7 @@ export function applyRotation(
301
323
  current: CodexAccountResolution,
302
324
  decision: RotationDecision,
303
325
  ): CodexAccountResolution {
304
- if ((decision.reason !== 'rotated' && decision.reason !== 'reset-credit-unknown') || !decision.to) return current;
326
+ if ((decision.reason !== 'rotated' && decision.reason !== 'reset-credit-unknown' && decision.reason !== 'credit-pace') || !decision.to) return current;
305
327
  return {
306
328
  name: decision.to.name,
307
329
  storeKey: decision.to.storeKey,