elanous 0.2.15 → 0.2.17

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 (309) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
  55. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  56. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +15 -15
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +15 -15
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +15 -15
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +15 -15
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +15 -15
  67. package/apps/pwa/out/ceo/index.html +1 -1
  68. package/apps/pwa/out/ceo/index.txt +15 -15
  69. package/apps/pwa/out/chat/index.html +1 -1
  70. package/apps/pwa/out/chat/index.txt +15 -15
  71. package/apps/pwa/out/consult/index.html +1 -1
  72. package/apps/pwa/out/consult/index.txt +15 -15
  73. package/apps/pwa/out/control/index.html +1 -1
  74. package/apps/pwa/out/control/index.txt +15 -15
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +15 -15
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +15 -15
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +15 -15
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +15 -15
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +15 -15
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +15 -15
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +15 -15
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +15 -15
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +15 -15
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +15 -15
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +15 -15
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +15 -15
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +15 -15
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +15 -15
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +15 -15
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +16 -16
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +15 -15
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +15 -15
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +15 -15
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +15 -15
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +15 -15
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +15 -15
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +15 -15
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +15 -15
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +15 -15
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +15 -15
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +15 -15
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +15 -15
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +15 -15
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +15 -15
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +15 -15
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +15 -15
  146. package/graphs/doc-gen/doc-gen.yaml +24 -0
  147. package/graphs/doc-gen/recipes.yaml +3 -0
  148. package/graphs/intake/intake-daily.yaml +9 -2
  149. package/graphs/intake/news-sources.yaml +4 -0
  150. package/graphs/intake/recipes.yaml +6 -1
  151. package/graphs/orchestrator/orchestrator.yaml +7 -0
  152. package/graphs/orchestrator/recipes.yaml +3 -0
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/__fixtures__/intake-news.xml +7 -0
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +34 -15
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/goals/goal-census.ts +343 -0
  163. package/scripts/intake-news.ts +169 -0
  164. package/scripts/measure/author-section-contribution.ts +84 -0
  165. package/scripts/release-loop/cut-branch.ts +43 -13
  166. package/scripts/release-loop/docs-land-node.ts +69 -10
  167. package/scripts/release-loop/gate-node.ts +3 -1
  168. package/scripts/release-loop/publish-node.ts +2 -2
  169. package/scripts/release-loop/release-docs.ts +134 -0
  170. package/scripts/release-loop/vault-note-node.ts +19 -2
  171. package/scripts/release-loop/version-node.ts +7 -4
  172. package/scripts/release-story/draft.ts +42 -14
  173. package/scripts/rhythm/monthly.ts +97 -0
  174. package/src/acp/client.ts +17 -2
  175. package/src/agent-mission/driver.ts +104 -11
  176. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  177. package/src/agent-mission/supervisor-metrics.ts +218 -0
  178. package/src/autopilot/track-agent-shadow.ts +2 -1
  179. package/src/autopilot/track-agent.ts +5 -3
  180. package/src/briefing/brief-items.ts +191 -36
  181. package/src/budget/usage-outcomes.ts +204 -0
  182. package/src/cli/autopilot-cli.ts +66 -0
  183. package/src/cli/brief-cli.ts +73 -0
  184. package/src/cli/decisions-cli.ts +4 -1
  185. package/src/cli/intake-cli.ts +1 -1
  186. package/src/cli/know-cli.ts +20 -0
  187. package/src/cli/log-event-names.ts +1 -0
  188. package/src/cli/logs-cli.ts +43 -1
  189. package/src/cli/pr-cli.ts +92 -13
  190. package/src/cli/provider-cli.ts +14 -0
  191. package/src/cli/release-cli.ts +11 -8
  192. package/src/cli/self-update-canary.ts +46 -0
  193. package/src/cli/self-update.ts +399 -1
  194. package/src/cli/usage-cli.ts +26 -0
  195. package/src/context-card/index.ts +45 -0
  196. package/src/context-card/task.ts +76 -0
  197. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  198. package/src/decisions/decision-ledger.ts +10 -1
  199. package/src/discord-self-message.ts +9 -0
  200. package/src/flow-loop/split.ts +24 -0
  201. package/src/harness/harness-cli-command.ts +178 -12
  202. package/src/harness/harness-pod-dispatch.ts +36 -55
  203. package/src/harness/harness-queue.ts +86 -7
  204. package/src/harness/harness-seams.ts +13 -3
  205. package/src/harness/helper-repair.ts +139 -8
  206. package/src/harness/helper-scan.ts +13 -2
  207. package/src/harness/nested-elanous-policy.ts +133 -0
  208. package/src/hq/fence-wrapper.ts +123 -0
  209. package/src/hq/hq.ts +16 -2
  210. package/src/hq/lease.ts +5 -3
  211. package/src/hq/transfer.ts +185 -0
  212. package/src/index.ts +168 -13
  213. package/src/intake-plane/digest.ts +85 -28
  214. package/src/knowledge/know-find.ts +105 -0
  215. package/src/llm.ts +75 -19
  216. package/src/loops/orchestrator/finish-rate.ts +14 -3
  217. package/src/loops/orchestrator/tick.ts +96 -25
  218. package/src/maturity/bot-slash-catalog.ts +11 -2
  219. package/src/maturity/feature-maturity.ts +2 -2
  220. package/src/maturity/tui-slash-availability.ts +9 -0
  221. package/src/nexus/api/grid.ts +61 -0
  222. package/src/nexus/api/http-server.ts +19 -1
  223. package/src/nexus/api/task-cards-api.ts +19 -0
  224. package/src/oauth/codex.ts +82 -2
  225. package/src/oauth/overload-failover-launch.ts +75 -0
  226. package/src/oauth/overload-failover.ts +137 -0
  227. package/src/release-loop/checklist.ts +10 -5
  228. package/src/release-loop/feature-store.ts +10 -9
  229. package/src/release-loop/landing-freeze.ts +18 -10
  230. package/src/release-loop/placement.ts +17 -8
  231. package/src/seat-dispatch/seat-ask.ts +33 -6
  232. package/src/seat-loop/seat-loop.ts +172 -21
  233. package/src/self-dev/ask-launch-flow.ts +48 -8
  234. package/src/self-dev/authored-goal-trace.ts +64 -0
  235. package/src/self-dev/dev-cli.ts +19 -1
  236. package/src/self-dev/dev-pipeline.ts +6 -5
  237. package/src/self-dev/entrance-registry.ts +10 -6
  238. package/src/self-dev/old-door.ts +73 -0
  239. package/src/self-dev/run-store.ts +5 -1
  240. package/src/self-dev/run-supervisor.ts +7 -1
  241. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  242. package/src/self-implement/app-api-budget.ts +166 -0
  243. package/src/self-implement/budget-gate.ts +7 -5
  244. package/src/self-implement/follow-up-goals.ts +289 -0
  245. package/src/self-implement/frozen-merges.ts +25 -20
  246. package/src/self-implement/goal-author-clarification.ts +81 -0
  247. package/src/self-implement/goal-author-cli.ts +33 -5
  248. package/src/self-implement/goal-author.ts +107 -25
  249. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  250. package/src/self-implement/goal-document-write.ts +28 -0
  251. package/src/self-implement/heal-intake.ts +4 -0
  252. package/src/self-implement/heal-triage.ts +44 -0
  253. package/src/self-implement/host-regate.ts +3 -1
  254. package/src/self-implement/orchestrator.ts +157 -8
  255. package/src/self-implement/run-ledger.ts +4 -0
  256. package/src/self-implement/seams.ts +44 -15
  257. package/src/session-runtime/index.ts +2 -0
  258. package/src/session-runtime/retry-api.ts +87 -0
  259. package/src/session-runtime/retry-policy.ts +107 -6
  260. package/src/steward/working-backwards.ts +16 -1
  261. package/src/task-agent/actions.ts +118 -0
  262. package/src/task-agent/judge.ts +96 -0
  263. package/src/task-cards/card-cli.ts +25 -0
  264. package/src/task-cards/card-store.ts +9 -4
  265. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
  266. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  267. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  268. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  269. package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
  270. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  271. package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
  272. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  273. package/src/telegram-commands.ts +6 -1
  274. package/src/telegram-loops-command.ts +21 -14
  275. package/src/telegram.ts +47 -0
  276. package/src/user-config.ts +59 -4
  277. package/src/version/packed-revision.json +1 -1
  278. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  305. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  306. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  307. package/src/launch-head/seat-goal-queue.ts +0 -198
  308. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
  309. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
@@ -8,6 +8,15 @@ export interface TuiSlashAvailability {
8
8
  discord: boolean;
9
9
  }
10
10
 
11
+ export function formatBotUnavailableSlashReply(name: string, surface: 'telegram' | 'discord'): string {
12
+ return `/${name}은(는) ${surface === 'telegram' ? '텔레그램' : '디스코드'}에서 아직 지원되지 않습니다. TUI에서 /${name}을(를) 사용하세요.`;
13
+ }
14
+
15
+ export function botUnavailableSlashReply(entry: TuiSlashAvailability | undefined, surface: 'telegram' | 'discord'): string | null {
16
+ if (!entry || entry[surface] || entry.maturity === 'system') return null;
17
+ return formatBotUnavailableSlashReply(entry.name, surface);
18
+ }
19
+
11
20
  /** Project canonical TUI commands onto the maturity map and bot-surface grade maps.
12
21
  * An absent bot grade means unavailable; a missing TUI grade is a registry error.
13
22
  */
@@ -0,0 +1,61 @@
1
+ import { getUserConfig } from '../../user-config.js';
2
+ import { readHqLease, type HqDeps } from '../../hq/hq.js';
3
+ import { measurePoolLease, type PoolLeaseMeasure } from '../../task-orchestrator/surfaces/pod-lease.js';
4
+ import { parsePodPool, resolvePodPoolSpec, type PodPoolMember, type PoolKubectl } from '../../task-orchestrator/surfaces/pod-pool.js';
5
+
6
+ export interface GridData {
7
+ /** null means the arbiter could not be read, not that no lease exists. */
8
+ hq: { record: ReturnType<typeof readHqLease>['record']; ageSeconds: number | null; expired: boolean | null; reason: string | null };
9
+ /** Configured member order; capacity comes from measurePoolLease when available. occupied is running + pending, or null on measurement failure. */
10
+ members: Array<{ context: string; capacity: number; running: number | null; pending: number | null; occupied: number | null; reason: string | null }>;
11
+ poolReason: string | null;
12
+ }
13
+
14
+ export interface GridDeps {
15
+ hq?: HqDeps;
16
+ readHq?: () => ReturnType<typeof readHqLease>;
17
+ poolSpec?: () => string | null;
18
+ /** Optional read-only kubectl seam; never use a probe that creates Pods. */
19
+ kubectl?: PoolKubectl;
20
+ measure?: (members: readonly PodPoolMember[]) => PoolLeaseMeasure;
21
+ }
22
+
23
+ /** Independent, fail-soft read snapshots. No lease status/heartbeat, DNS probe Pod, host reservation or writes. */
24
+ export function buildGridData(deps: GridDeps = {}): GridData {
25
+ let hq: GridData['hq'];
26
+ try {
27
+ hq = { ...(deps.readHq ?? (() => readHqLease(deps.hq)))(), reason: null };
28
+ } catch (error) {
29
+ hq = { record: null, ageSeconds: null, expired: null, reason: String(error) };
30
+ }
31
+
32
+ let members: GridData['members'] = [];
33
+ let poolReason: string | null = null;
34
+ try {
35
+ const spec = (deps.poolSpec ?? (() => resolvePodPoolSpec(undefined, process.env, () => {
36
+ const config = getUserConfig();
37
+ return config.harness?.podPool ?? config.pod?.pool;
38
+ })))();
39
+ if (spec) {
40
+ const configured = parsePodPool(spec);
41
+ try {
42
+ const measured = (deps.measure ?? ((pool) => measurePoolLease(pool, { skipDnsProbe: true, ...(deps.kubectl ? { kubectl: deps.kubectl } : {}) })))(configured);
43
+ const byContext = new Map(measured.members.map((m) => [m.context, m]));
44
+ members = configured.map((member) => {
45
+ const row = byContext.get(member.context);
46
+ const running = row?.running ?? null;
47
+ const pending = row?.pending ?? null;
48
+ return { context: member.context, capacity: row?.capacity ?? member.capacity, running, pending,
49
+ occupied: running === null || pending === null ? null : running + pending,
50
+ reason: row?.reason ?? (row ? null : '측정 불가') };
51
+ });
52
+ } catch (error) {
53
+ poolReason = String(error);
54
+ members = configured.map(({ context, capacity }) => ({ context, capacity, running: null, pending: null, occupied: null, reason: poolReason }));
55
+ }
56
+ } else poolReason = 'pool not configured';
57
+ } catch (error) {
58
+ poolReason = String(error);
59
+ }
60
+ return { hq, members, poolReason };
61
+ }
@@ -22,6 +22,7 @@ import { spawnSync } from 'node:child_process';
22
22
  import { isAbsolute } from 'node:path';
23
23
  import { createProject, listProjects } from '../../project/project-store.js';
24
24
  import { handleOutputsGet, type OutputsDeps } from './outputs-route.js';
25
+ import { buildGridData, type GridDeps } from './grid.js';
25
26
  import { DECISIONS_PATH, handleDecisions, type DecisionsRouteDeps } from './decisions-route.js';
26
27
  import { CARD_FOLLOWUP_PATH, createCardFollowupJobs, type CardFollowupJobs } from './card-followup-route.js';
27
28
  import { SKILL_EXEC_PATH, createSkillExecJobs, type SkillExecJobs } from './skill-exec-route.js';
@@ -341,7 +342,7 @@ import {
341
342
  handleTasksList,
342
343
  handleTaskDetail,
343
344
  } from './tasks-scheduler.js';
344
- import { handleTaskCardsGet, handleTaskCardsWishPost } from './task-cards-api.js';
345
+ import { handleTaskCardsGet, handleTaskCardsWishPost, handleTaskCardsClosePost } from './task-cards-api.js';
345
346
  import { handleTaskCreatePost, handleTaskApprovePost } from './tasks-create.js';
346
347
  import { matchIngestAuthorization } from './ingest-token.js';
347
348
  import {
@@ -478,6 +479,8 @@ export interface NexusHttpServerOpts {
478
479
  /** Optional installer state root for isolated API consumers and tests. */
479
480
  pluginStateRoot?: string;
480
481
  outputs?: OutputsDeps;
482
+ /** Read-only grid measurement seam; omitted in production for live HQ and Pod pool reads. */
483
+ grid?: GridDeps;
481
484
  decisions?: Pick<DecisionsRouteDeps, 'ledger'>;
482
485
  cardFollowup?: CardFollowupJobs;
483
486
  skillExec?: SkillExecJobs;
@@ -1385,6 +1388,12 @@ export async function routeRequest(
1385
1388
  });
1386
1389
  }
1387
1390
 
1391
+ // routeRequest is the live GET-only caller of buildGridData; keep it behind the owner auth gate.
1392
+ if (pathname === '/v1/grid') {
1393
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1394
+ if (method !== 'GET') return jsonResponse({ error: 'method-not-allowed', method }, 405);
1395
+ return jsonResponse(buildGridData(opts.grid));
1396
+ }
1388
1397
  if (method === 'GET' && pathname === '/v1/outputs') {
1389
1398
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1390
1399
  return handleOutputsGet(req, opts.outputs);
@@ -1457,6 +1466,15 @@ export async function routeRequest(
1457
1466
  const mutated = await handleGraphsMutation(pathname, req);
1458
1467
  if (mutated) return mutated;
1459
1468
  }
1469
+ const cardClose = /^\/v1\/task-cards\/([^/]+)\/close$/.exec(pathname);
1470
+ if (method === 'POST' && cardClose) {
1471
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1472
+ if (bearerCredential(req, opts.metaApi) !== 'bearer-match') return jsonResponse({ error: 'forbidden' }, 403);
1473
+ let id: string;
1474
+ try { id = decodeURIComponent(cardClose[1]!); }
1475
+ catch { return jsonResponse({ error: 'not_found' }, 404); }
1476
+ return handleTaskCardsClosePost(req, id);
1477
+ }
1460
1478
  if (method === 'POST' && pathname === '/v1/task-cards/wish') {
1461
1479
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1462
1480
  return handleTaskCardsWishPost(req);
@@ -25,6 +25,25 @@ export async function handleTaskCardsWishPost(req: Request, root?: string): Prom
25
25
  }
26
26
  }
27
27
 
28
+ export async function handleTaskCardsClosePost(req: Request, id: string, root?: string): Promise<Response> {
29
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) return jsonResponse({ error: 'not_found' }, 404);
30
+ let body: unknown;
31
+ try { body = await req.json(); }
32
+ catch { return jsonResponse({ error: 'bad_request' }, 400); }
33
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
34
+ const { reason } = body as Record<string, unknown>;
35
+ if (typeof reason !== 'string' || !reason.trim() || reason.trim().includes('\n') || reason.trim().includes('\r')) {
36
+ return jsonResponse({ error: 'bad_request' }, 400);
37
+ }
38
+ const store = new CardStore(root);
39
+ try {
40
+ if (!store.getCard(id)) return jsonResponse({ error: 'not_found' }, 404);
41
+ return jsonResponse({ card: store.closeCard(id, reason) });
42
+ } finally {
43
+ store.close();
44
+ }
45
+ }
46
+
28
47
  export interface WishPlacement { cellId: string; cellTitle: string; version: string; status: 'green' | 'yellow' | 'red' | 'done' }
29
48
 
30
49
  export function isSplitCell(cell: unknown): cell is { id: string } {
@@ -24,9 +24,11 @@
24
24
  // the official CLI stays in sync.
25
25
 
26
26
  import { execFileSync } from 'node:child_process';
27
+ import { chmodSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
27
28
  import { join } from 'node:path';
28
- import { resolveCodexAccount, effectiveCodexHome, type CodexAccountResolution } from './codex-account.js';
29
- import { resolveCodexAccountForRun } from './codex-account-store.js';
29
+ import { DEFAULT_CODEX_ACCOUNT, codexStoreKey, isValidAccountName, resolveCodexAccount, effectiveCodexHome, type CodexAccountResolution } from './codex-account.js';
30
+ import { importCodexAccountFromHome, resolveCodexAccountForRun } from './codex-account-store.js';
31
+ import { decodeJWTPayload } from './jwt.js';
30
32
  import { authStorePath } from './store.js';
31
33
  import { platform, release, arch } from 'node:os';
32
34
  import {
@@ -533,3 +535,81 @@ export async function getCodexAccessToken(
533
535
  const state = await loadFreshCodexAuthState(opts);
534
536
  return state ? state.tokens : null;
535
537
  }
538
+
539
+ // ── Named-account host refresh (POD-TOKEN-PREREFRESH · 10-06) ─────────
540
+ //
541
+ // 🩸 10-06 05:3x team · 09:47 third — Pod launches refused a codex account whose access token had
542
+ // under 3 h left, and a human refreshed it by hand (refreshCodexTokens → atomic 0600 write → account
543
+ // import). This is that path as one function: backup → refresh → atomic write → canonical store via
544
+ // the same import path. ⛔ Token values never leave this function — the result carries hours only.
545
+
546
+ export type CodexAccountRefreshResult =
547
+ | { ok: true; storeKey: string; codexHome: string; beforeH: number | null; afterH: number | null; backupPath: string }
548
+ | { ok: false; kind: 'bad-name' | 'default-refused' | 'unknown-account' | 'unreadable' | 'no-tokens' | 'refresh-failed' | 'write-failed' | 'import-failed'; message: string };
549
+
550
+ /** Hours left on a JWT access token (one decimal) — null when the token carries no readable `exp`. */
551
+ export function codexAccessHoursLeft(accessToken: unknown, now: number = Date.now()): number | null {
552
+ if (typeof accessToken !== 'string') return null;
553
+ const exp = decodeJWTPayload(accessToken)?.exp;
554
+ return typeof exp === 'number' ? Math.round(((exp * 1000 - now) / 3_600_000) * 10) / 10 : null;
555
+ }
556
+
557
+ export async function refreshCodexAccountHome(
558
+ name: string,
559
+ opts: {
560
+ /** ⛔ Test isolation seam — default is the canonical store. */
561
+ storePath?: string;
562
+ /** ⛔ Test seam — tests inject a fake refresher; never the network. */
563
+ refresh?: (refreshToken: string) => Promise<TokenResponse>;
564
+ now?: () => number;
565
+ } = {},
566
+ ): Promise<CodexAccountRefreshResult> {
567
+ // ⛔ The CEO's default account (~/.codex) is shared with the official CLI — refreshing it here
568
+ // rotates the refresh token out from under that CLI. Named accounts only.
569
+ if (name === DEFAULT_CODEX_ACCOUNT) {
570
+ return { ok: false, kind: 'default-refused', message: `'${DEFAULT_CODEX_ACCOUNT}'(~/.codex) 는 대표 기본 계정이다 — 여기서 갱신하지 않는다` };
571
+ }
572
+ if (!isValidAccountName(name)) return { ok: false, kind: 'bad-name', message: `계정 이름이 부적격이다: ${name}` };
573
+ const storePath = opts.storePath ?? authStorePath();
574
+ const now = opts.now ?? Date.now;
575
+ const storeKey = codexStoreKey(name);
576
+ const codexHome = loadTokens(storeKey, storePath)?.codexHome;
577
+ if (!codexHome) return { ok: false, kind: 'unknown-account', message: `${storeKey} 계정이 없거나 codexHome 을 모른다 — 먼저 account import` };
578
+ const authPath = join(codexHome, 'auth.json');
579
+ let raw: string;
580
+ let parsed: { tokens?: Record<string, unknown> } & Record<string, unknown>;
581
+ try { raw = readFileSync(authPath, 'utf8'); parsed = JSON.parse(raw); }
582
+ catch (error) { return { ok: false, kind: 'unreadable', message: `${authPath} 를 못 읽었다 (${error instanceof Error ? error.name : 'error'})` }; }
583
+ const refreshToken = parsed.tokens?.refresh_token;
584
+ if (typeof refreshToken !== 'string' || !refreshToken) return { ok: false, kind: 'no-tokens', message: '그 홈에 refresh 토큰이 없다 — 그 홈으로 codex login' };
585
+ const beforeH = codexAccessHoursLeft(parsed.tokens?.access_token, now());
586
+ // Backup first — the server rotates the refresh token, so a failed write after refresh must leave something to recover from.
587
+ const backupPath = `${authPath}.bak-${new Date(now()).toISOString().replace(/[:.]/g, '-')}`;
588
+ try { writeFileSync(backupPath, raw, { mode: 0o600, flag: 'wx' }); }
589
+ catch (error) { return { ok: false, kind: 'write-failed', message: `백업을 못 썼다 (${error instanceof Error ? error.name : 'error'})` }; }
590
+ let fresh: TokenResponse;
591
+ try { fresh = await (opts.refresh ?? ((token) => refreshCodexTokens(token)))(refreshToken); }
592
+ catch (error) {
593
+ // refreshCodexTokens' message is `codex refresh failed: status=N` — no token. Anything else: a fixed line.
594
+ const message = error instanceof Error && /^codex refresh failed: status=\d+$/.test(error.message) ? error.message : 'codex refresh failed';
595
+ return { ok: false, kind: 'refresh-failed', message };
596
+ }
597
+ const next = {
598
+ ...parsed,
599
+ tokens: { ...parsed.tokens, access_token: fresh.access_token, refresh_token: fresh.refresh_token, ...(fresh.id_token ? { id_token: fresh.id_token } : {}) },
600
+ last_refresh: new Date(now()).toISOString(),
601
+ };
602
+ const tmp = `${authPath}.tmp-${process.pid}-${now()}`;
603
+ try {
604
+ writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
605
+ chmodSync(tmp, 0o600);
606
+ renameSync(tmp, authPath);
607
+ } catch (error) {
608
+ try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
609
+ return { ok: false, kind: 'write-failed', message: `${authPath} 원자적 쓰기 실패 (${error instanceof Error ? error.name : 'error'}) — 백업: ${backupPath}` };
610
+ }
611
+ // ⭐ Canonical store through the same path as `account import` — not a second writer.
612
+ const imported = await importCodexAccountFromHome(name, codexHome, storePath);
613
+ if (!imported.ok) return { ok: false, kind: 'import-failed', message: imported.message };
614
+ return { ok: true, storeKey, codexHome, beforeH, afterH: codexAccessHoursLeft(fresh.access_token, now()), backupPath };
615
+ }
@@ -0,0 +1,75 @@
1
+ // Launch-time application of the overload gate. Kept out of dev-cli.ts so a
2
+ // unit test can import it without the dev-cli module graph (yaml, typescript).
3
+
4
+ import type { ChildLlmSelection } from '../agent/run-context.js';
5
+ import { debug } from '../debug/log.js';
6
+ import { resolveGrokCredential } from '../grok/credential.js';
7
+ import {
8
+ applyOverloadFailoverToChild,
9
+ readLlmCallOutcomes,
10
+ type OverloadFailoverLaunchDecision,
11
+ } from './overload-failover.js';
12
+
13
+ const CODEX_NAMES = new Set(['codex', 'openai-codex']);
14
+
15
+ export function codexProviderNamed(provider: string | undefined): boolean {
16
+ if (!provider?.trim()) return false;
17
+ return CODEX_NAMES.has(provider.trim().toLowerCase());
18
+ }
19
+
20
+ let launchOverloadOutcomesForTesting: (() => ReturnType<typeof readLlmCallOutcomes>) | undefined;
21
+
22
+ /** Test seam. `undefined` restores the real log store. */
23
+ export function setLaunchOverloadOutcomesForTesting(
24
+ reader: (() => ReturnType<typeof readLlmCallOutcomes>) | undefined,
25
+ ): void {
26
+ launchOverloadOutcomesForTesting = reader;
27
+ }
28
+
29
+ export interface LaunchOverloadFlags {
30
+ provider?: string;
31
+ model?: string;
32
+ /** Pinned childLlm config naming codex. A flag is handled via `provider`. */
33
+ codexPinned?: boolean;
34
+ /** Test seam. Default: a grok credential resolves on this host. */
35
+ grokAvailable?: boolean;
36
+ }
37
+
38
+ /** Explicit codex stays. Otherwise k `llm.call`/`outcome` overload rows move
39
+ * the unpinned child to grok before spawn. */
40
+ export function applyLaunchOverloadFailover(
41
+ selection: ChildLlmSelection | undefined,
42
+ flags: LaunchOverloadFlags = {},
43
+ readOutcomes?: () => ReturnType<typeof readLlmCallOutcomes>,
44
+ env: NodeJS.ProcessEnv = process.env,
45
+ ): ChildLlmSelection | undefined {
46
+ // Test processes never read this machine's operational logs.db — results must not depend on the host's history.
47
+ const inTest = env.NODE_ENV === 'test' || Boolean(env.ELANOUS_TEST_HOME);
48
+ const reader = launchOverloadOutcomesForTesting ?? readOutcomes ?? (inTest ? () => [] : () => readLlmCallOutcomes());
49
+ let outcomes: ReturnType<typeof readLlmCallOutcomes> = [];
50
+ try { outcomes = reader(); } catch { outcomes = []; }
51
+ const explicit = codexProviderNamed(flags.provider) || flags.codexPinned === true;
52
+ let grokAvailable = flags.grokAvailable;
53
+ if (grokAvailable === undefined && outcomes.length > 0 && !inTest) {
54
+ // Only resolve when a switch is possible — a host without grok credentials must stay on codex. Tests inject it.
55
+ try { grokAvailable = resolveGrokCredential() !== null; } catch { grokAvailable = false; }
56
+ }
57
+ const { selection: next, decision } = applyOverloadFailoverToChild(selection, {
58
+ codexExplicit: explicit,
59
+ outcomes,
60
+ ...(grokAvailable !== undefined ? { grokAvailable } : {}),
61
+ defaultProvider: selection?.provider ?? 'openai-codex',
62
+ });
63
+ if (decision.switched && next?.provider === 'grok' && next.model) {
64
+ debug.log('self-dev', 'overload-failover', {
65
+ from: selection?.provider ?? 'openai-codex',
66
+ to: next.provider,
67
+ model: next.model,
68
+ why: decision.why,
69
+ }, { level: 'warn' });
70
+ return { provider: next.provider, model: next.model, source: 'config' };
71
+ }
72
+ return selection;
73
+ }
74
+
75
+ export type { OverloadFailoverLaunchDecision };
@@ -0,0 +1,137 @@
1
+ // Launch-time codex → grok switch driven only by `llm.call` / `outcome`.
2
+ //
3
+ // #24256 read `llm.response` / `llm.stream`. Those rows do not carry
4
+ // provider + status + kind, so a launch that consulted them never saw
5
+ // overload. This module refuses every other category.
6
+ //
7
+ // Boundaries:
8
+ // • an explicit codex child (flag or pinned config) never switches (#24183)
9
+ // • k consecutive codex overload/5xx/rate-limit outcomes switch the next
10
+ // child to grok before that child is spawned
11
+ // • k later successful outcomes restore codex
12
+ // • a 400 (kind `other`) never counts
13
+
14
+ import { logsDbPath, LogStore, type LogStoreRow } from '../mss/logging/log-store.js';
15
+ import {
16
+ codexOverloadStreakReached,
17
+ overloadRecovered,
18
+ OVERLOAD_FAILOVER_STREAK,
19
+ type LlmCallOutcome,
20
+ type LlmCallOutcomeKind,
21
+ } from '../session-runtime/retry-policy.js';
22
+ import { lookupLlmTierSpec } from '../model-tier/llm-tier-map.js';
23
+
24
+ export const LLM_CALL_CATEGORY = 'llm.call';
25
+ export const LLM_CALL_EVENT = 'outcome';
26
+
27
+ const KINDS = new Set<LlmCallOutcomeKind>(['ok', 'overloaded', '5xx', 'rate-limit', 'other']);
28
+
29
+ export interface OverloadFailoverLaunchInput {
30
+ /** True when the launch named codex (flag or pinned childLlm). */
31
+ readonly codexExplicit: boolean;
32
+ /** Provider the launch would have used before this gate. */
33
+ readonly provider: string;
34
+ readonly model?: string;
35
+ readonly outcomes: readonly LlmCallOutcome[];
36
+ readonly streak?: number;
37
+ /** grok may be selected. Unknown is not a reason to stay on a hot codex. */
38
+ readonly grokAvailable?: boolean;
39
+ }
40
+
41
+ export interface OverloadFailoverLaunchDecision {
42
+ readonly provider: string;
43
+ readonly model?: string;
44
+ readonly switched: boolean;
45
+ readonly why: 'explicit-codex' | 'not-codex' | 'streak' | 'recovered' | 'below-streak' | 'grok-unavailable';
46
+ }
47
+
48
+ export function parseLlmCallOutcome(row: Pick<LogStoreRow, 'category' | 'event' | 'data'>): LlmCallOutcome | undefined {
49
+ if (row.category !== LLM_CALL_CATEGORY || row.event !== LLM_CALL_EVENT || !row.data) return undefined;
50
+ let parsed: unknown;
51
+ try { parsed = JSON.parse(row.data); } catch { return undefined; }
52
+ if (!parsed || typeof parsed !== 'object') return undefined;
53
+ const rec = parsed as { provider?: unknown; status?: unknown; kind?: unknown };
54
+ if (typeof rec.provider !== 'string' || rec.provider.length === 0) return undefined;
55
+ if (typeof rec.status !== 'number' || !Number.isFinite(rec.status)) return undefined;
56
+ if (typeof rec.kind !== 'string' || !KINDS.has(rec.kind as LlmCallOutcomeKind)) return undefined;
57
+ return { provider: rec.provider, status: rec.status, kind: rec.kind as LlmCallOutcomeKind };
58
+ }
59
+
60
+ /** Newest-last. Rows that are not `llm.call`/`outcome` are dropped, never guessed. */
61
+ export function outcomesFromCallRows(rows: readonly (Pick<LogStoreRow, 'category' | 'event' | 'data' | 'ts_ms'> & { id?: number })[]): LlmCallOutcome[] {
62
+ // The store returns newest first; calls in the same millisecond keep that order unless ties break on row id.
63
+ return [...rows]
64
+ .filter((row) => row.category === LLM_CALL_CATEGORY && row.event === LLM_CALL_EVENT)
65
+ .sort((a, b) => a.ts_ms - b.ts_ms || (a.id ?? 0) - (b.id ?? 0))
66
+ .flatMap((row) => {
67
+ const parsed = parseLlmCallOutcome(row);
68
+ return parsed ? [parsed] : [];
69
+ });
70
+ }
71
+
72
+ export function readLlmCallOutcomes(opts: {
73
+ dbPath?: string;
74
+ sinceMs?: number;
75
+ limit?: number;
76
+ open?: (path: string) => Pick<LogStore, 'query' | 'close'>;
77
+ } = {}): LlmCallOutcome[] {
78
+ const path = opts.dbPath ?? logsDbPath();
79
+ const open = opts.open ?? ((dbPath: string) => LogStore.openReadOnly(dbPath));
80
+ let store: Pick<LogStore, 'query' | 'close'> | undefined;
81
+ try {
82
+ store = open(path);
83
+ const rows = store.query({
84
+ exactCategories: [LLM_CALL_CATEGORY],
85
+ events: [LLM_CALL_EVENT],
86
+ ...(opts.sinceMs !== undefined ? { sinceMs: opts.sinceMs } : {}),
87
+ limit: opts.limit ?? 50,
88
+ });
89
+ return outcomesFromCallRows(rows);
90
+ } catch {
91
+ return [];
92
+ } finally {
93
+ try { store?.close(); } catch { /* a missing store is "no sample", not a launch failure */ }
94
+ }
95
+ }
96
+
97
+ export function decideOverloadFailoverLaunch(input: OverloadFailoverLaunchInput): OverloadFailoverLaunchDecision {
98
+ const streak = input.streak ?? OVERLOAD_FAILOVER_STREAK;
99
+ const base = { provider: input.provider, ...(input.model !== undefined ? { model: input.model } : {}) };
100
+ if (input.codexExplicit) return { ...base, switched: false, why: 'explicit-codex' };
101
+ const onCodex = input.provider === 'openai-codex' || input.provider === 'codex';
102
+ if (!onCodex) return { ...base, switched: false, why: 'not-codex' };
103
+ if (overloadRecovered(input.outcomes, streak)) return { ...base, switched: false, why: 'recovered' };
104
+ if (!codexOverloadStreakReached(input.outcomes, streak)) return { ...base, switched: false, why: 'below-streak' };
105
+ if (input.grokAvailable === false) return { ...base, switched: false, why: 'grok-unavailable' };
106
+ return {
107
+ provider: 'grok',
108
+ model: lookupLlmTierSpec('grok', 'better').model,
109
+ switched: true,
110
+ why: 'streak',
111
+ };
112
+ }
113
+
114
+ /** Apply the gate to a not-yet-pinned child selection. Explicit codex is left untouched. */
115
+ export function applyOverloadFailoverToChild<T extends { provider: string; model: string; source: 'flag' | 'config' }>(
116
+ selection: T | undefined,
117
+ opts: {
118
+ codexExplicit: boolean;
119
+ outcomes: readonly LlmCallOutcome[];
120
+ grokAvailable?: boolean;
121
+ defaultProvider?: string;
122
+ },
123
+ ): { selection: T | { provider: string; model: string; source: 'config' } | undefined; decision: OverloadFailoverLaunchDecision } {
124
+ const provider = selection?.provider ?? opts.defaultProvider ?? 'openai-codex';
125
+ const decision = decideOverloadFailoverLaunch({
126
+ codexExplicit: opts.codexExplicit,
127
+ provider,
128
+ ...(selection?.model !== undefined ? { model: selection.model } : {}),
129
+ outcomes: opts.outcomes,
130
+ grokAvailable: opts.grokAvailable,
131
+ });
132
+ if (!decision.switched || !decision.model) return { selection, decision };
133
+ return {
134
+ selection: { provider: decision.provider, model: decision.model, source: 'config' },
135
+ decision,
136
+ };
137
+ }
@@ -18,6 +18,8 @@ export interface ChecklistItem {
18
18
  deadlineVersion?: string;
19
19
  ceoMinutes?: number;
20
20
  ceoDate?: string;
21
+ /** 다른 칸의 처리량을 올리는 칸(자율성·효율성·동시성). 없으면 가속 등급이 아니다. */
22
+ accelerator?: true;
21
23
  evidence?: string;
22
24
  disposition?: ChecklistDisposition;
23
25
  kind?: ChecklistKind;
@@ -103,7 +105,7 @@ export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }
103
105
  if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
104
106
  }
105
107
 
106
- export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }, options: { allowDuplicateId?: boolean } = {}): Checklist {
108
+ export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
107
109
  if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
108
110
  if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
109
111
  if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
@@ -111,6 +113,7 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
111
113
  if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
112
114
  if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
113
115
  if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
116
+ if (input.accelerator !== undefined && input.accelerator !== true) throw new CliUserError('가속 등급은 true 이거나 생략한다');
114
117
  validateCeoLoad(input);
115
118
  return mutate(v, (data, otherItems) => {
116
119
  if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
@@ -122,14 +125,14 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
122
125
  }
123
126
  const by = process.env.ELANOUS_TRACK || 'cli';
124
127
  const at = new Date().toISOString();
125
- const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), updatedAt: at, updatedBy: by };
128
+ const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), updatedAt: at, updatedBy: by };
126
129
  data.items.push(item);
127
130
  change(data, item.id, 'add', null, item, by, at);
128
131
  return true;
129
132
  });
130
133
  }
131
134
 
132
- export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }, by: string): Checklist {
135
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
133
136
  return mutate(v, (data) => {
134
137
  const item = data.items.find((i) => i.id === id);
135
138
  if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
@@ -139,6 +142,7 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
139
142
  if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
140
143
  if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
141
144
  if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
145
+ if (patch.accelerator !== undefined && patch.accelerator !== true && patch.accelerator !== null) throw new CliUserError('가속 등급은 true(부여) 또는 null(해제)이다');
142
146
  validateCeoLoad(patch);
143
147
  if (patch.owner !== undefined) {
144
148
  parseOwner(patch.owner);
@@ -146,13 +150,14 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
146
150
  throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
147
151
  }
148
152
  }
149
- const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
153
+ const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
150
154
  if (fields.length === 0) return false;
151
155
  const at = new Date().toISOString();
152
156
  for (const field of fields) {
153
157
  const from = item[field];
154
158
  const to = patch[field]!;
155
- (item as unknown as Record<string, unknown>)[field] = to;
159
+ if (field === 'accelerator' && to === null) delete item.accelerator;
160
+ else (item as unknown as Record<string, unknown>)[field] = to;
156
161
  item.updatedAt = at;
157
162
  item.updatedBy = by;
158
163
  change(data, id, field, from, to, by, at);
@@ -33,7 +33,7 @@ const walWait = new Int32Array(new SharedArrayBuffer(4));
33
33
  const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
34
34
  imported_versions: ['json_hash', 'imported_at'],
35
35
  events: ['reason'],
36
- assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date'],
36
+ assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator'],
37
37
  release_schedules: ['freeze_from', 'freeze_until'],
38
38
  };
39
39
  function schemaCurrent(db: Database): boolean {
@@ -75,7 +75,7 @@ function open(root = releaseLedgerRoot()): Database {
75
75
  if (!(db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>).some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
76
76
  db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
77
77
  const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
78
- for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT']] as const) {
78
+ for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER']] as const) {
79
79
  if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
80
80
  }
81
81
  const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
@@ -169,7 +169,7 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
169
169
  return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
170
170
  }
171
171
  function items(db: Database, version: string): ChecklistItem[] {
172
- const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
172
+ const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
173
173
  FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`).all(version) as Array<Record<string, string | number | null>>;
174
174
  return rows.map((r) => {
175
175
  const evidence = [r.evidence, ...evidenceRefs(db, version, r.id as string)].filter((value) => value !== null).join('\n');
@@ -178,6 +178,7 @@ function items(db: Database, version: string): ChecklistItem[] {
178
178
  ...(r.priority !== null ? { priority: r.priority as ChecklistItem['priority'] } : {}), ...(r.predecessors !== null ? { predecessors: JSON.parse(r.predecessors as string) as string[] } : {}),
179
179
  ...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version as string } : {}),
180
180
  ...(r.ceo_minutes !== null ? { ceoMinutes: r.ceo_minutes as number } : {}), ...(r.ceo_date !== null ? { ceoDate: r.ceo_date as string } : {}),
181
+ ...(r.accelerator === 1 ? { accelerator: true as const } : {}),
181
182
  ...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
182
183
  updatedAt: r.updated_at as string, updatedBy: r.updated_by as string };
183
184
  });
@@ -191,9 +192,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
191
192
  if (!feature) db.query('INSERT INTO features (id, title, owner, kind, created_at) VALUES (?, ?, ?, ?, ?)').run(item.id, item.title, item.owner ?? null, item.kind ?? null, item.updatedAt);
192
193
  // features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
193
194
  else if (item.owner !== undefined || item.kind !== undefined) db.query('UPDATE features SET owner = COALESCE(?, owner), kind = COALESCE(?, kind) WHERE id = ?').run(item.owner ?? null, item.kind ?? null, item.id);
194
- db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
195
- ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
196
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.updatedAt, item.updatedBy);
195
+ db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
196
+ ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
197
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
197
198
  }
198
199
  function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
199
200
 
@@ -234,8 +235,8 @@ function importOnDb(db: Database, version: string): boolean {
234
235
  // Keep a legacy per-version title until an explicit retitle unifies the feature identity.
235
236
  if (!existing) putItem(db, version, item);
236
237
  else {
237
- db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
238
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.updatedAt, item.updatedBy);
238
+ db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
239
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
239
240
  }
240
241
  }
241
242
  for (const entry of data.history) record(db, version, entry);
@@ -262,7 +263,7 @@ function importOnDb(db: Database, version: string): boolean {
262
263
  record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: null, to: item, released: data.released, dev: data.dev });
263
264
  continue;
264
265
  }
265
- const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const;
266
+ const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const;
266
267
  const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
267
268
  const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
268
269
  const differs = fields.some((field) => field === 'evidence'