elanous 0.2.27 → 0.2.28

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 (285) 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/{1454-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +1 -0
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +13 -13
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +13 -13
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +13 -13
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +13 -13
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +13 -13
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/decisions/index.html +1 -1
  72. package/apps/pwa/out/decisions/index.txt +13 -13
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +13 -13
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +13 -13
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +13 -13
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +13 -13
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +13 -13
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +13 -13
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +13 -13
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +13 -13
  90. package/apps/pwa/out/live-run/index.html +1 -1
  91. package/apps/pwa/out/live-run/index.txt +13 -13
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +13 -13
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +13 -13
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +13 -13
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +13 -13
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +13 -13
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +13 -13
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +13 -13
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +14 -14
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +13 -13
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +13 -13
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +13 -13
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +13 -13
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +13 -13
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +13 -13
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +13 -13
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +13 -13
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +13 -13
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +13 -13
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +13 -13
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +13 -13
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +13 -13
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +13 -13
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +13 -13
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +13 -13
  146. package/graphs/implement-loop.yaml +38 -1
  147. package/graphs/steward/recipes.yaml +6 -0
  148. package/graphs/steward/release-day.yaml +23 -0
  149. package/graphs/steward/steward.yaml +9 -1
  150. package/graphs/work/code-pr.yaml +23 -0
  151. package/package.json +1 -1
  152. package/scripts/browser-observe-cycle.ts +76 -2
  153. package/scripts/ci-typecheck-changed.ts +50 -17
  154. package/scripts/detect-test-interference.ts +31 -1
  155. package/scripts/release-loop/docs-node.ts +51 -22
  156. package/scripts/release-loop/gate-node.ts +37 -4
  157. package/scripts/release-loop/node-verdict.ts +3 -1
  158. package/scripts/release-loop/publish-node.ts +3 -0
  159. package/scripts/release-loop/tui-node-checks.ts +5 -2
  160. package/scripts/skills/public-check.ts +5 -2
  161. package/scripts/test-deterministic.ts +520 -37
  162. package/scripts/ux-sim.ts +7 -1
  163. package/scripts/verify-kpack6-live.ts +230 -0
  164. package/src/acp/codex-plugins.ts +17 -0
  165. package/src/agent/task-notification.ts +2 -1
  166. package/src/agent-mission/in-agent-plugin.ts +8 -3
  167. package/src/agent-substrate/self-review-cli.ts +34 -10
  168. package/src/briefing/brief-items.ts +151 -35
  169. package/src/budget/llm-cost.ts +25 -2
  170. package/src/budget/usage-outcomes.ts +32 -0
  171. package/src/claims/claims-ledger.ts +23 -0
  172. package/src/claims/claims-render.ts +22 -2
  173. package/src/cli/brief-cli.ts +131 -10
  174. package/src/cli/claims-cli.ts +24 -4
  175. package/src/cli/msg-cli.ts +165 -9
  176. package/src/cli/plugin-cli.ts +11 -3
  177. package/src/cli/pr-cli.ts +80 -18
  178. package/src/cli/pr-land-refusal.ts +20 -0
  179. package/src/cli/release-cli.ts +2 -0
  180. package/src/cli/rfc-cli.ts +24 -0
  181. package/src/cli/tailscale-serve.ts +5 -8
  182. package/src/cli/tasks-cli.ts +393 -26
  183. package/src/cli/usage-cli.ts +9 -4
  184. package/src/dashboard/first-screen-band.ts +63 -13
  185. package/src/dashboard/index.ts +8 -3
  186. package/src/dashboard/skill-detection-runtime.ts +24 -4
  187. package/src/domains/morning-report.ts +3 -1
  188. package/src/github/pr-labels.ts +10 -4
  189. package/src/graph-runner/runner.ts +101 -14
  190. package/src/harness/harness-clean.ts +21 -3
  191. package/src/harness/harness-cli-command.ts +22 -6
  192. package/src/harness/harness-pod-dispatch.ts +11 -4
  193. package/src/hq/seats.ts +17 -3
  194. package/src/index.ts +34 -5
  195. package/src/intelligence-map/model-bench-scores.ts +102 -0
  196. package/src/loops/orchestrator/inbox.ts +73 -0
  197. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  198. package/src/loops/orchestrator/tick.ts +73 -2
  199. package/src/maturity/feature-maturity.ts +1 -1
  200. package/src/msg/msg-sse-server.ts +141 -0
  201. package/src/msg/msg-store.ts +84 -14
  202. package/src/patents/ledger.ts +128 -0
  203. package/src/plugins/install/market-fetch.ts +32 -1
  204. package/src/pod-lease/admission.ts +116 -41
  205. package/src/release-loop/checklist.ts +11 -0
  206. package/src/release-loop/merged-pr-checklist.ts +24 -5
  207. package/src/release-loop/rubric.ts +70 -0
  208. package/src/rfc-registry/registry.ts +88 -0
  209. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  210. package/src/seat-loop/knowhow-lint.ts +233 -0
  211. package/src/seat-loop/seat-pack.ts +16 -4
  212. package/src/seat-loop/traffic-signal.ts +7 -0
  213. package/src/self-dev/ask-launch-flow.ts +48 -8
  214. package/src/self-dev/ask-path-candidates.ts +69 -14
  215. package/src/self-dev/decompose-proposal.ts +43 -1
  216. package/src/self-dev/graph-journey-nodes.ts +2 -0
  217. package/src/self-dev/launch-preflight-text.ts +5 -1
  218. package/src/self-dev/launch-preflight.ts +143 -10
  219. package/src/self-implement/gate-cli.ts +6 -2
  220. package/src/self-implement/goal-author.ts +2 -1
  221. package/src/self-implement/goal-new-paths.ts +4 -5
  222. package/src/self-implement/graph-authority.ts +17 -3
  223. package/src/self-implement/graph-yaml.ts +15 -7
  224. package/src/self-implement/headless-elanous-driver.ts +16 -1
  225. package/src/self-implement/host-regate.ts +44 -9
  226. package/src/self-implement/orchestrator.ts +3 -10
  227. package/src/self-implement/rework-policy.ts +80 -2
  228. package/src/self-implement/speed-claims.ts +145 -6
  229. package/src/skills/tools/persistent-grounding.ts +59 -4
  230. package/src/skills/tools/shell.ts +79 -1
  231. package/src/steward/rejudge-tick.ts +12 -1
  232. package/src/steward/release-day.ts +158 -0
  233. package/src/steward/resync-tick.ts +123 -0
  234. package/src/task-agent/actions.ts +1 -0
  235. package/src/task-agent/adopt-pr.ts +140 -0
  236. package/src/task-agent/board.ts +11 -1
  237. package/src/task-agent/card-evidence.ts +37 -5
  238. package/src/task-agent/cover.ts +84 -17
  239. package/src/task-agent/failure-notify.ts +61 -0
  240. package/src/task-agent/green-sweep.ts +170 -0
  241. package/src/task-agent/land-worktree.ts +52 -5
  242. package/src/task-agent/live-move-history.ts +1 -0
  243. package/src/task-agent/live-moves.ts +43 -12
  244. package/src/task-agent/parent-host.ts +1 -1
  245. package/src/task-agent/postland-card.ts +139 -0
  246. package/src/task-agent/rejudge-on-head.ts +106 -7
  247. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  248. package/src/task-agent/repair-branch-cas.ts +182 -0
  249. package/src/task-agent/repair-cycle.ts +275 -0
  250. package/src/task-agent/repair-next-move.ts +31 -0
  251. package/src/task-agent/repair-review.ts +24 -0
  252. package/src/task-agent/repair-worker.ts +87 -0
  253. package/src/task-agent/review-result-capture.ts +2 -1
  254. package/src/task-agent/shadow.ts +13 -7
  255. package/src/task-agent/task-hand.ts +115 -6
  256. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  257. package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
  258. package/src/telegram.ts +30 -1
  259. package/src/user-config.ts +8 -1
  260. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  261. package/src/version/packed-revision.json +1 -1
  262. package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
  263. package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
  264. package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
  265. package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
  284. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  285. /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -1,14 +1,15 @@
1
1
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
- import { loadRunLedger as loadRunLedgerSync } from '../self-implement/run-ledger.js';
3
+ import { execFileSync, spawnSync } from 'node:child_process';
4
+ import { loadRunLedger as loadRunLedgerSync, loadRunLedgerWithMetadata, runLedgerDir, resolveFederatedRunLedgerDirectories } from '../self-implement/run-ledger.js';
4
5
  import { goalRunDbPath, loadGoalRunQuery } from '../self-implement/goal-run-store.js';
5
6
  import { loadGoalAskRecords } from '../self-implement/goal-ask-store.js';
6
- import { join, resolve } from 'node:path';
7
+ import { dirname, join, resolve } from 'node:path';
7
8
  import { InvalidArgumentError, Option, type Command } from 'commander';
8
9
  import { debug } from '../debug/log.js';
9
10
  import { getElanousConfigDir } from '../elanous-config-dir.js';
10
11
  import { readNexusRuntime } from '../nexus/runtime.js';
11
- import { checklistDevVersion } from '../release-loop/checklist.js';
12
+ import { checklistDevVersion, listChecklist } from '../release-loop/checklist.js';
12
13
  import { assignedVersions, releasedVersion, validateVersion } from '../release-loop/feature-store.js';
13
14
  import { compareVersions } from '../directives/version-at.js';
14
15
  import { cardsBoard, countBoardTasks, INTERNAL_PACK_PROJECT, mergeBoards, releasePackBoard, type BoardNode } from '../task-agent/board.js';
@@ -17,9 +18,11 @@ import { cardPrNumber, CLOSED_BY_HAND_EVENT, closeTaskCard, COMPLETION_KINDS, ha
17
18
  import { readCardSalvageBranch, recordWiringLiveVerified, refreshCardRunBinding, type CardRunEvidenceDeps } from '../task-agent/card-evidence.js';
18
19
  import { captureReviewResults } from '../task-agent/review-result-capture.js';
19
20
  import { replayRemoteCardMoves } from '../task-agent/shadow.js';
21
+ import { remoteLedgerMirrorDirs } from '../task-agent/parent-host.js';
22
+ import { ambiguousGreenSweepPrs, decideGreenSweep, greenSweepCandidates, observeGreenSweep, type GreenSweepPr } from '../task-agent/green-sweep.js';
20
23
  import { HAND_MOVE_KINDS, recordHandMove } from '../task-agent/live-move-history.js';
21
24
  import { advanceMission, handMission, MissionAdvanceError, openMissionIds, recordPieceRef, type AdvanceMissionResult, type MissionDecompose, type PieceEvidenceReader } from '../task-agent/mission.js';
22
- import { formatSevenVerbLive, formatTaskAgentCover, measureSevenVerbLive, measureTaskAgentCover, type CoverRow, type SevenVerbLive, type TaskAgentCoverInput } from '../task-agent/cover.js';
25
+ import { formatSevenVerbLive, formatTaskAgentCover, measureSevenVerbLive, measureTaskAgentCover, type CoverRow, type ImplementCardRunBinding, type ImplementRunMergeEvent, type SevenVerbLive, type TaskAgentCoverInput } from '../task-agent/cover.js';
23
26
  import { LogStore, STORE_SAFETY_MAX, type LogQuery, type LogStoreRow } from '../mss/logging/log-store.js';
24
27
  import { resolveLogTargets } from './logs-cli.js';
25
28
 
@@ -36,6 +39,8 @@ export interface TasksCliDeps {
36
39
  reviewResultNow?: () => Date;
37
40
  /** 과제 카드 상태 파일(시험 주입). 기본 = `effectiveInstanceRoot()/task-agent-actions.json`. */
38
41
  taskStatePath?: string;
42
+ /** 조합 선택 시험에서만 그래프 저장소 루트를 바꾼다. */
43
+ taskCompositionRoot?: string;
39
44
  /** logs.db 싱크 등록(시험 주입). 기본 = `registerStandaloneLogSink`. `hand`·`show` 가 부른다. */
40
45
  registerSink?: (surface: string) => Promise<unknown>;
41
46
  /** `task hand --mission` 의 분해기(시험 주입). 기본 = 나열형은 결정론 · 아니면 `decomposeSelfDevGoal`. */
@@ -46,6 +51,12 @@ export interface TasksCliDeps {
46
51
  releaseBoard?: (version?: string) => BoardNode[];
47
52
  /** `tasks hand --checklist` 판 목록(시험 주입). 기본은 발행 전 체크리스트가 있는 판 전부. */
48
53
  releaseVersions?: () => string[];
54
+ /** Isolated checklist read for PR adoption. */
55
+ adoptChecklist?: typeof listChecklist;
56
+ /** `tasks adopt-pr` live PR read seam; default is gh in this repository. */
57
+ adoptPrView?: (number: number, cwd: string) => import('../task-agent/adopt-pr.js').AdoptPrView;
58
+ /** Isolated ledger proof for a launched card being adopted. */
59
+ adoptRunTerminal?: typeof import('../task-agent/adopt-pr.js').adoptedRunTerminal;
49
60
  /** `tasks show`·`advance` 의 런 근거 읽기(시험 주입) — 기본 = 런 원장(이 우주 ⊕ 연합) ⊕ 호스트 logs 의 Pod job-finished. */
50
61
  cardRunEvidence?: CardRunEvidenceDeps;
51
62
  /** `tasks live-verified` 가 적는 런 원장 디렉터리(시험 주입). 기본 = `runLedgerDir()`. */
@@ -56,15 +67,20 @@ export interface TasksCliDeps {
56
67
  staleParents?: import('../task-agent/parent-host.js').StaleParentDeps;
57
68
  /** `tasks adopt-runs` 의 백필 의존(시험 주입 · 등록부·상태 파일). */
58
69
  adoptRuns?: import('../task-agent/run-adopt.js').RunAdoptDeps;
59
- /** `tasks cover` logs 조회 seam. 기본은 등록된 모든 우주(운영 ⊕ test)의 logs.db 를 read-only 로 연다 —
60
- * pr land 는 작업 트리마다 파생 test 우주에 기록되므로 현재 우주만 읽으면 분모가 늘 0 이다. */
61
- coverLogs?: (query: LogQuery) => LogStoreRow[] | CoverLogRead;
70
+ /** `tasks cover` logs 조회 seam. 기본은 운영 우주만, --include-test 때 시험 우주도 읽는다. */
71
+ coverLogs?: (query: LogQuery, includeTest?: boolean) => LogStoreRow[] | CoverLogRead;
62
72
  /** `tasks cover --by-card` 카드 원장 읽기 seam(읽기 전용). 플래그가 있을 때만 부른다. 기본 = TA 카드 원장(task-agent-actions.json). null = 못 읽음. */
63
73
  coverCards?: () => CoverCardRow[] | null | Promise<CoverCardRow[] | null>;
64
74
  /** Read-only authored goal provenance; null means the authoring ledger could not be read. */
65
75
  coverGoalAsks?: () => { authorRunId: string; goalId: string; createdAt: string }[] | null;
66
76
  /** Read-only terminal goal records; null means the goal ledger could not be read. */
67
77
  coverGoalRuns?: () => { goalId: string; outcome: string; stage?: string; startedAt: string; completedAt?: string }[] | null;
78
+ /** 구현 출처 시험 seam. 생략하면 task-agent-actions.json을 읽고, null이면 카드 원장을 못 읽은 것이다. */
79
+ coverImplementCardRuns?: () => readonly ImplementCardRunBinding[] | null;
80
+ /** 구현 출처 시험 seam. 생략하면 로컬·연합·원격 거울 run-ledger를 읽고, null이면 못 읽은 것이다. */
81
+ coverImplementRunMerges?: () => ReadonlyMap<string, ImplementRunMergeEvent> | null;
82
+ /** Read test-universe ledgers only when --include-test was requested. */
83
+ coverIncludeTest?: boolean;
68
84
  /** TA-REJUDGE-ON-HEAD — `tasks rejudge` 의 판단 의존(시험 주입 · 기본 = 설정 liveMoves ⊕ 실물 gh·리뷰·재게이트·land). */
69
85
  rejudge?: Partial<import('../task-agent/rejudge-on-head.js').RejudgeDeps>;
70
86
  /** TA-REJUDGE-ON-HEAD — `tasks regate-run` 의 재게이트(시험 주입 · 기본 `runHostRegate`). */
@@ -324,8 +340,8 @@ async function mirrorRemoteCardLedger(cardId: string, deps: TasksCliDeps): Promi
324
340
  try {
325
341
  if (deps.remoteLedgerMirror) { deps.remoteLedgerMirror(card); return; }
326
342
  const { mirrorRemoteRunLedgers } = await import('../task-agent/parent-host.js');
327
- const { prodInstanceRoot } = await import('../instance/resolve.js');
328
- mirrorRemoteRunLedgers(card.parentHost, card.runId, prodInstanceRoot());
343
+ const { effectiveInstanceRoot } = await import('../instance/resolve.js');
344
+ mirrorRemoteRunLedgers(card.parentHost, card.runId, effectiveInstanceRoot(), deps.cardRunEvidence?.ssh);
329
345
  } catch (error) {
330
346
  try { debug.log('task-agent', 'remote-ledger-mirror-failed', { card: cardId, host: card.parentHost.host, runId: card.runId, error: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ }
331
347
  }
@@ -456,8 +472,20 @@ const COVER_QUERY_LIMIT = STORE_SAFETY_MAX;
456
472
  /** Federated read: rows from every store, whether any single store hit the bound, and how many stores could not be read. */
457
473
  export interface CoverLogRead { rows: LogStoreRow[]; limitReached: boolean; unreadableStores: number; storesRead: number }
458
474
 
459
- function queryCoverLogs(query: LogQuery): CoverLogRead {
460
- const { targets, error } = resolveLogTargets({ all: true, includeTest: true });
475
+ /** Land attempts are observed separately from completed land actions; an appended order or skipped move is not a merge. */
476
+ export interface CoverLandSignals {
477
+ liveMoveEvents: number | null;
478
+ executedLiveMoves: number | null;
479
+ landOrderEvents: number | null;
480
+ }
481
+
482
+ export function formatCoverLandSignals(signals: CoverLandSignals): string {
483
+ const count = (value: number | null) => value === null ? '-' : String(value);
484
+ return `착지 시도 관측: live-move ${count(signals.liveMoveEvents)} · 집행 ${count(signals.executedLiveMoves)} · land-order ${count(signals.landOrderEvents)} (병합 수와 별개)`;
485
+ }
486
+
487
+ function queryCoverLogs(query: LogQuery, includeTest = false): CoverLogRead {
488
+ const { targets, error } = resolveLogTargets({ all: true, includeTest });
461
489
  if (error) throw new Error(error);
462
490
  const read: CoverLogRead = { rows: [], limitReached: false, unreadableStores: 0, storesRead: 0 };
463
491
  for (const target of targets) {
@@ -490,6 +518,61 @@ function coverEvents<T>(rows: readonly LogStoreRow[], category: string, event: s
490
518
  return events;
491
519
  }
492
520
 
521
+ /** A malformed land record makes the numerator unknown; dropping it would assert a false zero. */
522
+ function coverLandMoves(rows: readonly LogStoreRow[]): { card: string; executed: boolean; ok: boolean }[] | null {
523
+ const moves: { card: string; executed: boolean; ok: boolean }[] = [];
524
+ for (const row of rows) {
525
+ if (row.category !== 'task-agent' || row.event !== 'live-move') continue;
526
+ if (typeof row.data !== 'string') return null;
527
+ let data: unknown;
528
+ try { data = JSON.parse(row.data); } catch { return null; }
529
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) return null;
530
+ const move = data as Record<string, unknown>;
531
+ if (typeof move.kind !== 'string') return null;
532
+ if (move.kind !== 'land') continue;
533
+ if (typeof move.card !== 'string' || !move.card || typeof move.executed !== 'boolean'
534
+ || typeof move.ok !== 'boolean') return null;
535
+ moves.push({ card: move.card, executed: move.executed, ok: move.ok });
536
+ }
537
+ return moves;
538
+ }
539
+
540
+ /** An incomplete action source cannot provide a certain land numerator. */
541
+ function coverTaskActions(rows: readonly LogStoreRow[]): NonNullable<TaskAgentCoverInput['taskAgentActions']> | null {
542
+ const actions: NonNullable<TaskAgentCoverInput['taskAgentActions']>[number][] = [];
543
+ for (const row of rows) {
544
+ if (row.category !== 'task-agent' || row.event !== 'action') continue;
545
+ if (typeof row.data !== 'string') return null;
546
+ let data: unknown;
547
+ try { data = JSON.parse(row.data); } catch { return null; }
548
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) return null;
549
+ const action = data as Record<string, unknown>;
550
+ if (typeof action.kind !== 'string' || typeof action.result !== 'string') return null;
551
+ if (action.kind === 'land' && action.result === 'done'
552
+ && (typeof action.taskId !== 'string' || !action.taskId)) return null;
553
+ actions.push({ kind: action.kind as NonNullable<TaskAgentCoverInput['taskAgentActions']>[number]['kind'],
554
+ result: action.result,
555
+ ...(typeof action.taskId === 'string' ? { taskId: action.taskId } : {}),
556
+ ...(typeof action.reason === 'string' ? { reason: action.reason } : {}) });
557
+ }
558
+ return actions;
559
+ }
560
+
561
+ function coverLandSteps(rows: readonly LogStoreRow[]): NonNullable<TaskAgentCoverInput['allLands']> | null {
562
+ const steps: NonNullable<TaskAgentCoverInput['allLands']>[number][] = [];
563
+ for (const row of rows) {
564
+ if (row.category !== 'pr.land' || row.event !== 'step') continue;
565
+ if (typeof row.data !== 'string') return null;
566
+ let data: unknown;
567
+ try { data = JSON.parse(row.data); } catch { return null; }
568
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) return null;
569
+ const step = data as Record<string, unknown>;
570
+ if (typeof step.step !== 'string' || typeof step.ok !== 'boolean') return null;
571
+ steps.push({ step: step.step, ok: step.ok });
572
+ }
573
+ return steps;
574
+ }
575
+
493
576
  export interface CoverCardRow { id: string; createdAt: string }
494
577
 
495
578
  /**
@@ -511,14 +594,89 @@ export function coverCardsInWindow(cards: readonly CoverCardRow[] | null, window
511
594
  }).map(({ id }) => ({ id }));
512
595
  }
513
596
 
597
+ /** Read the card ledger itself. A missing file or malformed row makes the aggregate unknown. */
598
+ function readImplementCardBindings(statePath: string): ImplementCardRunBinding[] {
599
+ if (!existsSync(statePath)) throw new Error('TASK-AGENT card ledger is missing');
600
+ const state = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath);
601
+ if (state.tasks !== undefined && (!state.tasks || typeof state.tasks !== 'object' || Array.isArray(state.tasks))) {
602
+ throw new Error('TASK-AGENT card ledger is malformed');
603
+ }
604
+ const cards = Object.entries(state.tasks ?? {});
605
+ if (cards.length >= COVER_QUERY_LIMIT) throw new Error('TASK-AGENT card ledger reached query bound');
606
+ return cards.map(([key, card]) => {
607
+ if (!card || typeof card !== 'object' || Array.isArray(card) || typeof card.id !== 'string' || !card.id
608
+ || card.id !== key || (card.runId != null && typeof card.runId !== 'string')
609
+ || (card.runChildId != null && typeof card.runChildId !== 'string')) {
610
+ throw new Error('TASK-AGENT card binding is malformed');
611
+ }
612
+ return { cardId: card.id, runId: card.runId, runChildId: card.runChildId };
613
+ });
614
+ }
615
+
616
+ function implementLedgerDirectories(statePath: string, includeTest: boolean): string[] {
617
+ const root = dirname(statePath);
618
+ return [...new Set([
619
+ runLedgerDir(root),
620
+ runLedgerDir(),
621
+ ...resolveFederatedRunLedgerDirectories({ includeTest }),
622
+ ...remoteLedgerMirrorDirs(root),
623
+ ])];
624
+ }
625
+
626
+ /** Read only runs bound to cards. Merge time, rather than card launch time, owns the cover window. */
627
+ function readImplementRunMerges(
628
+ bindings: readonly ImplementCardRunBinding[],
629
+ directories: readonly string[],
630
+ window: { sinceMs?: number; untilMs?: number },
631
+ ): ReadonlyMap<string, ImplementRunMergeEvent> {
632
+ const runIds = new Set(bindings.flatMap(({ runId, runChildId }) => [runId, runChildId])
633
+ .filter((id): id is string => typeof id === 'string' && id.length > 0));
634
+ if (runIds.size >= COVER_QUERY_LIMIT) throw new Error('bound run ledger query reached bound');
635
+ const merges = new Map<string, ImplementRunMergeEvent>();
636
+ let eventsRead = 0;
637
+ for (const runId of runIds) {
638
+ let found = false;
639
+ let merged = false;
640
+ let failure: unknown;
641
+ for (const directory of directories) {
642
+ try {
643
+ const loaded = loadRunLedgerWithMetadata(runId, directory);
644
+ if (loaded === null) continue;
645
+ found = true;
646
+ if (loaded.skippedTrailingBytes) throw new Error(`run ledger is incomplete: ${runId}`);
647
+ eventsRead += loaded.entries.length;
648
+ if (eventsRead >= COVER_QUERY_LIMIT) throw new Error('run ledger query reached bound');
649
+ for (const entry of loaded.entries) {
650
+ if (entry.event !== 'merged') continue;
651
+ if (typeof entry.data.merged !== 'boolean') throw new Error(`merged event is malformed: ${runId}`);
652
+ if (!entry.data.merged) continue;
653
+ if (window.sinceMs !== undefined || window.untilMs !== undefined) {
654
+ const at = Date.parse(entry.timestamp ?? '');
655
+ if (!Number.isFinite(at)) throw new Error(`merged event timestamp is missing: ${runId}`);
656
+ if ((window.sinceMs !== undefined && at < window.sinceMs)
657
+ || (window.untilMs !== undefined && at > window.untilMs)) continue;
658
+ }
659
+ merged = true;
660
+ }
661
+ } catch (error) { failure ??= error; }
662
+ }
663
+ // A local parent may contain only launch events while a federated or mirrored copy has the merge.
664
+ // Read every physical copy. Any unreadable copy leaves the aggregate unknown.
665
+ if (failure !== undefined) throw failure;
666
+ if (!found) throw new Error(`bound run ledger is missing: ${runId}`);
667
+ merges.set(runId, { merged });
668
+ }
669
+ return merges;
670
+ }
671
+
514
672
  export function readTaskAgentCover(
515
673
  window: { sinceMs?: number; untilMs?: number } = {},
516
674
  query: NonNullable<TasksCliDeps['coverLogs']> = queryCoverLogs,
517
675
  taskCards?: readonly { id: string }[] | null,
518
- goalReaders: Pick<TasksCliDeps, 'coverGoalAsks' | 'coverGoalRuns'> = {},
519
- ): { rows: CoverRow[]; sevenVerbs: SevenVerbLive; limitReached: boolean; unreadableStores: number } {
676
+ goalReaders: Pick<TasksCliDeps, 'coverGoalAsks' | 'coverGoalRuns' | 'coverImplementCardRuns' | 'coverImplementRunMerges' | 'taskStatePath' | 'coverIncludeTest'> = {},
677
+ ): { rows: CoverRow[]; sevenVerbs: SevenVerbLive; landSignals: CoverLandSignals; limitReached: boolean; unreadableStores: number } {
520
678
  let unreadableStores = 0;
521
- const read = (category: 'task-agent' | 'pr.land', event: 'action' | 'step'): { rows: LogStoreRow[]; limitReached: boolean } | null => {
679
+ const read = (category: 'task-agent' | 'pr.land' | 'task-agent.land-order', event: 'action' | 'step' | 'live-move' | 'appended'): { rows: LogStoreRow[]; limitReached: boolean } | null => {
522
680
  try {
523
681
  const result = query({ ...window, limit: COVER_QUERY_LIMIT, exactCategories: [category], events: [event] });
524
682
  if (Array.isArray(result)) return { rows: result, limitReached: result.length >= COVER_QUERY_LIMIT };
@@ -528,9 +686,13 @@ export function readTaskAgentCover(
528
686
  };
529
687
  const actionRead = read('task-agent', 'action');
530
688
  const landRead = read('pr.land', 'step');
689
+ const moveRead = read('task-agent', 'live-move');
690
+ const orderRead = read('task-agent.land-order', 'appended');
531
691
  const actions = actionRead?.rows ?? null;
532
692
  const lands = landRead?.rows ?? null;
533
- const truncated = { taskAgentActions: actionRead?.limitReached === true, allLands: landRead?.limitReached === true };
693
+ const truncated = { taskAgentActions: actionRead?.limitReached === true, allLands: landRead?.limitReached === true,
694
+ liveLandMoves: moveRead?.limitReached === true };
695
+ const moves = moveRead && !moveRead.limitReached ? coverLandMoves(moveRead.rows) : null;
534
696
  const inWindow = (value: string): boolean => {
535
697
  const time = Date.parse(value);
536
698
  return Number.isFinite(time) && (window.sinceMs === undefined || time >= window.sinceMs) && (window.untilMs === undefined || time <= window.untilMs);
@@ -550,7 +712,9 @@ export function readTaskAgentCover(
550
712
  try {
551
713
  if (goalReaders.coverGoalRuns) goalRuns = goalReaders.coverGoalRuns();
552
714
  else {
553
- const runs = loadGoalRunQuery({ limit: COVER_QUERY_LIMIT }, goalRunDbPath());
715
+ // Cover consumes only goal identity and terminal outcome. Child-brain enrichment would query every
716
+ // runId against every registered test logs.db, even though this measurement never reads childBrain.
717
+ const runs = loadGoalRunQuery({ limit: COVER_QUERY_LIMIT }, goalRunDbPath(), { targets: [] });
554
718
  goalRunsTruncated = runs?.truncated ?? false;
555
719
  goalRuns = runs?.records.map(({ goalId, record }) => ({
556
720
  goalId, outcome: record.outcome, stage: record.stage, startedAt: record.startedAt ?? '', completedAt: record.completedAt,
@@ -559,11 +723,9 @@ export function readTaskAgentCover(
559
723
  } catch { goalRuns = null; }
560
724
  const authoredGoalIds = new Set(goalAsks?.map((ask) => ask.goalId) ?? []);
561
725
  const input: TaskAgentCoverInput = {
562
- taskAgentActions: actions === null ? null : coverEvents(actions, 'task-agent', 'action', (data) =>
563
- typeof data.kind === 'string' && typeof data.result === 'string'
564
- ? { kind: data.kind as NonNullable<TaskAgentCoverInput['taskAgentActions']>[number]['kind'], result: data.result, ...(typeof data.taskId === 'string' ? { taskId: data.taskId } : {}) } : null),
565
- allLands: lands === null ? null : coverEvents(lands, 'pr.land', 'step', (data) =>
566
- typeof data.step === 'string' && typeof data.ok === 'boolean' ? { step: data.step, ok: data.ok } : null),
726
+ taskAgentActions: actions === null ? null : coverTaskActions(actions),
727
+ liveLandMoves: moves,
728
+ allLands: lands === null ? null : coverLandSteps(lands),
567
729
  truncated: { ...truncated, goalAsks: goalAsksTruncated, goalRuns: goalRunsTruncated },
568
730
  // Authoring may start before this window; completion is the event being counted.
569
731
  goalAsks,
@@ -575,7 +737,32 @@ export function readTaskAgentCover(
575
737
  : goalRuns,
576
738
  ...(taskCards === undefined ? {} : { taskCards }),
577
739
  };
578
- return { rows: measureTaskAgentCover(input), sevenVerbs: measureSevenVerbLive(input), limitReached: truncated.taskAgentActions || truncated.allLands || goalAsksTruncated || goalRunsTruncated, unreadableStores };
740
+ // A supplied reader overrides only its own source. The ordinary CLI always reads both physical ledgers.
741
+ try {
742
+ input.implementCardRuns = goalReaders.coverImplementCardRuns
743
+ ? goalReaders.coverImplementCardRuns()
744
+ : readImplementCardBindings(goalReaders.taskStatePath ?? taskStatePathDefault());
745
+ } catch { input.implementCardRuns = null; }
746
+ try {
747
+ input.implementRunMerges = goalReaders.coverImplementRunMerges
748
+ ? goalReaders.coverImplementRunMerges()
749
+ : input.implementCardRuns === null
750
+ ? null
751
+ : readImplementRunMerges(
752
+ input.implementCardRuns,
753
+ implementLedgerDirectories(goalReaders.taskStatePath ?? taskStatePathDefault(), goalReaders.coverIncludeTest === true),
754
+ window,
755
+ );
756
+ } catch { input.implementRunMerges = null; }
757
+ const orders = orderRead && !orderRead.limitReached ? coverEvents(orderRead.rows, 'task-agent.land-order', 'appended', (data) =>
758
+ Number.isSafeInteger(data.pr) ? { pr: data.pr as number } : null) : null;
759
+ const landSignals: CoverLandSignals = {
760
+ liveMoveEvents: moves?.length ?? null,
761
+ executedLiveMoves: moves?.filter((move) => move.executed).length ?? null,
762
+ landOrderEvents: orders?.length ?? null,
763
+ };
764
+ return { rows: measureTaskAgentCover(input), sevenVerbs: measureSevenVerbLive(input), landSignals,
765
+ limitReached: truncated.taskAgentActions || truncated.allLands || moveRead?.limitReached === true || orderRead?.limitReached === true || goalAsksTruncated || goalRunsTruncated, unreadableStores };
579
766
  }
580
767
 
581
768
  export function registerTasksCommands(program: Command, deps: TasksCliDeps = {}): void {
@@ -590,6 +777,83 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
590
777
  };
591
778
  const tasks = program.command('tasks').alias('task').description('TOX 태스크 조회 및 승인 · TASK-AGENT 과제 넘기기(hand)');
592
779
 
780
+ tasks.command('green-sweep')
781
+ .description('병합된 과제 PR 과 칸 근거를 대조해 green/기록만/건너뜀을 미리 본다 (항상 dry-run)')
782
+ .requiredOption('--version <version>', '조회할 체크리스트 판')
783
+ .option('--cell <id>', '한 칸만 조회')
784
+ .option('--traffic <path>', '실물 green 판정에는 traffic.json 필수 — 빠지거나 못 읽으면 hold 불명으로 보수 판정')
785
+ .option('--json', 'JSON 출력')
786
+ .action((opts: { version: string; cell?: string; traffic?: string; json?: boolean }) => {
787
+ try {
788
+ validateVersion(opts.version);
789
+ const snapshot = listChecklist(opts.version);
790
+ const cards = Object.values(readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(deps.taskStatePath ?? taskStatePathDefault()).tasks ?? {});
791
+ let holds: Set<string> | null = null;
792
+ if (opts.traffic) {
793
+ try {
794
+ const traffic: unknown = JSON.parse(readFileSync(opts.traffic, 'utf8'));
795
+ if (traffic && typeof traffic === 'object' && !Array.isArray(traffic)
796
+ && Array.isArray((traffic as { hold?: unknown }).hold)
797
+ && (traffic as { hold: unknown[] }).hold.every((id) => typeof id === 'string'))
798
+ holds = new Set((traffic as { hold: string[] }).hold);
799
+ } catch { /* unknown is not an empty hold set */ }
800
+ }
801
+ const prCache = new Map<number, GreenSweepPr | null>();
802
+ const ambiguousPrs = ambiguousGreenSweepPrs(snapshot.items, opts.version, cards);
803
+ const origin = spawnSync('git', ['remote', 'get-url', 'origin'],
804
+ { cwd: process.cwd(), encoding: 'utf8', timeout: 20_000 });
805
+ const originRepo = origin.status === 0
806
+ ? /github\.com[:/]([^/\s]+\/[^/\s]+?)(?:\.git)?$/i.exec(origin.stdout.trim())?.[1]?.toLowerCase() : undefined;
807
+ const localMain = spawnSync('git', ['rev-parse', 'origin/main'],
808
+ { cwd: process.cwd(), encoding: 'utf8', timeout: 20_000 });
809
+ const remoteMain = spawnSync('git', ['ls-remote', 'origin', 'refs/heads/main'],
810
+ { cwd: process.cwd(), encoding: 'utf8', timeout: 20_000 });
811
+ const localMainSha = localMain.status === 0 ? localMain.stdout.trim() : '';
812
+ const remoteMainSha = remoteMain.status === 0 ? remoteMain.stdout.trim().split(/\s+/)[0] : '';
813
+ const mainFresh = /^[a-f0-9]{40}$/i.test(localMainSha) && localMainSha === remoteMainSha;
814
+ if (opts.cell && !snapshot.items.some((item) => item.id === opts.cell)) throw new Error(`없는 칸: ${opts.cell}`);
815
+ const decisions = snapshot.items.filter((item) => !opts.cell || item.id === opts.cell).flatMap((item) => {
816
+ const candidates = greenSweepCandidates(item, opts.version, cards);
817
+ return candidates.map((candidate) => {
818
+ let pr = prCache.get(candidate.pr);
819
+ if (pr === undefined) {
820
+ const view = spawnSync('gh', ['pr', 'view', String(candidate.pr), '--json', 'number,state,mergeCommit,url'],
821
+ { cwd: process.cwd(), encoding: 'utf8', timeout: 20_000 });
822
+ try {
823
+ if (view.status !== 0) throw new Error('gh failed');
824
+ const parsed = JSON.parse(view.stdout) as { number?: number; state?: string; mergeCommit?: { oid?: string }; url?: string };
825
+ pr = parsed.number === candidate.pr && typeof parsed.state === 'string'
826
+ ? { number: candidate.pr, state: parsed.state, mergeSha: parsed.mergeCommit?.oid, url: parsed.url } : null;
827
+ } catch { pr = null; }
828
+ prCache.set(candidate.pr, pr);
829
+ }
830
+ const sha = pr?.mergeSha;
831
+ const prRepo = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+(?:\/|$)/i.exec(pr?.url ?? '')?.[1]?.toLowerCase();
832
+ const repositoryMatches = originRepo && prRepo ? originRepo === prRepo : null;
833
+ const ancestor = !mainFresh || !sha || !/^[a-f0-9]{40}$/i.test(sha) ? null : (() => {
834
+ const check = spawnSync('git', ['merge-base', '--is-ancestor', sha, 'origin/main'],
835
+ { cwd: process.cwd(), encoding: 'utf8', timeout: 20_000 });
836
+ return check.status === 0 ? true : check.status === 1 ? false : null;
837
+ })();
838
+ const decision = decideGreenSweep({ item, candidate, candidateCount: candidates.length,
839
+ pr: pr ?? null, ancestor, hold: holds === null ? null : holds.has(item.id), repositoryMatches,
840
+ ambiguousAcrossCells: ambiguousPrs.has(candidate.pr) });
841
+ observeGreenSweep(decision);
842
+ return decision;
843
+ });
844
+ });
845
+ const counts = { candidate: decisions.length, green: decisions.filter((d) => d.verdict === 'green').length,
846
+ recordOnly: decisions.filter((d) => d.verdict === 'record-only').length,
847
+ skipped: decisions.filter((d) => d.verdict === 'skip').length };
848
+ if (opts.json) out(JSON.stringify({ dryRun: true, version: opts.version, counts, decisions }));
849
+ else {
850
+ out(`dry-run · ${opts.version} · 후보 ${counts.candidate} · green ${counts.green} · 기록만 ${counts.recordOnly} · 건너뜀 ${counts.skipped}`);
851
+ for (const d of decisions) out(`${d.verdict}\t${d.cell}\t#${d.pr}\t${d.mergeSha ?? '-'}\t${d.reason}\t${d.evidence}\tbefore=${JSON.stringify(d.existingEvidence)}\tafter=${JSON.stringify(d.plannedEvidence)}`);
852
+ if (holds === null) out('⚠ hold 원장을 읽지 못해 green 없이 기록만 판정');
853
+ }
854
+ } catch (error) { out(`green-sweep 조회 실패: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
855
+ });
856
+
593
857
  tasks.command('cover')
594
858
  .description('TASK-AGENT 가 대신한 착지 비율과 리뷰·재발사·green 의 아직 없는 분모를 조회(읽기 전용)')
595
859
  .option('--since <iso>', '시작 시각 (ISO)')
@@ -597,7 +861,8 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
597
861
  .option('--json', 'JSON 출력')
598
862
  .option('--lines', '동사별 한 줄 요약 출력 (읽기 전용)')
599
863
  .option('--by-card', 'review·retry·green 분모를 카드 원장(창 안 createdAt 카드 수)으로 센다 (읽기 전용)')
600
- .action(async (opts: { since?: string; until?: string; json?: boolean; lines?: boolean; byCard?: boolean }) => {
864
+ .option('--include-test', '시험 우주 logs.db 도 운영 우주와 합산(기본은 운영만)')
865
+ .action(async (opts: { since?: string; until?: string; json?: boolean; lines?: boolean; byCard?: boolean; includeTest?: boolean }) => {
601
866
  const parseIso = (value: string | undefined, flag: string): number | undefined => {
602
867
  if (value === undefined) return undefined;
603
868
  const time = Date.parse(value);
@@ -617,18 +882,23 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
617
882
  }
618
883
  taskCards = coverCardsInWindow(cards, { sinceMs, untilMs });
619
884
  }
620
- const { rows, sevenVerbs, limitReached, unreadableStores } = readTaskAgentCover({ sinceMs, untilMs }, deps.coverLogs, taskCards, deps);
885
+ const scopedCoverLogs = (query: LogQuery) => (deps.coverLogs ?? queryCoverLogs)(query, opts.includeTest === true);
886
+ const { rows, sevenVerbs, landSignals, limitReached, unreadableStores } = readTaskAgentCover(
887
+ { sinceMs, untilMs }, scopedCoverLogs, taskCards, { ...deps, coverIncludeTest: opts.includeTest === true },
888
+ );
621
889
  const limitWarning = limitReached ? `조회 상한 ${COVER_QUERY_LIMIT} 도달 — 더 오래된 사건이 있을 수 있다(절단된 수를 전체로 읽지 말 것)` : null;
622
890
  const storeWarning = unreadableStores > 0 ? `logs.db ${unreadableStores}개를 못 읽었다 — 그 우주의 사건은 수에 없다(«없다»로 읽지 말 것)` : null;
623
- if (opts.json) out(JSON.stringify({ rows, sevenVerbs, limitReached, unreadableStores, ...(limitWarning ? { warning: limitWarning } : {}), ...(storeWarning ? { storeWarning } : {}) }));
891
+ if (opts.json) out(JSON.stringify({ rows, sevenVerbs, landSignals, limitReached, unreadableStores, ...(limitWarning ? { warning: limitWarning } : {}), ...(storeWarning ? { storeWarning } : {}) }));
624
892
  else {
625
893
  if (opts.lines) {
626
894
  for (const row of rows) out(formatTaskAgentCover(row));
627
895
  out(formatSevenVerbLive(sevenVerbs));
896
+ out(formatCoverLandSignals(landSignals));
628
897
  }
629
898
  else {
630
899
  out('동사\t대신\t전체\t비율\t상태\t관측 사건');
631
900
  for (const row of rows) out(`${row.verb}\t${row.byTaskAgent ?? '-'}\t${row.total ?? '-'}\t${row.ratio ?? '-'}\t${row.state}\t${row.observedActions ?? '-'}`);
901
+ out(formatCoverLandSignals(landSignals));
632
902
  }
633
903
  if (limitWarning) out(`⚠ ${limitWarning}`);
634
904
  if (storeWarning) out(`⚠ ${storeWarning}`);
@@ -820,6 +1090,7 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
820
1090
  ...(opts.childLlm !== undefined ? { childLlm: opts.childLlm } : {}),
821
1091
  ...(opts.childLlmEffort !== undefined ? { childLlmEffort: opts.childLlmEffort } : {}),
822
1092
  ...(deps.taskStatePath ? { statePath: deps.taskStatePath } : {}),
1093
+ ...(deps.taskCompositionRoot ? { compositionRoot: deps.taskCompositionRoot } : {}),
823
1094
  launcher: deps.taskLauncher ?? defaultTaskLauncher,
824
1095
  });
825
1096
  if (opts.json) out(JSON.stringify(result));
@@ -935,6 +1206,74 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
935
1206
  for (const row of report.rows) out([row.pid, row.ageHours, row.seat ?? '-', row.version ?? '-', row.runId ?? '-', row.reasons.join(',')].join('\t'));
936
1207
  });
937
1208
 
1209
+ tasks.command('adopt-pr <number>')
1210
+ .description('기존 OPEN/main PR을 TA 카드에 묶고 다음 tasks rejudge 대상에 넣는다(구현 발사 0)')
1211
+ .option('--card <id>', '기존 launched 카드(미바인딩 또는 같은 PR이 등록부에서 바인딩됨)')
1212
+ .option('--checklist <id>', '새 카드에 붙일 체크리스트 칸')
1213
+ .option('--version <version>', '체크리스트 판(기존 카드에도 고정 가능)')
1214
+ .option('--unlinked-terminal', '기존 카드의 모든 구현 자식이 종결되고 PR 사건이 전혀 없을 때만 별도로 열린 PR을 명시적으로 입양')
1215
+ .option('--json', 'JSON 출력')
1216
+ .action(async (value: string, opts: { card?: string; checklist?: string; version?: string; unlinkedTerminal?: boolean; json?: boolean }) => {
1217
+ await registerTaskAgentSink(deps.registerSink);
1218
+ try {
1219
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) throw new Error(`PR 번호가 아니다: ${value}`);
1220
+ if ((opts.card === undefined) === (opts.checklist === undefined))
1221
+ throw new Error('--card 또는 --checklist [--version] 중 하나만 준다');
1222
+ if (opts.unlinkedTerminal && !opts.card) throw new Error('--unlinked-terminal은 --card와 함께 준다');
1223
+ if (opts.version) validateVersion(opts.version);
1224
+ let cardVersion = opts.version;
1225
+ let observedChecklist: { id: string; version: string; status: string; cardVersion?: string } | undefined;
1226
+ if (opts.card) {
1227
+ const current = readTaskCard(opts.card, deps.taskStatePath ?? taskStatePathDefault());
1228
+ if (!current?.checklistId) throw new Error(`카드 ${opts.card}의 칸이 없다`);
1229
+ const versions = opts.version ? [opts.version] : [...new Set([checklistDevVersion(), ...(deps.releaseVersions?.() ?? assignedVersions())])];
1230
+ const matches = versions.filter((version) => {
1231
+ try { return (deps.adoptChecklist ?? listChecklist)(version).items.some((item) => item.id === current.checklistId); }
1232
+ catch (error) { if (opts.version) throw error; return false; }
1233
+ });
1234
+ if (matches.length !== 1) throw new Error(matches.length ? `칸 ${current.checklistId}가 여러 판에 있다 — --version 지정: ${matches.join(', ')}` : `체크리스트 칸 없음: ${current.checklistId}`);
1235
+ cardVersion = matches[0]!;
1236
+ const item = (deps.adoptChecklist ?? listChecklist)(cardVersion).items.find((candidate) => candidate.id === current.checklistId)!;
1237
+ if (item.status === 'green' || item.status === 'done') throw new Error(`이미 완료된 칸은 새 PR 입양 대상이 아니다: ${cardVersion}/${item.id}`);
1238
+ observedChecklist = { id: item.id, version: cardVersion, status: item.status, cardVersion: current.checklistVersion };
1239
+ }
1240
+ let checklist: { version: string; id: string; title: string; status: string } | undefined;
1241
+ if (opts.checklist) {
1242
+ const versions = opts.version ? [opts.version] : [...new Set([checklistDevVersion(), ...(deps.releaseVersions?.() ?? assignedVersions())])];
1243
+ const matches = versions.flatMap((version) => {
1244
+ try { return (deps.adoptChecklist ?? listChecklist)(version).items.filter((item) => item.id === opts.checklist).map((item) => ({ version, id: item.id, title: item.title, status: item.status })); }
1245
+ catch (error) { if (opts.version) throw error; return []; }
1246
+ });
1247
+ if (matches.length !== 1) throw new Error(matches.length ? `칸 ${opts.checklist}가 여러 판에 있다 — --version 지정: ${matches.map((item) => item.version).join(', ')}` : `체크리스트 칸 없음: ${opts.checklist}`);
1248
+ checklist = matches[0];
1249
+ }
1250
+ const number = Number(value);
1251
+ const origin = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: process.cwd(), encoding: 'utf8' }).trim().replace(/\.git\/?$/, '');
1252
+ const repo = origin.match(/^(?:git@github\.com:|https:\/\/github\.com\/|ssh:\/\/git@github\.com\/)([^/\s]+\/[^/\s]+)\/?$/)?.[1];
1253
+ if (!repo) throw new Error(`현재 worktree origin이 GitHub 저장소가 아니다: ${origin}`);
1254
+ const view = (deps.adoptPrView ?? ((pr: number, cwd: string) => JSON.parse(execFileSync('gh', ['pr', 'view', String(pr), '--repo', repo, '--json', 'number,url,state,baseRefName,headRefOid'], { cwd, encoding: 'utf8' }))))(number, process.cwd());
1255
+ if (view.number !== number) throw new Error(`조회 PR 번호가 다르다: 요청 #${number} · 응답 #${view.number}`);
1256
+ const { adoptExistingPr, adoptedRunTerminal } = await import('../task-agent/adopt-pr.js');
1257
+ const targetCard = opts.card ? readTaskCard(opts.card, deps.taskStatePath ?? taskStatePathDefault()) : undefined;
1258
+ const checkRun = deps.adoptRunTerminal ?? adoptedRunTerminal;
1259
+ const linkedEvidence = targetCard ? checkRun(targetCard, view) : undefined;
1260
+ const terminalRunEvidence = linkedEvidence || (opts.unlinkedTerminal && targetCard ? checkRun(targetCard, view, undefined, true) : false);
1261
+ const unlinkedTerminalUsed = opts.unlinkedTerminal === true && linkedEvidence !== true;
1262
+ const chosen = observedChecklist ?? checklist;
1263
+ const result = adoptExistingPr({ pr: view, repo, statePath: deps.taskStatePath ?? taskStatePathDefault(),
1264
+ ...(opts.card ? { cardId: opts.card, version: cardVersion, terminalRunEvidence, unlinkedTerminal: unlinkedTerminalUsed, observedChecklist,
1265
+ observedRun: { runId: targetCard?.runId, runChildId: targetCard?.runChildId, parentHost: targetCard?.parentHost?.host, launchId: targetCard?.launchId } } : {}),
1266
+ ...(chosen ? { checklistAtWrite: () => (deps.adoptChecklist ?? listChecklist)(chosen.version).items.find((item) => item.id === chosen.id) } : {}),
1267
+ ...(checklist ? { checklist } : {}) });
1268
+ try { debug.log('task-agent', 'pr-adopted', { card: result.card.id, pr: number, head: view.headRefOid, created: result.created, changed: result.changed, launched: false }); } catch { /* fail-soft */ }
1269
+ if (opts.json) out(JSON.stringify({ card: result.card.id, pr: number, head: view.headRefOid, created: result.created, changed: result.changed, next: `tasks rejudge ${result.card.id}` }));
1270
+ else out(`${result.card.id}\tPR #${number}\t${result.changed ? 'adopted' : 'already-adopted'}\t구현 발사 0 · 다음: tasks rejudge ${result.card.id}`);
1271
+ } catch (error) {
1272
+ out(error instanceof Error ? error.message : String(error));
1273
+ process.exitCode = 1;
1274
+ }
1275
+ });
1276
+
938
1277
  tasks.command('adopt-runs')
939
1278
  .description('TA-OWN-ALL-RUNS 백필 — 발사 등록부의 이 우주 런 중 카드 없는 것에 `ta-<runId>` 카드를 만든다(발사 훅 이전 런 몫 · 있으면 건드리지 않는다)')
940
1279
  .option('--json', 'JSON 출력')
@@ -1151,7 +1490,15 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
1151
1490
  const { rejudgeCandidates, rejudgeCardOnHead } = await import('../task-agent/rejudge-on-head.js');
1152
1491
  const statePath = deps.taskStatePath ?? taskStatePathDefault();
1153
1492
  let ids: string[];
1154
- try { ids = id ? [id] : rejudgeCandidates(readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {}); } catch (error) {
1493
+ try {
1494
+ if (!id) {
1495
+ const { bindMirroredRemoteRejudgeCards } = await import('../task-agent/remote-rejudge-bind.js');
1496
+ const cards = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {};
1497
+ const binding = await bindMirroredRemoteRejudgeCards(cards, statePath);
1498
+ try { debug.log('task-agent', 'remote-rejudge-bind', binding); } catch { /* fail-soft */ }
1499
+ }
1500
+ ids = id ? [id] : rejudgeCandidates(readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {});
1501
+ } catch (error) {
1155
1502
  out(error instanceof Error ? error.message : '과제 카드 읽기 실패'); process.exitCode = 1; return;
1156
1503
  }
1157
1504
  const { configuredTaskAgentLiveMoves } = await import('../task-agent/live-moves.js');
@@ -1206,6 +1553,26 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
1206
1553
  if (!result.passed) process.exitCode = 1;
1207
1554
  });
1208
1555
 
1556
+ tasks.command('repair-run', { hidden: true })
1557
+ .description('TA-REPAIR-SAME-BRANCH 내부 — source PR 머리에서 commit-only Pod 수리를 실행하고 결과를 파일로 남긴다')
1558
+ .requiredOption('--card <id>').requiredOption('--pr <n>').requiredOption('--head <sha>')
1559
+ .requiredOption('--repo <root>').requiredOption('--out <path>').requiredOption('--goal-file <path>')
1560
+ .requiredOption('--run-id <id>').requiredOption('--model-provider <provider>').requiredOption('--model-id <id>')
1561
+ .option('--seat <seat>')
1562
+ .action(async (opts: { card: string; pr: string; head: string; repo: string; out: string; goalFile: string;
1563
+ runId: string; modelProvider: string; modelId: string; seat?: string }) => {
1564
+ const { readFileSync } = await import('node:fs');
1565
+ const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
1566
+ const { effectiveInstanceRoot } = await import('../instance/resolve.js');
1567
+ const { runRepairWorker } = await import('../task-agent/repair-worker.js');
1568
+ const result = await runRepairWorker({ card: opts.card, pr: Number(opts.pr), head: opts.head,
1569
+ repo: opts.repo, resultPath: opts.out, entry: ELANOUS_CLI_ENTRY,
1570
+ configDir: getElanousConfigDirOverride() ?? effectiveInstanceRoot(), runId: opts.runId,
1571
+ model: { provider: opts.modelProvider, model: opts.modelId }, seat: opts.seat,
1572
+ goal: readFileSync(opts.goalFile, 'utf8') });
1573
+ if (!result.ok) process.exitCode = 1;
1574
+ });
1575
+
1209
1576
  tasks.command('approve <id...>')
1210
1577
  .description('외부 태스크 승인')
1211
1578
  .action(async (ids: string[]) => {
@@ -78,6 +78,8 @@ type AxisUsageRow = {
78
78
  unknownCostCalls: number;
79
79
  includedCalls: number;
80
80
  apiEquivalentUsd: number;
81
+ /** 비용 순위 = usdKnown + apiEquivalentUsd — 구독 호출의 목록가 «그림자 비용»까지 더한 값. */
82
+ rankUsd: number;
81
83
  };
82
84
 
83
85
  function usageData(row: LogStoreRow, unwrapPod = false): ExtendedRunUsageInput | null {
@@ -115,6 +117,8 @@ function rollupByAxis(inputs: readonly ExtendedRunUsageInput[], axis: UsageAxis)
115
117
  }
116
118
  return [...groups].map(([value, group]) => {
117
119
  const rows = rollupRunUsage(group);
120
+ const usdKnown = rows.reduce((sum, row) => sum + row.usdKnown, 0);
121
+ const apiEquivalentUsd = rows.reduce((sum, row) => sum + row.apiEquivalentUsd, 0);
118
122
  const result: AxisUsageRow = {
119
123
  [axis]: value,
120
124
  hostIds: [...new Set(rows.flatMap((row) => row.hostIds))],
@@ -124,13 +128,14 @@ function rollupByAxis(inputs: readonly ExtendedRunUsageInput[], axis: UsageAxis)
124
128
  cacheReadInputTokens: rows.reduce((sum, row) => sum + row.cacheReadInputTokens, 0),
125
129
  cacheCreationInputTokens: rows.reduce((sum, row) => sum + row.cacheCreationInputTokens, 0),
126
130
  reasoningOutputTokens: rows.reduce((sum, row) => sum + row.reasoningOutputTokens, 0),
127
- usdKnown: rows.reduce((sum, row) => sum + row.usdKnown, 0),
131
+ usdKnown,
128
132
  unknownCostCalls: rows.reduce((sum, row) => sum + row.unknownCostCalls, 0),
129
133
  includedCalls: rows.reduce((sum, row) => sum + row.includedCalls, 0),
130
- apiEquivalentUsd: rows.reduce((sum, row) => sum + row.apiEquivalentUsd, 0),
134
+ apiEquivalentUsd,
135
+ rankUsd: usdKnown + apiEquivalentUsd,
131
136
  };
132
137
  return result;
133
- }).sort((a, b) => b.calls - a.calls);
138
+ }).sort((a, b) => (b.rankUsd - a.rankUsd) || (b.calls - a.calls));
134
139
  }
135
140
 
136
141
  function isUsageAxis(value: string): value is 'run' | 'role' | UsageAxis {
@@ -139,7 +144,7 @@ function isUsageAxis(value: string): value is 'run' | 'role' | UsageAxis {
139
144
 
140
145
  function formatAxisUsage(row: AxisUsageRow, axis: UsageAxis): string {
141
146
  const value = row[axis] ?? '(none)';
142
- return `${axis}=${value} hosts=${row.hostIds.join(',') || '(none)'} calls=${row.calls} input=${row.inputTokens} output=${row.outputTokens} cacheRead=${row.cacheReadInputTokens} cacheCreation=${row.cacheCreationInputTokens} reasoning=${row.reasoningOutputTokens} usdKnown=${row.usdKnown} unknownCostCalls=${row.unknownCostCalls} includedCalls=${row.includedCalls} apiEquivalentUsd=${row.apiEquivalentUsd}`;
147
+ return `${axis}=${value} hosts=${row.hostIds.join(',') || '(none)'} calls=${row.calls} input=${row.inputTokens} output=${row.outputTokens} cacheRead=${row.cacheReadInputTokens} cacheCreation=${row.cacheCreationInputTokens} reasoning=${row.reasoningOutputTokens} usdKnown=${row.usdKnown} unknownCostCalls=${row.unknownCostCalls} includedCalls=${row.includedCalls} apiEquivalentUsd=${row.apiEquivalentUsd} rankUsd=${row.rankUsd}`;
143
148
  }
144
149
 
145
150
  function defaultCreditPlan(): CodexCreditPlan | null {