elanous 0.2.15 → 0.2.16

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 (254) 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-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -8,9 +8,11 @@ import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
8
  import { getUserConfig } from '../../user-config.js';
9
9
  import { debug } from '../../debug/log.js';
10
10
  import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
11
+ import { planAction, runSeatLoopOnce, type SeatItem } from '../../seat-loop/seat-loop.js';
12
+ import { harnessQueueOutcome } from '../../harness/harness-queue.js';
11
13
 
12
14
  export type Window = '08' | '12' | '18';
13
- export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'reconcile' | 'report';
15
+ export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
14
16
  export type Mode = 'off' | 'shadow' | 'live';
15
17
  const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cmo-seat', TC: 'tc-seat', UX: 'ux-seat' };
16
18
  export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
@@ -19,6 +21,9 @@ export interface TickState {
19
21
  cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number; rebalanceBlocked?: Array<{ id: string; reason: string }>;
20
22
  reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
21
23
  runSummaries?: string[];
24
+ launched?: Array<{ seat: string; status: string; key?: string; queueId?: string; runId?: string }>;
25
+ queueOutcomes?: Array<{ queueId: string; key?: string; outcome: string }>;
26
+ steps: Array<{ node: Node; reason: string; count: number; targetLoopId?: string }>;
22
27
  nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
23
28
  missing: string[];
24
29
  }
@@ -26,11 +31,13 @@ export interface TickState {
26
31
  export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; seat?: string; host?: string }>;
27
32
  /** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
28
33
  export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
29
- export type PlaceAdapter = (cell: PlacementInput) => Promise<{ version?: string } | null> | { version?: string } | null;
34
+ export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
30
35
  export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown[]; blocked: Array<{ id: string; reason: string }> }> | { decisions: unknown[]; blocked: Array<{ id: string; reason: string }> };
31
36
  export interface TickDeps {
32
37
  root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
33
38
  cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
39
+ seatTurn?: (seat: string, root: string) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
40
+ queueOutcome?: (queueId: string, root: string) => 'pending' | 'unknown' | 'retryable' | 'succeeded';
34
41
  loadAdapter?: (path: string, name: string) => Promise<unknown>;
35
42
  checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
36
43
  observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
@@ -54,7 +61,10 @@ export function wishCards(cards: readonly TaskCard[]): TaskCard[] {
54
61
  return cards.filter(card => card.status === 'open' && card.goalId.startsWith('wish:') &&
55
62
  !card.sections.some(section => section.key === 'orch:split' || section.key.startsWith('orch:split:')));
56
63
  }
57
- export function reconciliation(cells: readonly Cell[], requests: readonly { key: string; status: string }[], checklist: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[]): TickState['reconciled'] {
64
+ export function reconciliation(cells: readonly Cell[], requests: readonly { key: string; status: string }[], checklist: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[],
65
+ queue: readonly { key: string; outcome: string }[] = []): TickState['reconciled'] {
66
+ // 체크리스트가 정본이고, 칸이 아직 체크리스트에 없을 때만 «그 칸의 의뢰 키»로 발사된 대기열 결과를 본다.
67
+ const queueByKey = new Map(queue.map(row => [row.key, row.outcome]));
58
68
  const result = { reached: 0, progressing: 0, blocked: 0, unknown: 0 };
59
69
  const byVersion = new Map<string, Pick<ChecklistItem, 'id' | 'status'>[]>();
60
70
  const keys = new Set(requests.filter(row => row.status === 'queued').map(row => row.key));
@@ -65,7 +75,12 @@ export function reconciliation(cells: readonly Cell[], requests: readonly { key:
65
75
  if (status === 'green' || status === 'done') result.reached++;
66
76
  else if (status === 'yellow') result.progressing++;
67
77
  else if (status === 'red') result.blocked++;
68
- else result.unknown++;
78
+ else {
79
+ const outcome = queueByKey.get(`orch:${cell.cardId}:${cell.id}`);
80
+ if (outcome === 'pending' || outcome === 'succeeded') result.progressing++;
81
+ else if (outcome === 'retryable') result.blocked++;
82
+ else result.unknown++;
83
+ }
69
84
  }
70
85
  return result;
71
86
  }
@@ -116,7 +131,7 @@ function stateFile(root: string, runId: string): string {
116
131
  if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
117
132
  return join(root, 'loop', 'orchestrator', `${runId}.json`);
118
133
  }
119
- function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells'> | null {
134
+ function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells' | 'launched'> | null {
120
135
  const dir = join(root, 'loop', 'orchestrator');
121
136
  if (!existsSync(dir)) return null;
122
137
  const states = readdirSync(dir).filter(name => /^[a-zA-Z0-9._-]+\.json$/.test(name))
@@ -126,15 +141,17 @@ function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState,
126
141
  states.sort((a, b) => a.name.localeCompare(b.name));
127
142
  const cards = new Map<string, TaskCard>();
128
143
  const cells = new Map<string, Cell>();
144
+ const launched = new Map<string, NonNullable<TickState['launched']>[number]>();
129
145
  for (const { state } of states) {
130
146
  for (const card of state.cards) cards.set(card.id, card);
131
147
  for (const cell of state.cells) cells.set(`${cell.cardId}:${cell.id}`, cell);
148
+ for (const turn of state.launched ?? []) if (turn.queueId) launched.set(turn.queueId, turn);
132
149
  }
133
- return { cards: [...cards.values()], cells: [...cells.values()] };
150
+ return { cards: [...cards.values()], cells: [...cells.values()], launched: [...launched.values()] };
134
151
  }
135
152
  function initial(runId: string, window: Window, mode: Mode, day: string): TickState {
136
153
  return { runId, window, mode, day, cards: [], skippedCards: [], cells: [], placed: 0, delegated: 0, wouldDelegate: 0,
137
- reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, nodes: {}, missing: [] };
154
+ reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, steps: [], nodes: {}, missing: [] };
138
155
  }
139
156
  function graphRunId(): string | undefined {
140
157
  const context = process.env.ELANOUS_GRAPH_CONTEXT;
@@ -163,8 +180,9 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
163
180
  const configured = deps.mode ?? getUserConfig().loops?.orchestrator?.mode;
164
181
  const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
165
182
  const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
183
+ state.steps ??= [];
166
184
  if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
167
- const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', reconcile: 'delegate', report: 'reconcile' };
185
+ const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', launch: 'delegate', reconcile: 'launch', report: 'reconcile' };
168
186
  const preceding = predecessor[node];
169
187
  if (preceding && !state.nodes[preceding]) throw new Error(`orchestrator ${node}: missing ${preceding} result`);
170
188
  const loadAdapter = deps.loadAdapter ?? optionalAdapter;
@@ -174,6 +192,7 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
174
192
  };
175
193
  const observe = (event: string, reason = '', count = 0, targetLoopId?: string): void => {
176
194
  const data = { loopId: 'orchestrator', runId, window, node, reason, count, ...(targetLoopId ? { targetLoopId } : {}) };
195
+ if (event !== 'tick') state.steps.push({ node, reason, count, ...(targetLoopId ? { targetLoopId } : {}) });
177
196
  try { (deps.observe ?? ((name, payload) => debug.log('loop.orchestrator', name, payload)))(event, data); }
178
197
  catch { /* observation must not change the run */ }
179
198
  };
@@ -198,6 +217,7 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
198
217
  if (!cell.id?.trim() || !cell.title?.trim()) throw new Error('invalid split cell');
199
218
  state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
200
219
  }
220
+ observe('exchange', `split:${card.id}`, cells.length);
201
221
  if (split && cells.length && mode === 'live') {
202
222
  const store = new CardStore(root);
203
223
  try {
@@ -209,15 +229,24 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
209
229
  const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
210
230
  const available = typeof place === 'function';
211
231
  if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
212
- else if (mode === 'shadow') observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
213
- if (mode === 'live' && available) for (const cell of state.cells) {
214
- if (cell.origin !== 'flow1a') continue;
215
- // TC review must-fix ②: map to the placer's required shape; a cell without an owner is unplaced, not a crash.
216
- if (!cell.seat) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); continue; }
217
- const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority: cell.priority ?? 'P2', predecessors: cell.predecessors ?? [] });
218
- if (validVersion(placed?.version)) {
219
- cell.version = placed.version;
220
- state.placed++;
232
+ if (mode === 'shadow' && deps.placeCell) observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
233
+ // 담당 없는 칸은 배치를 부르든 안 부르든 센다 — 배치 호출 여부와 집계를 묶지 않는다.
234
+ for (const cell of state.cells) {
235
+ if (cell.origin === 'flow1a' && (!cell.seat || !SEAT_LOOP_IDS[cell.seat])) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); }
236
+ }
237
+ if (available && (mode === 'live' || !deps.placeCell)) for (const cell of state.cells) {
238
+ if (cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) continue;
239
+ // RELPLAN's dryRun calculates the same release without writing a checklist in shadow.
240
+ try {
241
+ const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority: cell.priority ?? 'P2', predecessors: cell.predecessors ?? [] }, { dryRun: mode === 'shadow', now });
242
+ if (validVersion(placed?.version)) {
243
+ cell.version = placed.version;
244
+ state.placed++;
245
+ observe('exchange', mode === 'shadow' ? `would-place:${cell.id}:${cell.version}` : `placed:${cell.id}:${cell.version}`, 1);
246
+ } else { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-release', 1); }
247
+ } catch (error) {
248
+ state.unplaced = (state.unplaced ?? 0) + 1;
249
+ observe('exchange', `unplaced:${cell.id}:${String(error).slice(0, 160)}`, 1);
221
250
  }
222
251
  }
223
252
  } else if (node === 'delegate') {
@@ -229,9 +258,11 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
229
258
  if (!targetLoopId) continue;
230
259
  const key = `orch:${cell.cardId}:${cell.id}`;
231
260
  if (keys.has(key)) continue;
232
- // Shadow shows what it would hand over even before a release is chosen (placement is not invoked in shadow).
233
- if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', validVersion(cell.version) ? key : `${key} (unplaced)`, 1, targetLoopId); continue; }
234
- if (!validVersion(cell.version)) continue;
261
+ if (!validVersion(cell.version)) {
262
+ if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
263
+ continue;
264
+ }
265
+ if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', key, 1, targetLoopId); continue; }
235
266
  keys.add(key);
236
267
  mkdirSync(dirname(path), { recursive: true });
237
268
  // TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
@@ -249,13 +280,53 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
249
280
  state.delegated++;
250
281
  observe('exchange', 'queued', 1, targetLoopId);
251
282
  }
283
+ } else if (node === 'launch') {
284
+ const eligible = state.cells.filter(cell => cell.origin === 'flow1a' && cell.seat && SEAT_LOOP_IDS[cell.seat]
285
+ && validVersion(cell.version) && (mode === 'shadow' || readRequests(root).some(row => row.key === `orch:${cell.cardId}:${cell.id}` && row.status === 'queued')));
286
+ const seats = [...new Set(eligible.map(cell => cell.seat!))];
287
+ state.launched = [];
288
+ for (const seat of seats) {
289
+ if (mode === 'shadow') {
290
+ const cell = eligible.find(row => row.seat === seat)!;
291
+ const item: SeatItem = { source: 'request', id: `orch:${cell.cardId}:${cell.id}`, title: cell.title, text: cell.title };
292
+ const action = planAction(item, seat);
293
+ state.launched.push({ seat, key: item.id, status: action.kind === 'harness' ? 'would-launch' : `would-${action.kind}` });
294
+ observe('exchange', `would-${action.kind}:${seat}`, 1, SEAT_LOOP_IDS[seat]);
295
+ } else {
296
+ const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string) => runSeatLoopOnce(assigned, {
297
+ root: stateRoot, config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] },
298
+ })))(seat, root);
299
+ // 자리 턴이 «이 틱이 넣은 의뢰»를 처리했을 때만 칸에 잇는다 — 대기열의 다른 의뢰는 이 카드의 발사가 아니다.
300
+ const targets = new Set(eligible.filter(row => row.seat === seat).map(row => `orch:${row.cardId}:${row.id}`));
301
+ const handled = 'item' in turn && turn.item?.source === 'request' ? turn.item.id : undefined;
302
+ const key = handled && targets.has(handled) ? handled : undefined;
303
+ if (handled && !key) observe('exchange', `seat-turn-other-request:${seat}`, 1, SEAT_LOOP_IDS[seat]);
304
+ state.launched.push({ seat, status: turn.status, ...(key ? { key } : {}),
305
+ ...('queueId' in turn && turn.queueId ? { queueId: turn.queueId } : {}),
306
+ ...('runId' in turn && turn.runId ? { runId: turn.runId } : {}) });
307
+ observe('exchange', `seat-turn:${seat}:${turn.status}`, 1, SEAT_LOOP_IDS[seat]);
308
+ }
309
+ }
252
310
  } else if (node === 'reconcile') {
253
- if (window !== '08' && state.cells.length === 0) {
311
+ if (window !== '08') {
312
+ // 새 카드가 있어도 앞 틱의 의뢰·발사를 같이 대조한다 — 지금 틱 것이 같은 키를 이긴다.
254
313
  const previous = restorePrevious(root, state.day, mode);
255
- if (previous) { state.cards = previous.cards; state.cells = previous.cells; }
314
+ if (previous) {
315
+ const merge = <T>(older: readonly T[], newer: readonly T[], key: (row: T) => string) =>
316
+ [...new Map([...older, ...newer].map(row => [key(row), row])).values()];
317
+ state.cards = merge(previous.cards, state.cards, card => card.id);
318
+ state.cells = merge(previous.cells, state.cells, cell => `${cell.cardId}:${cell.id}`);
319
+ state.launched = merge(previous.launched ?? [], state.launched ?? [], turn => turn.queueId ?? `${turn.seat}:${turn.key ?? ''}`);
320
+ }
256
321
  }
322
+ state.queueOutcomes = (state.launched ?? []).filter(turn => turn.queueId).map(turn => {
323
+ const outcome = (deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot })))(turn.queueId!, root);
324
+ observe('exchange', `queue-outcome:${turn.queueId}:${outcome}`, 1, SEAT_LOOP_IDS[turn.seat]);
325
+ return { queueId: turn.queueId!, ...(turn.key ? { key: turn.key } : {}), outcome };
326
+ });
257
327
  const requests = readRequests(root);
258
- state.reconciled = reconciliation(state.cells, requests, mode === 'shadow' ? version => readChecklist(root, version) : (deps.checklist ?? (version => readChecklist(root, version))));
328
+ state.reconciled = reconciliation(state.cells, requests, mode === 'shadow' ? version => readChecklist(root, version) : (deps.checklist ?? (version => readChecklist(root, version))),
329
+ state.queueOutcomes.flatMap(row => row.key ? [{ key: row.key, outcome: row.outcome }] : []));
259
330
  const from = new Date(`${state.day}T00:00:00+09:00`).getTime();
260
331
  state.runSummaries = listSelfDevRuns(selfDevRunsDir(root))
261
332
  .filter(run => run.updatedAt >= from && run.updatedAt < from + 86_400_000)
@@ -295,9 +366,9 @@ if (import.meta.main) {
295
366
  const node = process.argv[2];
296
367
  const index = process.argv.indexOf('--window');
297
368
  const window = index < 0 ? undefined : process.argv[index + 1];
298
- if (!['intake', 'split', 'place', 'delegate', 'reconcile', 'report'].includes(node ?? '') ||
369
+ if (!['intake', 'split', 'place', 'delegate', 'launch', 'reconcile', 'report'].includes(node ?? '') ||
299
370
  (window !== undefined && !['08', '12', '18'].includes(window))) {
300
- console.error('orchestrator: expected <intake|split|place|delegate|reconcile|report> [--window 08|12|18]');
371
+ console.error('orchestrator: expected <intake|split|place|delegate|launch|reconcile|report> [--window 08|12|18]');
301
372
  process.exitCode = 2;
302
373
  } else {
303
374
  void (async () => {
@@ -1,6 +1,7 @@
1
1
  import type { SlashCommand } from '../chat/index.js';
2
2
  import type { Maturity } from './feature-maturity.js';
3
3
  import type { BotSurface } from './bot-command-maturity.js';
4
+ import { botUnavailableSlashReply, deriveTuiSlashAvailability, formatBotUnavailableSlashReply } from './tui-slash-availability.js';
4
5
 
5
6
  export interface BotSlashCatalogEntry {
6
7
  name: string;
@@ -56,11 +57,19 @@ export function buildBotSlashCatalog(options: {
56
57
  commands.push({ name, description: `${description.slice(0, 100 - suffix.length)}${suffix}`, supported: false });
57
58
  seen.add(name);
58
59
  }
59
- const unsupported = new Set(commands.filter((command) => !command.supported).map((command) => command.name));
60
+ const handled = new Set(handledCommands.map(({ name }) => name));
61
+ const unsupported = new Set(commands.filter(({ supported }) => !supported).map(({ name }) => name));
62
+ const availability = new Map(deriveTuiSlashAvailability(
63
+ coreCommands.filter(({ name }) => Object.hasOwn(maturity.tuiSlash, name)), maturity,
64
+ ).map((entry) => [entry.name, entry]));
60
65
  return {
61
66
  commands,
62
67
  unsupportedReply(name) {
63
- return unsupported.has(name) ? `/${name}은(는) ${surface === 'telegram' ? '텔레그램' : '디스코드'}에서 아직 지원되지 않습니다. TUI에서 /${name}을(를) 사용하세요.` : null;
68
+ if (handled.has(name)) return null;
69
+ const entry = availability.get(name);
70
+ if (entry?.maturity === 'system') return null;
71
+ if (unsupported.has(name)) return formatBotUnavailableSlashReply(name, surface);
72
+ return botUnavailableSlashReply(entry, surface);
64
73
  },
65
74
  };
66
75
  }
@@ -59,7 +59,7 @@ export const FEATURE_MATURITY = {
59
59
  pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta', 'agent-env': 'beta',
60
60
  where: 'stable', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
61
61
  pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
62
- research: 'beta', release: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
62
+ research: 'beta', release: 'beta', know: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
63
63
  control: 'beta', resources: 'beta', hooks: 'beta', setup: 'stable', start: 'stable',
64
64
  grounding: 'beta', graph: 'beta', loop: 'stable', card: 'stable', 'launch-head': 'beta',
65
65
  a2a: 'beta', plugin: 'stable', make: 'beta', skills: 'beta', connect: 'beta', import: 'beta',
@@ -75,7 +75,7 @@ export const FEATURE_MATURITY = {
75
75
  schedule: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
76
76
  decisions: 'beta', directives: 'beta', claims: 'beta', lesson: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
77
77
  env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
78
- config: 'stable', tasks: 'stable', labels: 'beta', msg: 'beta', ask: 'stable',
78
+ config: 'stable', tasks: 'stable', brief: 'beta', labels: 'beta', msg: 'beta', ask: 'stable',
79
79
  onboarding: 'beta', login: 'stable', session: 'beta', chat: 'stable', repl: 'stable',
80
80
  agent: 'stable', registry: 'beta', tier: 'stable', local: 'beta', scheduler: 'beta',
81
81
  attach: 'beta', wf: 'beta', nexus: 'stable', phone: 'beta', token: 'beta',
@@ -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,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);