elanous 0.2.6 → 0.2.8

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 (517) 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-3084c58c1725a40b.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/7032-f4ef698518fba61c.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.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-9aa47b0bc6d570ff.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-d4b6df905f185055.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.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-6a8158b71d1fe9d7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2e002ad8faa43291.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-7464c1987dd76e6d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.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-085847b088675978.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.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/5ef72a618a8994f3.css +1 -0
  96. package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_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/graphs/test-diet/recipes.yaml +9 -0
  178. package/graphs/test-diet/test-diet.yaml +22 -0
  179. package/package.json +6 -5
  180. package/persona-presets/_index.yaml +10 -0
  181. package/persona-presets/_schema.json +146 -0
  182. package/persona-presets/dana.yaml +26 -0
  183. package/persona-presets/haru.yaml +27 -0
  184. package/persona-presets/kai.yaml +27 -0
  185. package/persona-presets/leo.yaml +27 -0
  186. package/persona-presets/mira.yaml +30 -0
  187. package/persona-presets/nari.yaml +27 -0
  188. package/persona-presets/sora.yaml +27 -0
  189. package/persona-presets/yuna.yaml +27 -0
  190. package/plugins/card-followup/README.md +13 -4
  191. package/plugins/card-followup/graphs/card-followup.yaml +3 -1
  192. package/plugins/card-followup/graphs/recipes.yaml +5 -1
  193. package/plugins/card-followup/graphs/run-step.ts +122 -18
  194. package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
  195. package/plugins/card-followup/plugin.json +1 -1
  196. package/plugins/doc-draft/README.md +13 -0
  197. package/plugins/doc-draft/examples/brief-sample.json +8 -0
  198. package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
  199. package/plugins/doc-draft/graphs/recipes.yaml +9 -0
  200. package/plugins/doc-draft/graphs/run-step.ts +113 -0
  201. package/plugins/doc-draft/plugin.json +24 -0
  202. package/plugins/geo-check/README.md +3 -1
  203. package/plugins/geo-check/graphs/run-step.ts +58 -13
  204. package/plugins/geo-check/plugin.json +1 -1
  205. package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
  206. package/plugins/job-coach/README.md +1 -1
  207. package/plugins/job-coach/graphs/run-step.ts +2 -2
  208. package/plugins/job-coach/plugin.json +1 -1
  209. package/plugins/job-coach/plugin.ts +1 -1
  210. package/scripts/affinity/endcard.sh +3 -2
  211. package/scripts/affinity/reframe.sh +3 -1
  212. package/scripts/ci-public-export-leak-gate.ts +48 -0
  213. package/scripts/codex-quota-alert.ts +122 -258
  214. package/scripts/coord-tracks.json +4 -4
  215. package/scripts/field-feed/deliver-node.ts +57 -0
  216. package/scripts/field-feed/feed-node.ts +67 -0
  217. package/scripts/field-feed/lib.ts +164 -0
  218. package/scripts/field-feed/reel-node.ts +24 -0
  219. package/scripts/field-feed/start.ts +62 -0
  220. package/scripts/fixtures/vflow-sample.ndjson +6 -0
  221. package/scripts/install.sh +109 -21
  222. package/scripts/landing-heal/clean-checkout.ts +114 -0
  223. package/scripts/landing-heal/run-mission.ts +38 -3
  224. package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
  225. package/scripts/persona-presets/lint.ts +79 -0
  226. package/scripts/public-export.ts +8 -4
  227. package/scripts/release-loop/baseline-from-logs.ts +92 -0
  228. package/scripts/release-loop/checklist-gate-node.ts +2 -2
  229. package/scripts/release-loop/env-known-failures.ts +31 -0
  230. package/scripts/release-loop/gate-node.ts +108 -22
  231. package/scripts/release-loop/known-issues-node.ts +159 -0
  232. package/scripts/release-loop/npm-publish-node.ts +124 -0
  233. package/scripts/release-loop/ops-upgrade-node.ts +95 -0
  234. package/scripts/release-loop/publish-node.ts +23 -1
  235. package/scripts/release-loop/release-readiness.ts +58 -0
  236. package/scripts/release-loop/shard-plan.ts +17 -0
  237. package/scripts/release-loop/unattended-release.ts +93 -0
  238. package/scripts/storyboard/storyboard.ts +167 -0
  239. package/scripts/test-diet/lib.ts +88 -0
  240. package/scripts/test-diet/node.ts +102 -0
  241. package/scripts/ux/measure-first-text.ts +102 -0
  242. package/scripts/vflow-collect.ts +20 -7
  243. package/scripts/vflow-search.ts +73 -0
  244. package/scripts/video-free-line-test-walker.ts +36 -0
  245. package/scripts/video-pipeline.ts +2 -2
  246. package/skills/explainer-video/SKILL.md +12 -9
  247. package/skills/explainer-video/engine/build.mjs +1 -1
  248. package/skills/explainer-video/engine/reel.mjs +138 -31
  249. package/skills/explainer-video/engine/reel.sh +2 -1
  250. package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
  251. package/skills/grill-me/SKILL.md +1 -1
  252. package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
  253. package/src/a2a/server.ts +2 -0
  254. package/src/acp/auth-methods.ts +30 -0
  255. package/src/acp/elanous-aware-peers.ts +30 -0
  256. package/src/acp/server.ts +96 -20
  257. package/src/agent/monad-agent-turn.ts +25 -6
  258. package/src/agent/shared-app-tools.ts +5 -3
  259. package/src/agent-substrate/review-intent.ts +6 -2
  260. package/src/agent-substrate/shard-boundary-intent.ts +70 -0
  261. package/src/auth/github-app-token.ts +4 -1
  262. package/src/autopilot/track-agent-shadow.ts +151 -0
  263. package/src/autopilot/track-agent.ts +23 -3
  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/channel-import/detect.ts +191 -0
  271. package/src/channel-import/run.ts +111 -0
  272. package/src/chat/chat-cli.ts +4 -1
  273. package/src/chat/short-question.ts +40 -0
  274. package/src/cli/agent-cli.ts +24 -15
  275. package/src/cli/ask-bare.ts +42 -0
  276. package/src/cli/cli-user-error.ts +25 -0
  277. package/src/cli/codex-credit-plan-view.ts +25 -0
  278. package/src/cli/config-test-sync.ts +4 -0
  279. package/src/cli/decisions-cli.ts +13 -3
  280. package/src/cli/dependence-gauge.ts +237 -0
  281. package/src/cli/doctor-bundle.ts +128 -0
  282. package/src/cli/doctor-cli.ts +41 -1
  283. package/src/cli/doctor-local-llm.ts +95 -0
  284. package/src/cli/doctor-modes.ts +48 -0
  285. package/src/cli/import-cli.ts +60 -0
  286. package/src/cli/intake-cli.ts +12 -2
  287. package/src/cli/intake-source-cli.ts +58 -0
  288. package/src/cli/machine-cli.ts +23 -0
  289. package/src/cli/mcp-login.ts +13 -4
  290. package/src/cli/msg-cli.ts +137 -0
  291. package/src/cli/persona-cli.ts +116 -0
  292. package/src/cli/plugin-cli.ts +25 -0
  293. package/src/cli/pr-cli.ts +36 -0
  294. package/src/cli/provider-cli.ts +5 -0
  295. package/src/cli/release-cli.ts +50 -8
  296. package/src/cli/release-run-if-ready.ts +48 -0
  297. package/src/cli/setup-cli.ts +27 -6
  298. package/src/cli/setup-recommend.ts +8 -0
  299. package/src/cli/skills-cli.ts +173 -0
  300. package/src/cli/start-cli.ts +13 -4
  301. package/src/cli/usage-cli.ts +11 -1
  302. package/src/codex/setup.ts +37 -16
  303. package/src/dashboard/index.ts +18 -2
  304. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
  305. package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
  306. package/src/decisions/decision-cards.ts +291 -0
  307. package/src/decisions/decision-ledger.ts +5 -2
  308. package/src/decisions/discord-decision-cards.ts +133 -0
  309. package/src/decisions/telegram-decision-cards.ts +100 -0
  310. package/src/directives/version-at.ts +77 -0
  311. package/src/discord.ts +73 -6
  312. package/src/domains/core-tools.ts +37 -14
  313. package/src/domains/outbound-alert.ts +94 -12
  314. package/src/domains/repeated-failure.ts +1 -1
  315. package/src/error-report/report.ts +200 -0
  316. package/src/exec-requests/planner.ts +146 -0
  317. package/src/exec-requests/runner.ts +378 -0
  318. package/src/exec-requests/store.ts +78 -0
  319. package/src/field/field-feed-auto.ts +48 -0
  320. package/src/field/field-media.ts +8 -3
  321. package/src/field/field-reel.ts +203 -0
  322. package/src/grok/credential.ts +7 -5
  323. package/src/harness/aside-run-status.ts +33 -0
  324. package/src/harness/browser-verify.ts +48 -17
  325. package/src/harness/harness-cli-command.ts +25 -3
  326. package/src/harness/image-load-state.ts +21 -0
  327. package/src/harness/process-garbage.ts +45 -0
  328. package/src/hooks/error-report.ts +210 -0
  329. package/src/hooks/receiver.ts +41 -1
  330. package/src/import/apply.ts +128 -0
  331. package/src/import/plan.ts +197 -0
  332. package/src/index.ts +168 -21
  333. package/src/input-core/text-entry.ts +13 -5
  334. package/src/instance/leader.ts +13 -1
  335. package/src/intake-plane/collect-github.ts +5 -3
  336. package/src/intake-plane/collect-telegram-saved.ts +15 -3
  337. package/src/intake-plane/discord-seat-work.ts +56 -0
  338. package/src/intake-plane/intake-sources.ts +202 -0
  339. package/src/intake-plane/items.ts +28 -12
  340. package/src/intake-plane/seat-answer.ts +96 -0
  341. package/src/intake-plane/seat-doc-route.ts +221 -0
  342. package/src/intake-plane/submit-intake-work.ts +9 -1
  343. package/src/intake-plane/telegram-seat-work.ts +72 -0
  344. package/src/llm/model-defaults.ts +24 -1
  345. package/src/llm.ts +5 -4
  346. package/src/machines/machine-ledger.ts +127 -0
  347. package/src/market/pack-internal-refs.ts +28 -0
  348. package/src/market/publish.ts +31 -4
  349. package/src/maturity/feature-maturity.ts +65 -0
  350. package/src/model-tier/turn-tier.ts +24 -0
  351. package/src/msg/msg-store.ts +149 -0
  352. package/src/native-tool-catalog.ts +2 -1
  353. package/src/nexus/api/chat-fast-path-config.ts +28 -0
  354. package/src/nexus/api/consult-requests.ts +128 -0
  355. package/src/nexus/api/discord-trigger-bot.ts +3 -0
  356. package/src/nexus/api/error-reports.ts +108 -0
  357. package/src/nexus/api/exec-requests.ts +51 -0
  358. package/src/nexus/api/field-uploads.ts +40 -3
  359. package/src/nexus/api/graph-approvals-feed.ts +140 -0
  360. package/src/nexus/api/graph-approvals.ts +71 -4
  361. package/src/nexus/api/http-server.ts +82 -8
  362. package/src/nexus/api/outbound-report.ts +7 -0
  363. package/src/nexus/api/running-runs-background.ts +68 -0
  364. package/src/nexus/api/running-runs-child.ts +6 -0
  365. package/src/nexus/api/seat-requests.ts +254 -0
  366. package/src/nexus/api/share-captures.ts +91 -0
  367. package/src/nexus/api/skill-problems.ts +56 -0
  368. package/src/nexus/api/terminals.ts +24 -8
  369. package/src/nexus/api/trace-run-scan.ts +37 -0
  370. package/src/nexus/api/trace.ts +19 -3
  371. package/src/nexus/index.ts +28 -1
  372. package/src/nexus/setup-status.ts +11 -7
  373. package/src/notify/llm-budget-alert.ts +16 -10
  374. package/src/oauth/browser-availability.ts +14 -0
  375. package/src/oauth/codex-account-rotation.ts +26 -4
  376. package/src/oauth/codex-account-store.ts +20 -2
  377. package/src/oauth/codex-browser-login.ts +173 -0
  378. package/src/oauth/codex-quota-policy.ts +16 -0
  379. package/src/oauth/codex.ts +4 -1
  380. package/src/oauth/fallback-chain.ts +5 -5
  381. package/src/oauth/store.ts +42 -5
  382. package/src/onboarding/continue-here.ts +34 -0
  383. package/src/onboarding/entry-hints.ts +8 -0
  384. package/src/onboarding/full-screen-io.ts +35 -3
  385. package/src/onboarding/help/skills.md +1 -1
  386. package/src/onboarding/non-interactive.ts +1 -0
  387. package/src/onboarding/subscription-hint.ts +27 -0
  388. package/src/onboarding/web-first.ts +90 -0
  389. package/src/onboarding.ts +287 -26
  390. package/src/persona/presets.ts +105 -0
  391. package/src/plugins/maker/node-install.ts +92 -0
  392. package/src/plugins/maker/node-maker.ts +139 -0
  393. package/src/plugins/maker/plugin-maker.ts +19 -7
  394. package/src/policy/llm-policy.ts +268 -0
  395. package/src/prompt-library/session-guidance.ts +7 -1
  396. package/src/prompt-library/universal-preamble.ts +2 -2
  397. package/src/relay/relay-cli.ts +20 -0
  398. package/src/relay/relay-server.ts +220 -0
  399. package/src/release-loop/checklist.ts +37 -13
  400. package/src/self-dev/decompose.ts +1 -0
  401. package/src/self-dev/dev-pipeline.ts +6 -3
  402. package/src/self-dev/orchestrate.ts +18 -11
  403. package/src/self-dev/run-record-mirror.ts +44 -0
  404. package/src/self-dev/run-store.ts +12 -3
  405. package/src/self-implement/harness-policy-gates.ts +4 -1
  406. package/src/self-implement/report-origin.ts +12 -1
  407. package/src/session/chat.ts +17 -5
  408. package/src/shell-env-bootstrap.ts +44 -7
  409. package/src/skills/connect-claude-plugin.ts +142 -0
  410. package/src/skills/connect.ts +70 -0
  411. package/src/skills/index.ts +29 -1
  412. package/src/skills/repair.ts +124 -0
  413. package/src/skills/runner.ts +11 -2
  414. package/src/skills/shared-agent-skill-roots.ts +25 -0
  415. package/src/skills/trigger-extract.ts +1 -1
  416. package/src/steward/failure-streak.ts +78 -0
  417. package/src/steward/steward-cards.ts +24 -0
  418. package/src/steward/triage-budget.ts +127 -0
  419. package/src/steward/triage-plan.ts +75 -0
  420. package/src/steward/triage.ts +244 -42
  421. package/src/task-cards/card-store.ts +1 -1
  422. package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
  423. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  424. package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
  425. package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
  426. package/src/telegram-commands.ts +13 -0
  427. package/src/telegram.ts +72 -2
  428. package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
  429. package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
  430. package/src/user-config.ts +189 -15
  431. package/src/version/packed-revision.json +1 -1
  432. package/src/video-pipeline/recipes/character.ts +42 -5
  433. package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
  434. package/src/video-pipeline/recipes/free-line.ts +16 -3
  435. package/src/video-pipeline/recipes/remote.ts +7 -1
  436. package/src/video-pipeline/vflow-reference.ts +109 -0
  437. package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
  446. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
  515. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
  516. package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
  517. /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
 
3
3
  import { debug } from '../debug/log.js';
4
- import { buildUserConfig, type UserConfig as MainUserConfig } from '../user-config.js';
4
+ import { buildUserConfig, resolveSkillSources, type UserConfig as MainUserConfig } from '../user-config.js';
5
5
  import { decideProviderForConfig } from '../llm.js';
6
6
  import { resolveGrokCredential } from '../grok/credential.js';
7
7
  import { loadTokens } from '../oauth/store.js';
@@ -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,12 +172,17 @@ 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
 
180
179
  function checkSkillDirs(cfg: MainUserConfig, exists: (path: string) => boolean): SetupItem {
181
- const dirs = Array.isArray(cfg.skills.dirs) ? cfg.skills.dirs.filter(hasText) : [];
180
+ // EN5 — the same list the loader reads (resolveSkillSources): the preset and what `elanous connect` registered.
181
+ // Derived roots (shared ~/.agents/skills, Claude package roots) are optional extras, not «configured» dirs.
182
+ const dirs = resolveSkillSources(cfg)
183
+ .filter((source) => source.enabled && (source.kind === 'preset' || source.kind === 'connected'))
184
+ .map((source) => source.path)
185
+ .filter(hasText);
182
186
  const existingDirs = dirs.filter(exists);
183
187
  const missingDirs = dirs.filter((dir) => !exists(dir));
184
188
  const detail = `${dirs.length} dir${dirs.length === 1 ? '' : 's'} · ${existingDirs.length} exist`;
@@ -187,7 +191,7 @@ function checkSkillDirs(cfg: MainUserConfig, exists: (path: string) => boolean):
187
191
  label: 'Skill dirs',
188
192
  passed: existingDirs.length > 0,
189
193
  hint: missingDirs.length > 0
190
- ? `create the missing skill directories or choose an already-existing skill directory; ${UNATTENDED_SETUP_HINT}`
194
+ ? `create the missing skill directories or choose an already-existing skill directory; run \`${SKILLS_STEP_COMMAND}\`; ${unattendedSetupHint()}`
191
195
  : '',
192
196
  ...(dirs.length > 0 ? {
193
197
  detail: missingDirs.length > 0 ? `${detail} · missing: ${missingDirs.join(', ')}` : detail,
@@ -42,6 +42,8 @@ export interface LlmBudgetAlert {
42
42
  }
43
43
 
44
44
  const EXPIRY_WARN_DAYS = 7;
45
+ /** 공개 알림에 표시할 수 있는 폴백 제공자명. 현재 폴백 체인의 외부 제공자는 grok 뿐이다. */
46
+ const PUBLIC_FALLBACK_PROVIDERS = new Set(['grok']);
45
47
  /** 이 시각(서울) 뒤에야 «오늘 페이스가 느리다»를 말한다 — 아침에 0 인 건 정상이다. */
46
48
  const PACE_CHECK_HOUR = 18;
47
49
 
@@ -87,22 +89,26 @@ export function buildLlmBudgetAlert(now: LlmBudgetSnapshot, prevKey?: string): L
87
89
  `구독 계정 ${accounts}개 한도 · 크레딧·대체 모델도 못 씁니다.`,
88
90
  '할 일: 크레딧 사용을 허락하거나 대체 모델 키를 확인해 주세요.',
89
91
  ];
90
- } else if (now.selected.reason === 'fallback' && now.fallback) {
91
- const soonest = now.accounts.map((a) => a.resetInHours).filter((h): h is number => typeof h === 'number').sort((a, b) => a - b)[0];
92
- key = `fallback:${now.fallback.provider}`;
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'}`;
93
99
  lines = [
94
- `⚠️ codex 를 못 써서 ${now.fallback.provider} 로 일하고 있습니다${typeof now.fallback.remainingPct === 'number' ? `(남은 ${Math.round(now.fallback.remainingPct)}%)` : ''}.`,
100
+ `⚠️ codex 를 못 써서 ${provider ? `${provider} 로` : '대체 모델로'} 일하고 있습니다${typeof now.fallback?.remainingPct === 'number' ? `(남은 ${Math.round(now.fallback.remainingPct)}%)` : ''}.`,
95
101
  soonest !== undefined ? `codex 는 ${Math.ceil(soonest)}시간 뒤 돌아옵니다.` : 'codex 가 언제 돌아오는지 아직 모릅니다.',
96
102
  '할 일: 없음 — 참고만 하세요.',
97
103
  ];
98
- } else if (expiryDays !== undefined && expiryDays <= EXPIRY_WARN_DAYS && now.credits.total > 0) {
99
- key = `expiry:${Math.max(0, expiryDays)}`;
104
+ } else if (expiryDays !== undefined && expiryDays >= 0 && expiryDays <= EXPIRY_WARN_DAYS && now.credits.total > 0) {
105
+ key = `expiry:${expiryDays}`;
100
106
  lines = [
101
- `💳 크레딧 ${n(now.credits.total)} 이 ${expiryDays <= 0 ? '오늘' : `${expiryDays}일 뒤`} 사라집니다.`,
107
+ `💳 크레딧 ${n(now.credits.total)} 이 ${expiryDays === 0 ? '오늘' : `${expiryDays}일 뒤`} 사라집니다.`,
102
108
  creditsLine(now),
103
- '할 일: 무거운 작업을 지금 맡기면 크레딧으로 돕니다.',
109
+ '할 일: 만료 전에 크레딧 사용을 확인해 주세요.',
104
110
  ];
105
- } else if (now.credits.useFirst && now.credits.total > 0 && hour >= PACE_CHECK_HOUR && now.credits.usedToday < now.credits.paceTarget * 0.5) {
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) {
106
112
  // 하루 한 번만 — 날짜를 열쇠에 넣는다.
107
113
  key = `pace-behind:${date}`;
108
114
  lines = [
@@ -115,5 +121,5 @@ export function buildLlmBudgetAlert(now: LlmBudgetSnapshot, prevKey?: string): L
115
121
  }
116
122
 
117
123
  const send = key !== 'ok' && key !== prevKey;
118
- return { send, key, text: lines.join('\n') };
124
+ return { send, key, text: send ? lines.join('\n') : '' };
119
125
  }
@@ -0,0 +1,14 @@
1
+ // OB5d — 브라우저를 열 수 없는 세션이면 «왜 못 여는지 ⊕ 대안»을 한 줄로 말한다(대표 10-01 node-c 실측: ssh 에서 아무 말 없이 기기 코드로 갔다).
2
+ // null = 이 세션에서 브라우저 로그인을 먼저 시도해도 된다.
3
+ export function browserUnavailableReason(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string | null {
4
+ const ssh = !!(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
5
+ if (platform === 'darwin' || platform === 'win32') {
6
+ if (!ssh) return null;
7
+ const where = platform === 'darwin' ? '이 Mac' : '이 PC';
8
+ return `원격(ssh) 접속이라 브라우저 로그인을 마칠 수 없습니다 — ${where} 화면의 터미널에서 같은 명령을 실행하면 브라우저가 바로 열립니다(붙여넣기 없이). 지금은 아래 코드로 로그인합니다.`;
9
+ }
10
+ if (env.DISPLAY || env.WAYLAND_DISPLAY) return null;
11
+ return ssh
12
+ ? '원격(ssh) 접속이고 화면이 없어 브라우저를 열 수 없습니다 — 아무 기기의 브라우저에서 아래 주소를 열고 코드를 넣으세요.'
13
+ : '화면(디스플레이)이 없는 세션이라 브라우저를 열 수 없습니다 — 아무 기기의 브라우저에서 아래 주소를 열고 코드를 넣으세요.';
14
+ }
@@ -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,
@@ -13,6 +13,8 @@ import {
13
13
  } from './codex-account.js';
14
14
  import { authStorePath, defaultCodexHome, listProviders, loadTokens, saveTokens } from './store.js';
15
15
  import { readFreshAvailabilityState, quotaSignalDir, readQuotaSignal, readQuotaSignalCredits, readQuotaSignalUsedPercent, readQuotaSignalObservedAt, readQuotaSignalObservedAtRaw } from '../budget/codex-reset-credit-state.js';
16
+ import { creditPaceStatus } from '../budget/codex-credit-pace.js';
17
+ import { loadLlmPolicy } from '../policy/llm-policy.js';
16
18
  import { getUserConfig } from '../user-config.js';
17
19
  import {
18
20
  decideCodexRotation, observeRotation, applyRotation, readCodexAccountRotationConfig, rotatedChildEnv,
@@ -278,6 +280,13 @@ function buildRotationCandidates(path: string, now: number, env: NodeJS.ProcessE
278
280
  .filter((c): c is RotationCandidate => c !== null);
279
281
  }
280
282
 
283
+ function rotationCreditPace(candidates: readonly RotationCandidate[], now: number) {
284
+ const policy = loadLlmPolicy({ now: new Date(now) }).policy;
285
+ const balances = Object.fromEntries(candidates.filter((c) => c.creditBalance !== undefined)
286
+ .map((c) => [c.name, c.creditBalance!]));
287
+ return creditPaceStatus({ policy, balances, now: new Date(now) });
288
+ }
289
+
281
290
  function formatUsedPercent(value: number | undefined): string {
282
291
  return value === undefined ? 'unknown' : `${value}%`;
283
292
  }
@@ -517,7 +526,7 @@ export function inspectCodexRotation(
517
526
  const rawThreshold = rotationThresholdFromConfig();
518
527
  const thresholdPercentByAccount = rotationThresholdsByAccountFromConfig();
519
528
  const decision = decideCodexRotation({
520
- current, explicit, enabled, disabledProvenance: rotationConfig.state, currentReached, currentUsedPercent, accountOrder: accountOrderFromConfig(), resetCreditAvailability: resetCreditAvailability(currentHome, now), thresholdPercent: rawThreshold, thresholdPercentByAccount, creditsAllowed: codexCreditsAllowedFromConfig(), ...currentCredits, candidates,
529
+ current, explicit, enabled, disabledProvenance: rotationConfig.state, currentReached, currentUsedPercent, accountOrder: accountOrderFromConfig(), resetCreditAvailability: resetCreditAvailability(currentHome, now), thresholdPercent: rawThreshold, thresholdPercentByAccount, creditsAllowed: codexCreditsAllowedFromConfig(), ...currentCredits, candidates, creditPace: rotationCreditPace(candidates, now),
521
530
  });
522
531
  // ⛔⭐ 나이는 «만료돼도» 낸다 — 「65분 전」과 「3일 전」은 다른 진단이다(리뷰 must-fix).
523
532
  const observedAtByHome: Record<string, number> = {};
@@ -603,6 +612,7 @@ export function resolveCodexAccountForRun(
603
612
  const candidates = buildRotationCandidates(path, now, env);
604
613
  const currentSignalAttributable = signalAttributableToAccount(currentHomeInfo);
605
614
 
615
+ const creditPace = rotationCreditPace(candidates, now);
606
616
  const decision = decideCodexRotation({
607
617
  current,
608
618
  explicit,
@@ -617,6 +627,13 @@ export function resolveCodexAccountForRun(
617
627
  creditsAllowed: codexCreditsAllowedFromConfig(),
618
628
  ...currentCreditsInput(currentSignalAttributable ? currentHome : undefined, now),
619
629
  candidates,
630
+ creditPace,
631
+ });
632
+ debug.log('codex.rotation', 'credit-pace', {
633
+ active: creditPace.active,
634
+ todaySpent: creditPace.todaySpent,
635
+ target: creditPace.target,
636
+ to: decision.reason === 'credit-pace' ? decision.to?.name ?? current.name : decision.to?.name,
620
637
  });
621
638
  observeRotation(decision, current.name);
622
639
  const resolved = applyRotation(current, decision);
@@ -726,7 +743,7 @@ export function resolveRunFallback(
726
743
  try {
727
744
  const snapshot = inspectCodexRotation(env, deps.storePath ? { storePath: deps.storePath } : {});
728
745
  let rotation: RotationOutcome;
729
- if ((snapshot.reason === 'rotated' || snapshot.reason === 'reset-credit-unknown') && snapshot.to) {
746
+ if ((snapshot.reason === 'rotated' || snapshot.reason === 'reset-credit-unknown' || snapshot.reason === 'credit-pace') && snapshot.to) {
730
747
  // ⛔ 스냅샷은 이름만 준다 — 후보 목록에서 «그 객체»를 되찾는다(홈까지 필요하다).
731
748
  const to = snapshot.candidates.find((c) => c.name === snapshot.to);
732
749
  rotation = to ? { reason: snapshot.reason, to } : { reason: 'no-candidate' };
@@ -734,6 +751,7 @@ export function resolveRunFallback(
734
751
  snapshot.reason === 'explicit'
735
752
  || snapshot.reason === 'disabled'
736
753
  || snapshot.reason === 'not-reached'
754
+ || snapshot.reason === 'credit-pace'
737
755
  || snapshot.reason === 'reset-credit-available'
738
756
  || snapshot.reason === 'no-candidate'
739
757
  ) {
@@ -0,0 +1,173 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { createServer, type Server } from 'node:http';
3
+ import { spawn } from 'node:child_process';
4
+ import { debug } from '../debug/log.js';
5
+ import { CODEX_OAUTH_CLIENT_ID, CODEX_OAUTH_TOKEN_URL } from './codex.js';
6
+ import { expiresAtFromSeconds, saveTokens, type ProviderAuthState } from './store.js';
7
+
8
+ const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
9
+ const DEFAULT_PORT = 1455;
10
+ const TIMEOUT_MS = 5 * 60 * 1000;
11
+
12
+ export interface CodexBrowserLoginOpts {
13
+ fetchImpl?: typeof fetch;
14
+ openBrowser?: (url: string) => boolean | Promise<boolean>;
15
+ timeoutMs?: number;
16
+ preferredPort?: number;
17
+ mirrorCodex?: boolean;
18
+ }
19
+
20
+ export function browserLaunchCommand(url: string, platform: NodeJS.Platform = process.platform): { command: string; args: string[] } {
21
+ if (platform === 'win32') {
22
+ // Use the Windows URL handler directly; cmd.exe expands & and % even inside OAuth URLs.
23
+ return { command: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] };
24
+ }
25
+ return { command: platform === 'darwin' ? 'open' : 'xdg-open', args: [url] };
26
+ }
27
+
28
+ /** A launcher that exits non-zero this soon has failed; one still running (or exiting 0) handed the URL off. */
29
+ const LAUNCH_GRACE_MS = 1_000;
30
+
31
+ function openBrowser(url: string): Promise<boolean> {
32
+ const { command, args } = browserLaunchCommand(url);
33
+ return new Promise(resolve => {
34
+ let settled = false;
35
+ const settle = (ok: boolean) => { if (!settled) { settled = true; resolve(ok); } };
36
+ try {
37
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
38
+ child.once('error', () => settle(false));
39
+ child.once('exit', code => settle(code === 0));
40
+ child.once('spawn', () => {
41
+ const grace = setTimeout(() => { child.unref(); settle(true); }, LAUNCH_GRACE_MS);
42
+ grace.unref?.();
43
+ child.once('exit', () => clearTimeout(grace));
44
+ });
45
+ } catch { settle(false); }
46
+ });
47
+ }
48
+
49
+ function listen(server: Server, port: number): Promise<void> {
50
+ return new Promise((resolve, reject) => {
51
+ server.once('error', reject);
52
+ server.listen(port, '127.0.0.1', () => {
53
+ server.off('error', reject);
54
+ resolve();
55
+ });
56
+ });
57
+ }
58
+
59
+ /** Local OAuth callback, independent of a bundled or PATH Codex executable. */
60
+ export async function loginWithCodexBrowser(opts: CodexBrowserLoginOpts = {}): Promise<ProviderAuthState> {
61
+ const verifier = randomBytes(32).toString('base64url');
62
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
63
+ const state = randomBytes(32).toString('base64url');
64
+ let port = opts.preferredPort ?? DEFAULT_PORT;
65
+ let resolveCallback!: (value: ProviderAuthState) => void;
66
+ let rejectCallback!: (reason: Error) => void;
67
+ const callback = new Promise<ProviderAuthState>((resolve, reject) => {
68
+ resolveCallback = resolve;
69
+ rejectCallback = reject;
70
+ });
71
+ void callback.catch(() => {});
72
+ let settled = false;
73
+ let handling = false;
74
+ const tokenAbort = new AbortController();
75
+ const server = createServer((req, res) => {
76
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
77
+ if (req.method !== 'GET' || url.pathname !== '/auth/callback') {
78
+ res.writeHead(404).end();
79
+ return;
80
+ }
81
+ debug.log('oauth.codex-browser', 'callback', { port });
82
+ if (handling || settled) { res.writeHead(409).end(); return; }
83
+ if (url.searchParams.get('state') !== state) {
84
+ res.writeHead(400).end('Invalid login state.');
85
+ return;
86
+ }
87
+ const code = url.searchParams.get('code');
88
+ if (!code || url.searchParams.has('error')) {
89
+ res.writeHead(400).end('Login failed. Return to the terminal.');
90
+ rejectCallback(new Error('authorization-denied'));
91
+ return;
92
+ }
93
+ handling = true;
94
+ void (async () => {
95
+ try {
96
+ const response = await (opts.fetchImpl ?? fetch)(CODEX_OAUTH_TOKEN_URL, {
97
+ method: 'POST',
98
+ signal: tokenAbort.signal,
99
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
100
+ body: new URLSearchParams({
101
+ grant_type: 'authorization_code', code, redirect_uri: `http://localhost:${port}/auth/callback`,
102
+ client_id: CODEX_OAUTH_CLIENT_ID, code_verifier: verifier,
103
+ }).toString(),
104
+ });
105
+ if (!response.ok) throw new Error('token-exchange');
106
+ const token = await response.json() as {
107
+ access_token?: string; refresh_token?: string; id_token?: string;
108
+ expires_in?: number; scope?: string; token_type?: string;
109
+ };
110
+ if (!token.access_token || !token.refresh_token) throw new Error('token-exchange');
111
+ if (settled) { res.writeHead(410).end(); return; }
112
+ const saved = saveTokens('openai-codex', {
113
+ accessToken: token.access_token,
114
+ refreshToken: token.refresh_token,
115
+ ...(token.id_token ? { idToken: token.id_token } : {}),
116
+ expiresAt: expiresAtFromSeconds(token.expires_in),
117
+ scope: token.scope,
118
+ tokenType: token.token_type ?? 'Bearer',
119
+ }, { authMode: 'chatgpt', mirrorCodex: opts.mirrorCodex });
120
+ debug.log('oauth.codex-browser', 'exchanged', { port });
121
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
122
+ .end('<!doctype html><html lang="ko"><meta charset="utf-8"><body>로그인됨 — 터미널로 돌아가세요</body></html>');
123
+ resolveCallback(saved);
124
+ } catch {
125
+ res.writeHead(400).end('Login failed. Return to the terminal.');
126
+ rejectCallback(new Error('token-exchange'));
127
+ }
128
+ })();
129
+ });
130
+ let timer: ReturnType<typeof setTimeout> | undefined;
131
+ const cancel = () => rejectCallback(new Error('cancelled'));
132
+ const onExit = () => server.close();
133
+ const onServerError = () => rejectCallback(new Error('port-unavailable'));
134
+ let listening = false;
135
+ try {
136
+ try { await listen(server, port); }
137
+ catch (err) {
138
+ if ((err as NodeJS.ErrnoException).code !== 'EADDRINUSE' || port === 0) throw err;
139
+ await listen(server, 0);
140
+ }
141
+ const address = server.address();
142
+ if (!address || typeof address === 'string') throw new Error('port-unavailable');
143
+ listening = true;
144
+ port = address.port;
145
+ server.on('error', onServerError);
146
+ process.once('SIGINT', cancel);
147
+ process.once('exit', onExit);
148
+ timer = setTimeout(() => rejectCallback(new Error('timeout')), opts.timeoutMs ?? TIMEOUT_MS);
149
+ const authorize = new URL(AUTHORIZE_URL);
150
+ for (const [key, value] of Object.entries({
151
+ client_id: CODEX_OAUTH_CLIENT_ID, redirect_uri: `http://localhost:${port}/auth/callback`,
152
+ response_type: 'code', code_challenge: challenge, code_challenge_method: 'S256', state,
153
+ scope: 'openid profile email offline_access', id_token_add_organizations: 'true',
154
+ codex_cli_simplified_flow: 'true', originator: 'codex_cli_rs',
155
+ })) authorize.searchParams.set(key, value);
156
+ debug.log('oauth.codex-browser', 'started', { port });
157
+ if (!await (opts.openBrowser ?? openBrowser)(authorize.toString())) throw new Error('browser-unavailable');
158
+ return await callback;
159
+ } catch (err) {
160
+ const reason = err instanceof Error && ['timeout', 'cancelled', 'browser-unavailable', 'authorization-denied', 'token-exchange'].includes(err.message)
161
+ ? err.message : 'port-unavailable';
162
+ debug.log('oauth.codex-browser', 'failed', { port, reason });
163
+ throw new Error(`codex browser login failed: ${reason}`);
164
+ } finally {
165
+ settled = true;
166
+ tokenAbort.abort();
167
+ if (timer) clearTimeout(timer);
168
+ process.off('SIGINT', cancel);
169
+ process.off('exit', onExit);
170
+ if (listening) server.close();
171
+ server.off('error', onServerError);
172
+ }
173
+ }
@@ -53,3 +53,19 @@ export function codexPolicyAllowsCredits(policy: CodexQuotaPolicy): boolean {
53
53
  export function codexPolicyAllowsFallback(policy: CodexQuotaPolicy): boolean {
54
54
  return policy !== 'within-quota';
55
55
  }
56
+
57
+ /** POL1 — the one line a harness launch prints about the codex quota policy it will use, and a loud warning when a
58
+ * test universe disagrees with production (10-01: work-tree launches resolved «fallback» and leaked to grok). */
59
+ export function describeLaunchQuotaPolicy(input: {
60
+ current: ResolvedCodexQuotaPolicy;
61
+ universe: { kind: 'prod' | 'test'; root: string };
62
+ production?: ResolvedCodexQuotaPolicy;
63
+ }): { line: string; warning?: string } {
64
+ const { current, universe, production } = input;
65
+ const line = `[pod] codex 한도 정책 = ${current.policy}(${CODEX_QUOTA_POLICY_LABEL[current.policy]}) · 출처 ${current.source} · 우주 ${universe.kind} ${universe.root}`;
66
+ if (universe.kind === 'test' && production && production.policy !== current.policy) {
67
+ return { line, warning: `⚠️ 이 시험 우주의 한도 정책(${current.policy})이 운영(${production.policy})과 다르다 — 운영과 같게 하려면: bun bin/elanous.mjs config sync-test` };
68
+ }
69
+ return { line };
70
+ }
71
+
@@ -121,6 +121,8 @@ interface UserCodeResponse {
121
121
  device_auth_id: string;
122
122
  interval?: number;
123
123
  expires_in?: number;
124
+ verification_uri_complete?: string;
125
+ verification_uri?: string;
124
126
  }
125
127
 
126
128
  interface PollSuccessResponse {
@@ -278,7 +280,8 @@ export async function loginWithCodex(opts: CodexLoginOpts = {}): Promise<Provide
278
280
  const userCode = await requestDeviceCode({ fetchImpl });
279
281
  const serverInterval = typeof userCode.interval === 'number' ? userCode.interval * 1000 : 0;
280
282
  const pollInterval = Math.max(reportedInterval, serverInterval || 0);
281
- onProgress({ type: 'user_code', userCode: userCode.user_code, loginUrl: CODEX_DEVICE_LOGIN_URL });
283
+ onProgress({ type: 'user_code', userCode: userCode.user_code,
284
+ loginUrl: userCode.verification_uri_complete || userCode.verification_uri || CODEX_DEVICE_LOGIN_URL });
282
285
 
283
286
  // 2) Poll until ready or timeout.
284
287
  const startedAt = Date.now();
@@ -69,8 +69,8 @@ export function normalizeFallbackChain(
69
69
  /** 회전 판정이 낸 이유 — `codex-account-rotation.ts` 의 `RotationReason` 과 같은 어휘.
70
70
  * ⛔ 그쪽 타입이 비공개라 구조적으로 받는다(그 파일의 리뷰 규율: dead export 금지). */
71
71
  export type RotationOutcome =
72
- | { readonly reason: 'rotated' | 'reset-credit-unknown'; readonly to: RotationCandidate }
73
- | { readonly reason: 'explicit' | 'disabled' | 'not-reached' | 'reset-credit-available' | 'no-candidate' };
72
+ | { readonly reason: 'rotated' | 'reset-credit-unknown' | 'credit-pace'; readonly to: RotationCandidate }
73
+ | { readonly reason: 'explicit' | 'disabled' | 'not-reached' | 'credit-pace' | 'reset-credit-available' | 'no-candidate' };
74
74
 
75
75
  export type FallbackDecision =
76
76
  /** codex 계정을 갈아탄다(종전 동작). */
@@ -156,14 +156,14 @@ export function decideFallback(input: FallbackInput): FallbackDecision {
156
156
  return stepAfter(chain, currentStep, grokAvailable, grokQuota, 'chain-exhausted');
157
157
  }
158
158
  if (rotation.reason === 'explicit') return { action: 'stay', why: 'explicit' };
159
- if (rotation.reason === 'not-reached') {
159
+ if (rotation.reason === 'not-reached' || (rotation.reason === 'credit-pace' && !('to' in rotation))) {
160
160
  return { action: 'stay', why: 'not-reached' };
161
161
  }
162
162
  if (rotation.reason === 'reset-credit-available' && stayOnResetCreditAvailable) {
163
163
  return { action: 'stay', why: 'reset-credit-available' };
164
164
  }
165
165
 
166
- if (rotation.reason === 'rotated' || rotation.reason === 'reset-credit-unknown') {
166
+ if (rotation.reason === 'rotated' || rotation.reason === 'reset-credit-unknown' || (rotation.reason === 'credit-pace' && 'to' in rotation)) {
167
167
  // 회전이 답을 냈는데 체인이 codex-rotate 를 «빼» 놨다면 그 뜻을 존중하고
168
168
  // 다음 칸으로 간다(회전을 원치 않는 구성).
169
169
  if (chain.includes('codex-rotate')) return { action: 'codex-rotate', to: rotation.to };
@@ -172,7 +172,7 @@ export function decideFallback(input: FallbackInput): FallbackDecision {
172
172
 
173
173
  // no-candidate | disabled | reset-credit-available(기본) — codex 축이 끝났다. 다음 칸을 본다.
174
174
  // ⛔ 다음 칸이 «없을 때»의 이름만 고른다. 진행(stepAfterCodex)은 그대로다.
175
- const fallbackWhy = stayReasonWhenCodexAxisEnds(rotation.reason);
175
+ const fallbackWhy = stayReasonWhenCodexAxisEnds(rotation.reason === 'credit-pace' ? 'no-candidate' : rotation.reason);
176
176
  return stepAfterCodex(chain, grokAvailable, grokQuota, fallbackWhy);
177
177
  }
178
178
 
@@ -51,7 +51,9 @@ export interface OAuthTokens {
51
51
  tokenType?: string;
52
52
  }
53
53
 
54
- export type CodexMirrorResult = 'written' | 'not-created-missing-cli-fields' | 'write-failed';
54
+ export type CodexMirrorResult = 'written' | 'not-created-missing-cli-fields' | 'write-failed'
55
+ // AUTH1 — ~/.codex is the Codex CLI's file: never mix another account's tokens into it.
56
+ | 'skipped-different-account' | 'skipped-unknown-account';
55
57
 
56
58
  export interface ProviderAuthState {
57
59
  tokens: OAuthTokens;
@@ -245,6 +247,9 @@ export interface WriteTokensOpts {
245
247
  /** Default true for 'openai-codex'. Set false to skip the ~/.codex
246
248
  * mirror (e.g. in tests that shouldn't touch the user's home). */
247
249
  mirrorCodex?: boolean;
250
+ /** AUTH1 — when the Codex CLI file belongs to a different account, replace it wholly with this
251
+ * login (id_token · account_id · tokens) instead of leaving it. Only on explicit user consent. */
252
+ replaceDifferentCodexAccount?: boolean;
248
253
  /** Explicit ChatGPT claims override. Normally left undefined — for
249
254
  * 'openai-codex' with OAuth tokens, saveTokens auto-decodes the
250
255
  * access-token JWT so every refresh site picks up fresh claims
@@ -296,7 +301,10 @@ export function saveTokens(
296
301
  ?? (provider === 'openai-codex' ? defaultCodexHome(process.env) : undefined);
297
302
  if (home) {
298
303
  // loginWithCodex → saveTokens reaches the Codex CLI mirror on each token save.
299
- state.codexMirrorResult = mirrorCodexAuth(tokens, state.lastRefresh, join(home, 'auth.json'));
304
+ state.codexMirrorResult = mirrorCodexAuth(tokens, state.lastRefresh, join(home, 'auth.json'), {
305
+ knownAccountId: state.chatGPT?.accountId,
306
+ replaceDifferentAccount: opts.replaceDifferentCodexAccount === true,
307
+ });
300
308
  }
301
309
  // ⛔ 이름 계정인데 홈을 모르면 «미러하지 않는다» — 모르는 곳에 토큰을 쓰지 않는다
302
310
  }
@@ -377,7 +385,12 @@ export function assertMirrorTargetIsSafeUnderTest(mirrorPath: string): void {
377
385
  * under `oauth.codex-mirror`. ⛔ The 2026-08-05 incident had to be traced by
378
386
  * guessing from the file's `last_refresh` FORMAT, which is deduction, not
379
387
  * observation. Token values are never logged — path + error kind only. */
380
- function mirrorCodexAuth(tokens: OAuthTokens, lastRefresh: string, mirrorPath: string = codexAuthPath()): CodexMirrorResult {
388
+ function mirrorCodexAuth(
389
+ tokens: OAuthTokens,
390
+ lastRefresh: string,
391
+ mirrorPath: string = codexAuthPath(),
392
+ account: { knownAccountId?: string; replaceDifferentAccount?: boolean } = {},
393
+ ): CodexMirrorResult {
381
394
  assertMirrorTargetIsSafeUnderTest(mirrorPath);
382
395
  try {
383
396
  const path = mirrorPath;
@@ -400,21 +413,45 @@ function mirrorCodexAuth(tokens: OAuthTokens, lastRefresh: string, mirrorPath: s
400
413
  && tokens.idToken.length > 0
401
414
  && typeof accountId === 'string'
402
415
  && accountId.length > 0;
416
+ // AUTH1 — the file is the Codex CLI's. Same account: rotate tokens as before. Different account
417
+ // (or ours unknown): leave it untouched unless the user asked to replace it — the old «hasBase»
418
+ // path kept the file's id_token/account_id and wrote our tokens next to them (two accounts mixed).
419
+ // Our account: id_token claim → stored claim → the access token's own claim. Refresh responses
420
+ // carry no id_token, and named rotation accounts keep their CLI homes in sync through this path —
421
+ // so an unknown account keeps the old rotation behaviour; only «both known and different» is refused.
422
+ const ourAccount = (typeof accountId === 'string' && accountId) || account.knownAccountId
423
+ || extractChatGPTClaims(tokens.accessToken)?.accountId || undefined;
424
+ const baseTokens = (existing.tokens && typeof existing.tokens === 'object') ? existing.tokens as Record<string, unknown> : {};
425
+ const baseAccount = typeof baseTokens.account_id === 'string' && baseTokens.account_id ? baseTokens.account_id : undefined;
426
+ let replaceWholly = false;
427
+ if (hasBase && baseAccount && !ourAccount) debug.log('oauth.codex-mirror', 'account-unknown', { path });
428
+ if (hasBase && baseAccount && ourAccount) {
429
+ if (ourAccount !== baseAccount) {
430
+ if (!account.replaceDifferentAccount || !canCreateUsableMirror) {
431
+ debug.log('oauth.codex-mirror', 'skipped-different-account', { path, replaceRequested: account.replaceDifferentAccount === true });
432
+ return 'skipped-different-account';
433
+ }
434
+ replaceWholly = true;
435
+ }
436
+ }
437
+
403
438
  if (!hasBase && !canCreateUsableMirror) {
404
439
  debug.log('oauth.codex-mirror', 'not-created-missing-cli-fields', { path });
405
440
  return 'not-created-missing-cli-fields';
406
441
  }
407
442
 
408
443
  mkdirSync(dirname(path), { recursive: true });
444
+ if (replaceWholly) existing = {};
445
+ const fresh = !hasBase || replaceWholly;
409
446
  const priorTokens = (existing.tokens && typeof existing.tokens === 'object')
410
447
  ? existing.tokens as Record<string, unknown> : {};
411
448
  existing.tokens = {
412
449
  ...priorTokens,
413
- ...(hasBase ? {} : { id_token: tokens.idToken, account_id: accountId }),
450
+ ...(fresh ? { id_token: tokens.idToken, account_id: accountId } : {}),
414
451
  access_token: tokens.accessToken,
415
452
  refresh_token: tokens.refreshToken,
416
453
  };
417
- if (!hasBase) {
454
+ if (fresh) {
418
455
  existing.OPENAI_API_KEY = null;
419
456
  existing.auth_mode = 'chatgpt';
420
457
  }