elanous 0.2.18 → 0.2.20

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 (350) 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-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
  14. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
  65. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  66. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  67. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -1
  87. package/apps/pwa/out/decisions/index.txt +14 -14
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +16 -15
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +16 -15
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +24 -23
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/demo/editor-recipes.yaml +22 -0
  160. package/graphs/ops/guardian.yaml +26 -0
  161. package/graphs/ops/recipes.yaml +3 -0
  162. package/graphs/retro/daily.yaml +25 -0
  163. package/graphs/retro/recipes.yaml +24 -0
  164. package/package.json +1 -1
  165. package/scripts/ask-marker-check.ts +3 -1
  166. package/scripts/coord-channel-watch.sh +2 -2
  167. package/scripts/discord-seat-probe.ts +3 -1
  168. package/scripts/land-global-checks.ts +89 -0
  169. package/scripts/lib/pch-regress-map.ts +24 -0
  170. package/scripts/morning-report.ts +18 -4
  171. package/scripts/pch-regress.ts +88 -0
  172. package/scripts/release-loop/gate-diff.ts +18 -0
  173. package/scripts/release-loop/gate-node.ts +628 -65
  174. package/scripts/release-loop/gate-partial.ts +106 -0
  175. package/scripts/release-loop/gate-timing.ts +229 -0
  176. package/scripts/release-loop/preflight.ts +6 -4
  177. package/scripts/release-loop/publish-window.ts +141 -0
  178. package/scripts/release-loop/release-docs.ts +1 -4
  179. package/scripts/release-loop/release-universe.ts +38 -0
  180. package/scripts/release-loop/resume-release.ts +67 -5
  181. package/scripts/release-loop/shard-plan.ts +65 -0
  182. package/scripts/release-loop/unattended-release.ts +12 -2
  183. package/scripts/release-story/draft.ts +5 -1
  184. package/scripts/retro/shadow-compare.ts +71 -0
  185. package/scripts/rhythm/daily.ts +6 -1
  186. package/scripts/video/scene-table.ts +159 -0
  187. package/src/acp/core-turn-bridge.ts +39 -2
  188. package/src/agent-mission/driver.ts +34 -6
  189. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  190. package/src/autopilot/build/merge-intent.ts +135 -7
  191. package/src/away/presence.ts +40 -0
  192. package/src/away/release-watch.ts +272 -0
  193. package/src/chat/index.ts +2 -1
  194. package/src/chat/short-question.ts +3 -40
  195. package/src/cli/away-cli.ts +68 -0
  196. package/src/cli/config-drift.ts +513 -0
  197. package/src/cli/guardian-cli.ts +49 -0
  198. package/src/cli/intake-cli.ts +13 -7
  199. package/src/cli/log-event-names.ts +5 -0
  200. package/src/cli/pod-cli.ts +5 -1
  201. package/src/cli/pr-cli.ts +20 -0
  202. package/src/cli/pty-drive-cli.ts +14 -3
  203. package/src/cli/release-cli.ts +52 -12
  204. package/src/cli/retro-cli.ts +78 -0
  205. package/src/cli/setup-cli.ts +31 -1
  206. package/src/cli/tasks-cli.ts +193 -14
  207. package/src/context-bus/context-now-public.ts +1 -1
  208. package/src/context-bus/context-now-surfaces.ts +16 -6
  209. package/src/context-bus/context-now.ts +97 -3
  210. package/src/core-turn/short-question.ts +100 -0
  211. package/src/dashboard/dashboard-status-lines.ts +24 -4
  212. package/src/dashboard/index.ts +50 -5
  213. package/src/dashboard/input/slash-executor.ts +6 -2
  214. package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
  215. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
  217. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  218. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  219. package/src/domains/guardian-tick.ts +138 -0
  220. package/src/domains/outbound-alert.ts +44 -13
  221. package/src/domains/outbound-log-probe.ts +6 -1
  222. package/src/domains/schedule-registry.ts +23 -0
  223. package/src/domains/telegram-kind-route.ts +39 -0
  224. package/src/domains/watch-default-scheduler.ts +95 -0
  225. package/src/esc-abort-gate.ts +44 -0
  226. package/src/graph-runner/graph-run-api.ts +205 -0
  227. package/src/graph-runner/runner.ts +36 -6
  228. package/src/harness/harness-cli-command.ts +420 -51
  229. package/src/harness/harness-queue.ts +315 -25
  230. package/src/harness/harness-salvage-cli.ts +140 -0
  231. package/src/harness/harness-worktree-add.ts +68 -0
  232. package/src/harness/ledger-write-guard.ts +6 -16
  233. package/src/hq/fence-outcomes.ts +63 -0
  234. package/src/hq/fence-wrapper.ts +33 -0
  235. package/src/hq/hq.ts +17 -2
  236. package/src/index.ts +118 -6
  237. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  238. package/src/instance/test-write-guard.ts +200 -0
  239. package/src/intake-plane/digest.ts +39 -15
  240. package/src/intake-plane/harness-target.ts +4 -3
  241. package/src/intake-plane/intake-sources.ts +1 -0
  242. package/src/intake-plane/items.ts +1 -0
  243. package/src/loops/orchestrator/degradation.ts +57 -0
  244. package/src/loops/orchestrator/finish-rate.ts +33 -3
  245. package/src/loops/orchestrator/tick.ts +166 -45
  246. package/src/maturity/feature-maturity.ts +6 -4
  247. package/src/maturity/tui-slash-maturity.ts +1 -0
  248. package/src/mss/logging/log-store.ts +3 -0
  249. package/src/nexus/api/graph-kinds.ts +10 -4
  250. package/src/nexus/api/graphs-api.ts +178 -6
  251. package/src/nexus/api/http-server.ts +54 -8
  252. package/src/nexus/api/loop-edges.ts +292 -17
  253. package/src/nexus/api/ops-api.ts +112 -29
  254. package/src/nexus/api/ops-board-route.ts +48 -0
  255. package/src/nexus/index.ts +7 -0
  256. package/src/onboarding/entry-hints.ts +5 -0
  257. package/src/onboarding.ts +24 -6
  258. package/src/release-loop/checklist.ts +2 -0
  259. package/src/release-loop/feature-store.ts +68 -18
  260. package/src/release-loop/gate-shards.ts +158 -0
  261. package/src/release-loop/light-rc.ts +173 -0
  262. package/src/release-loop/placement.ts +51 -17
  263. package/src/release-loop/release-schedule.ts +6 -2
  264. package/src/release-loop/rubric.ts +57 -1
  265. package/src/retro/daily.ts +372 -0
  266. package/src/seat-loop/op-decide.ts +173 -0
  267. package/src/seat-loop/seat-loop.ts +60 -3
  268. package/src/seat-loop/seat-report.ts +58 -3
  269. package/src/self-dev/ask-launch-flow.ts +5 -4
  270. package/src/self-dev/ask-launch-io.ts +49 -1
  271. package/src/self-dev/ask-path-candidates.ts +40 -2
  272. package/src/self-dev/dev-cli.ts +36 -0
  273. package/src/self-dev/draft-sweep.ts +371 -16
  274. package/src/self-dev/draft-triage-rules.ts +19 -4
  275. package/src/self-dev/orchestrate.ts +1 -0
  276. package/src/self-dev/pr-preview.ts +247 -0
  277. package/src/self-dev/shard-run.ts +23 -7
  278. package/src/self-implement/blocked-draft-policy.ts +101 -0
  279. package/src/self-implement/execution-clarification.ts +11 -0
  280. package/src/self-implement/gate-cli.ts +29 -8
  281. package/src/self-implement/gate-env-retry.ts +83 -0
  282. package/src/self-implement/goal-author-clarification.ts +88 -13
  283. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  284. package/src/self-implement/orchestrator.ts +220 -6
  285. package/src/self-implement/run-stop.ts +123 -0
  286. package/src/self-implement/run-ttl.ts +258 -0
  287. package/src/self-implement/salvage-branches.ts +254 -0
  288. package/src/self-implement/salvage-retention.ts +123 -0
  289. package/src/self-implement/seams.ts +4 -0
  290. package/src/self-implement/self-implement-cli.ts +2 -0
  291. package/src/self-implement/sibling-resync.ts +223 -0
  292. package/src/self-implement/stop-autoheal.ts +204 -0
  293. package/src/task-agent/actions.ts +63 -2
  294. package/src/task-agent/board.ts +132 -0
  295. package/src/task-agent/card-evidence.ts +292 -0
  296. package/src/task-agent/completion-evidence.ts +126 -0
  297. package/src/task-agent/judge.ts +25 -0
  298. package/src/task-agent/live-moves.ts +276 -0
  299. package/src/task-agent/mission.ts +33 -7
  300. package/src/task-agent/shadow.ts +192 -15
  301. package/src/task-agent/task-hand.ts +124 -17
  302. package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
  303. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  304. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
  308. package/src/telegram-away-command.ts +38 -0
  309. package/src/telegram-commands.ts +52 -0
  310. package/src/telegram-report-log-probe.ts +21 -0
  311. package/src/telegram-report.ts +27 -3
  312. package/src/testing/real-cli-spawn.ts +106 -0
  313. package/src/tui-client/dashboard-session.ts +1 -0
  314. package/src/user-config.ts +190 -5
  315. package/src/version/packed-revision.json +1 -1
  316. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  324. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  349. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
  350. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -1,7 +1,8 @@
1
- import { execFileSync, spawn } from 'node:child_process';
1
+ import { execFile, execFileSync, spawn } from 'node:child_process';
2
+ import { promisify } from 'node:util';
2
3
  import { dlopen, FFIType } from 'bun:ffi';
3
4
  import { createHash } from 'node:crypto';
4
- import { existsSync, readFileSync } from 'node:fs';
5
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
5
6
  import { constants as osConstants, hostname } from 'node:os';
6
7
  import { basename, dirname, join, resolve, sep } from 'node:path';
7
8
  import { findGitDir } from '../git-fs/locate.js';
@@ -16,13 +17,15 @@ import { Command, Option } from 'commander';
16
17
  import { runGitCommand } from '../git-fs/runner.js';
17
18
  import { GOAL_TYPES, lintGoalFile, parseGoalId, parseGoalType, resolveGoalAuthorGrade, tracedPathReferences, type GoalAuthorGrade, type GoalAuthorGradeSelection, type GoalType } from '../self-implement/goal-author.js';
18
19
  import { templateForGoalType } from '../self-implement/graph-templates.js';
19
- import { listRunLedgers, loadFederatedRunLedger, loadRunLedger, runLedgerDir, type RunLedgerMatch } from '../self-implement/run-ledger.js';
20
+ import { listRunLedgers, loadFederatedRunLedger, loadRunLedger, queryRunScreenKey, runLedgerDir, type RunLedgerMatch } from '../self-implement/run-ledger.js';
21
+ import { classifyPodExitStop, recordRunStop, runStopRecorded, RUN_STOP_EVENT } from '../self-implement/run-stop.js';
20
22
  import { resolveChildLlmEffort, resolveImplementationChildModel } from '../self-dev/dev-cli.js';
21
23
  import { resolveHarnessTarget } from '../self-implement/harness-target-options.js';
22
24
  import { queryRunningRuns } from '../self-implement/running-runs.js';
23
25
  import { DevPipelineError } from '../self-dev/dev-pipeline.js';
24
- import { runDraftSweep, sweepFailureReason, type DraftSweepAdapters, type DraftSweepResult, type SweepDraft, type SweepMergedPr, type SweepReviewGate } from '../self-dev/draft-sweep.js';
26
+ import { DRAFT_SWEEP_CONCURRENCY, mapBounded, runDraftSweep, sweepFailureReason, type DraftSweepAdapters, type DraftSweepResult, type SweepDraft, type SweepMergedPr, type SweepReviewGate } from '../self-dev/draft-sweep.js';
25
27
  import { debug } from '../debug/log.js';
28
+ import { installHarnessSalvageCommand, installHarnessSalvageRetentionCommand } from './harness-salvage-cli.js';
26
29
  import { decideNestedElanousLaunch, readNestedElanousDepth } from './nested-elanous-policy.js';
27
30
  import { dispatchTask, type DispatchTaskInput, type DispatchTaskDeps } from '../execution-loop/dispatch-task.js';
28
31
  import { launchRequestId, preLaunchGate, type PreLaunchGateDeps } from '../execution-loop/launch-gate.js';
@@ -31,6 +34,9 @@ import { CODEX_PROVIDER, GROK_PROVIDER, decideBudget, decideLaunchBudget, readBu
31
34
  import { DEFAULT_FALLBACK_CHAIN } from '../oauth/fallback-chain.js';
32
35
  import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDeps } from './deliverable-verify-cli.js';
33
36
  import { getUserConfig } from '../user-config.js';
37
+ import type { SiblingResyncAdapters, SiblingResyncResult, SiblingRunTarget } from '../self-implement/sibling-resync.js';
38
+ import type { RunTtlAdapters, RunTtlResult } from '../self-implement/run-ttl.js';
39
+ import type { ControlMemoPayload } from './control-inbox.js';
34
40
  import { resolveRepositoryName } from './repository-name.js';
35
41
  import { installHarnessCliSinkHook } from './harness-cli-sink.js';
36
42
  import { addHarnessQueue, HarnessQueueDuplicateError, harnessQueueReceiptPath, listHarnessQueue, queueSeatForCwd, reconcileHarnessQueue, removeHarnessQueue, setHarnessQueuePriority, tickHarnessQueue, type HarnessQueueDeps, type QueueItem, type QueueSeat } from './harness-queue.js';
@@ -669,11 +675,40 @@ async function onPod(opts: unknown, entrance: 'cli-harness-ask' | 'cli-harness-s
669
675
  const classified = classifyHarnessPodExit({ status }, output, { ...(runId ? { runId } : {}) });
670
676
  recordClassifiedHarnessPodExit(runId ?? harnessPodRunId(output), classified.reason, status,
671
677
  { entrance, seat: o.seat, output });
678
+ recordHarnessPodStop(runId ?? harnessPodRunId(output), status, classified);
672
679
  for (const line of classified.lines) console.error(line);
673
680
  process.exitCode = status;
674
681
  }
675
682
  }
676
683
 
684
+ /**
685
+ * STOP-RECORD — a nonzero Pod exit appends one `stop` line to the host run ledger (additive: the
686
+ * `exit-classified` log and the existing ledger stay as they are). A failed Pod Job loses its in-Pod
687
+ * ledger, so without this line the run reads `outcome: unknown`. Skips when a stop is already recorded.
688
+ */
689
+ export function recordHarnessPodStop(
690
+ runId: string | undefined,
691
+ status: number,
692
+ classified: { reason: string; lines: string[] },
693
+ deps: { loadLedger?: typeof loadRunLedger; record?: typeof recordRunStop } = {},
694
+ ): boolean {
695
+ if (!runId) return false;
696
+ try {
697
+ if (runStopRecorded(runId)) return false;
698
+ if ((deps.loadLedger ?? loadRunLedger)(runId)?.some((entry) => entry.event === RUN_STOP_EVENT)) return false;
699
+ // The classified line is our own one-line verdict («Pod 안 자식이 실패했다 — OOMKilled»), not the raw output tail.
700
+ const verdict = classified.lines[0]?.split(' — ').slice(1).join(' — ').trim();
701
+ (deps.record ?? recordRunStop)({
702
+ runId, site: 'pod-exit', class: classifyPodExitStop(classified.reason),
703
+ cause: `Pod exit ${status}: ${classified.reason}${verdict ? ` — ${verdict}` : ''}`,
704
+ evidenceRef: `logs:harness.pod/exit-classified:${runId}`,
705
+ nextMove: classified.reason === 'no-launch' ? 'Check Pod launch gate and pool, then relaunch'
706
+ : 'Inspect Pod exit classification and harvest the branch before relaunch',
707
+ });
708
+ return true;
709
+ } catch { return false; /* ledger observation cannot change Pod output or exit */ }
710
+ }
711
+
677
712
  const LAUNCH_BUDGET_PROVIDER: Readonly<Record<string, string>> = { 'codex-rotate': CODEX_PROVIDER, grok: GROK_PROVIDER };
678
713
 
679
714
  /**
@@ -1800,7 +1835,7 @@ type GithubPull = {
1800
1835
  title: string;
1801
1836
  draft: boolean;
1802
1837
  state: 'open' | 'closed';
1803
- head: { ref: string };
1838
+ head: { ref: string; sha?: string };
1804
1839
  base?: { ref: string };
1805
1840
  labels: Array<{ name: string }>;
1806
1841
  created_at: string;
@@ -1843,6 +1878,147 @@ export async function supersedeMergedGoalDrafts(
1843
1878
  await supersedeDraftsOnMerge({ repository, merged, adapters: githubDraftSweepAdapters(execute, git, cwd) });
1844
1879
  }
1845
1880
 
1881
+ /**
1882
+ * SIBLING-RESYNC-ON-MERGE — merge-time adapter: open harness PRs whose files intersect the merged PR get a
1883
+ * resync memo in their live run's control inbox, or the quiet `elanous:needs-rebase` marker when no run is alive.
1884
+ */
1885
+ export async function resyncMergedSiblings(
1886
+ number: number, cwd: string,
1887
+ execute: GhExecute = executeGh, git: DraftSweepGitExecute = runGitCommand,
1888
+ overrides: Partial<Pick<SiblingResyncAdapters, 'resolveRun' | 'sendResync'>> & {
1889
+ maxFetch?: number; fetchConcurrency?: number; log?: (category: string, event: string, data: Record<string, unknown>) => void;
1890
+ } = {},
1891
+ ): Promise<SiblingResyncResult> {
1892
+ const remote = git(cwd, ['config', '--get', 'remote.origin.url']);
1893
+ const repository = remote.status === 0 ? githubRemoteRepository(remote.stdout) : undefined;
1894
+ if (!repository) throw new Error('Merge repository unavailable');
1895
+ const sweep = githubDraftSweepAdapters(execute, git, cwd);
1896
+ const [{ requestSiblingResync }, inbox, pod] = await Promise.all([
1897
+ import('../self-implement/sibling-resync.js'),
1898
+ import('./control-inbox.js'),
1899
+ import('./self-send-target.js'),
1900
+ ]);
1901
+ const maxFetch = overrides.maxFetch ?? (() => {
1902
+ try { return getUserConfig().harness?.siblingResync?.maxFetch; } catch { return undefined; }
1903
+ })();
1904
+ let ledgerMatches: readonly RunLedgerMatch[] | undefined;
1905
+ return requestSiblingResync({
1906
+ merged: { number },
1907
+ ...(maxFetch !== undefined ? { maxFetch } : {}),
1908
+ ...(overrides.fetchConcurrency !== undefined ? { fetchConcurrency: overrides.fetchConcurrency } : {}),
1909
+ ...(overrides.log ? { log: overrides.log } : {}),
1910
+ adapters: {
1911
+ mergedFiles: async () => sweep.getPrFiles!(repository, number),
1912
+ listOpenPrs: async () => {
1913
+ const all: GithubPull[] = [];
1914
+ for (let page = 1; page <= 100; page++) {
1915
+ const batch = githubPullsPage(repository, 'open', page, execute);
1916
+ if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete open PR inventory');
1917
+ all.push(...batch);
1918
+ if (batch.length < 100) break;
1919
+ if (page === 100) throw new Error('Open PR inventory exceeded pagination limit');
1920
+ }
1921
+ // No per-PR call here: the core prefilters, prioritizes and caps file fetches (#24893 review should-fix ①).
1922
+ return all.filter((pr) => pr.base?.ref === 'main' && pr.head?.ref?.startsWith('self-impl/') && pr.number !== number)
1923
+ .map((pr) => ({ number: pr.number, branch: pr.head.ref, labels: pr.labels.map((label) => label.name) }));
1924
+ },
1925
+ prFiles: async (prNumber) => sweep.getPrFiles!(repository, prNumber),
1926
+ resolveRun: overrides.resolveRun ?? (async (pr) => {
1927
+ ledgerMatches ??= listRunLedgers().matches;
1928
+ return resolveOwningRunForPr(ledgerMatches, repository, pr.number, 'sibling-resync');
1929
+ }),
1930
+ sendResync: overrides.sendResync ?? (({ spaceId }, memo) => deliverResyncMemo(spaceId, memo, inbox, pod)),
1931
+ addMarker: async (prNumber, label) => sweep.setLabels(repository, prNumber, { add: label, remove: [] }),
1932
+ },
1933
+ });
1934
+ }
1935
+
1936
+ /** The PR's owning run, alive only when running and its control screen resolves (shared by SIBLING-RESYNC and RUN-TTL). */
1937
+ function resolveOwningRunForPr(ledgerMatches: readonly RunLedgerMatch[], repository: string, number: number,
1938
+ caller: string): SiblingRunTarget {
1939
+ const runId = currentDraftSweepRunId(ledgerMatches, repository, number);
1940
+ if (!runId) return { alive: false, reason: 'owner-run-unknown' };
1941
+ const status = queryRunningRuns({ runIds: [runId], caller }).entries.find((entry) => entry.runId === runId)?.status;
1942
+ if (status !== 'running' && status !== 'probable-running') return { alive: false, runId, reason: `run-${status ?? 'not-running'}` };
1943
+ const screen = queryRunScreenKey(runId).screenKey;
1944
+ if (!screen) return { alive: false, runId, reason: 'screen-key-unresolved' };
1945
+ return { alive: true, runId, spaceId: screen };
1946
+ }
1947
+
1948
+ /** One supervisor memo into a live run's control inbox — Pod runs through their fragment, host runs through the file inbox. */
1949
+ function deliverResyncMemo(spaceId: string, memo: ControlMemoPayload,
1950
+ inbox: typeof import('./control-inbox.js'), pod: typeof import('./self-send-target.js')): void {
1951
+ if (pod.readPodFragment(spaceId)) {
1952
+ if (!pod.dispatchPodSelfSend(spaceId, { memo })) throw new Error('pod fragment unreachable');
1953
+ return;
1954
+ }
1955
+ inbox.enqueueControlMemo(spaceId, memo, { explicitInboxDir: inbox.controlInboxPath(spaceId) });
1956
+ }
1957
+
1958
+ const execFileAsync = promisify(execFile);
1959
+ const executeGhAsync = async (args: string[]): Promise<string> => (await execFileAsync('gh', args, {
1960
+ encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024, env: process.env,
1961
+ })).stdout;
1962
+
1963
+ /** RUN-TTL adapters: open self-impl PRs, head commit time (async, bounded by the core), owning run, memo, label. */
1964
+ export async function githubRunTtlAdapters(repository: string, execute: GhExecute = executeGh,
1965
+ executeAsync: (args: string[]) => Promise<string> = executeGhAsync, root: string = effectiveInstanceRoot(),
1966
+ shared: Partial<Pick<GithubDraftSweepAdapters, 'openPullRequests' | 'runLedgerMatches'>> = {}): Promise<RunTtlAdapters> {
1967
+ const [inbox, pod] = await Promise.all([import('./control-inbox.js'), import('./self-send-target.js')]);
1968
+ const ledger = join(root, 'run-ttl', 'memos.jsonl');
1969
+ let sent: Set<string> | undefined;
1970
+ const loadSent = (): Set<string> => {
1971
+ if (sent) return sent;
1972
+ sent = new Set<string>();
1973
+ try {
1974
+ for (const line of readFileSync(ledger, 'utf8').split('\n')) {
1975
+ if (!line.trim()) continue;
1976
+ try { const key = (JSON.parse(line) as { key?: unknown }).key; if (typeof key === 'string') sent.add(key); } catch { /* skip */ }
1977
+ }
1978
+ } catch (error) {
1979
+ // Only a missing journal is «nothing sent yet»; an unreadable one cannot prove «once».
1980
+ if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') { sent = undefined; throw error; }
1981
+ }
1982
+ return sent;
1983
+ };
1984
+ let ledgerMatches: readonly RunLedgerMatch[] | undefined;
1985
+ return {
1986
+ listOpenPrs: async () => {
1987
+ const all: GithubPull[] = [];
1988
+ // Inside the draft sweep the open listing is already in memory — one listing per sweep (DRAFT-SWEEP-SLOW).
1989
+ if (shared.openPullRequests) all.push(...await shared.openPullRequests(repository));
1990
+ else for (let page = 1; page <= 100; page++) {
1991
+ const batch = githubPullsPage(repository, 'open', page, execute);
1992
+ if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete open PR inventory');
1993
+ all.push(...batch);
1994
+ if (batch.length < 100) break;
1995
+ if (page === 100) throw new Error('Open PR inventory exceeded pagination limit');
1996
+ }
1997
+ return all.filter((pr) => pr.base?.ref === 'main' && pr.head?.ref?.startsWith('self-impl/') && typeof pr.head.sha === 'string')
1998
+ .map((pr) => ({ number: pr.number, branch: pr.head.ref, labels: pr.labels.map((label) => label.name),
1999
+ createdAt: pr.created_at, headSha: pr.head.sha! }));
2000
+ },
2001
+ lastCommitAt: async (pr) => {
2002
+ const rows = JSON.parse(await executeAsync(['api', `repos/${repository}/commits?sha=${pr.headSha}&per_page=1`])) as
2003
+ Array<{ sha?: string; commit?: { committer?: { date?: string } } }>;
2004
+ const head = Array.isArray(rows) ? rows[0] : undefined;
2005
+ return head?.sha === pr.headSha ? head.commit?.committer?.date : undefined;
2006
+ },
2007
+ resolveRun: async (pr) => {
2008
+ ledgerMatches ??= shared.runLedgerMatches?.() ?? listRunLedgers().matches;
2009
+ return resolveOwningRunForPr(ledgerMatches, repository, pr.number, 'run-ttl');
2010
+ },
2011
+ sendMemo: ({ spaceId }, memo) => deliverResyncMemo(spaceId, memo, inbox, pod),
2012
+ addLabel: async (prNumber, label) => { execute(['pr', 'edit', String(prNumber), '--repo', repository, '--add-label', label]); },
2013
+ memoSent: (key) => loadSent().has(key),
2014
+ recordMemoSent: (key) => {
2015
+ mkdirSync(dirname(ledger), { recursive: true });
2016
+ appendFileSync(ledger, `${JSON.stringify({ key, at: new Date().toISOString() })}\n`);
2017
+ loadSent().add(key);
2018
+ },
2019
+ };
2020
+ }
2021
+
1846
2022
  function ghJson<T>(args: string[], execute: GhExecute): T {
1847
2023
  return JSON.parse(execute(args)) as T;
1848
2024
  }
@@ -1907,7 +2083,94 @@ function githubRemoteRepository(url: string): string | undefined {
1907
2083
  return match?.[1];
1908
2084
  }
1909
2085
 
1910
- export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: DraftSweepGitExecute = runGitCommand, cwd = process.cwd()): DraftSweepAdapters {
2086
+ /** One PR's sweep evidence read in a GraphQL batch. A `null` field was not complete in the batch ⇒ its REST path is used. */
2087
+ interface GraphqlPrDetail {
2088
+ readonly files: string[] | null;
2089
+ readonly comments: string[] | null;
2090
+ readonly reviews: Array<{ state?: string; body?: string }> | null;
2091
+ readonly headSha: string | null;
2092
+ readonly mergeCommit: { oid: string; message: string } | null;
2093
+ }
2094
+
2095
+ /** PRs per GraphQL request: three nested connections × 100 nodes stays far under the node limit and costs ~1 point. */
2096
+ const DRAFT_SWEEP_GRAPHQL_BATCH = 40;
2097
+
2098
+ type GraphqlConnection<T> = { totalCount?: number; nodes?: T[] | null; pageInfo?: { hasNextPage?: boolean } } | null | undefined;
2099
+ const completeNodes = <T>(connection: GraphqlConnection<T>): T[] | null =>
2100
+ connection && Array.isArray(connection.nodes) && connection.pageInfo?.hasNextPage === false
2101
+ && (connection.totalCount === undefined || connection.totalCount === connection.nodes.length) ? connection.nodes : null;
2102
+
2103
+ /**
2104
+ * Batched PR evidence (files · issue comments · reviews · head · merge commit) — one GraphQL request per
2105
+ * DRAFT_SWEEP_GRAPHQL_BATCH PRs instead of 3–7 REST calls per PR (DRAFT-SWEEP-SLOW, 10-08: ~1,300 PRs ⇒ >600 s).
2106
+ * Fail-soft: an unreadable batch or PR yields no entry, and the caller falls back to the per-PR REST path.
2107
+ */
2108
+ async function graphqlPrDetails(repository: string, numbers: readonly number[],
2109
+ executeAsync: (args: string[]) => Promise<string>): Promise<Map<number, GraphqlPrDetail>> {
2110
+ const details = new Map<number, GraphqlPrDetail>();
2111
+ const [owner, name] = repository.split('/');
2112
+ if (!owner || !name || numbers.length === 0) return details;
2113
+ const batches: number[][] = [];
2114
+ for (let index = 0; index < numbers.length; index += DRAFT_SWEEP_GRAPHQL_BATCH) batches.push(numbers.slice(index, index + DRAFT_SWEEP_GRAPHQL_BATCH));
2115
+ let failedBatches = 0;
2116
+ // Fields the batch could not prove complete (each one is read again over REST by the caller).
2117
+ const incompleteFields = { files: 0, comments: 0, reviews: 0 };
2118
+ await mapBounded(batches, DRAFT_SWEEP_CONCURRENCY, async (batch) => {
2119
+ const fields = batch.map((number) => `p${number}: pullRequest(number: ${number}) { number headRefOid `
2120
+ + 'files(first: 100) { totalCount nodes { path } pageInfo { hasNextPage } } '
2121
+ + 'comments(first: 100) { totalCount nodes { body } pageInfo { hasNextPage } } '
2122
+ + 'reviews(first: 100) { totalCount nodes { state body } pageInfo { hasNextPage } } '
2123
+ + 'mergeCommit { oid message } }').join(' ');
2124
+ const query = `query { repository(owner: ${JSON.stringify(owner)}, name: ${JSON.stringify(name)}) { ${fields} } }`;
2125
+ let repositoryNode: Record<string, unknown> | undefined;
2126
+ try {
2127
+ const parsed = JSON.parse(await executeAsync(['api', 'graphql', '-f', `query=${query}`])) as { data?: { repository?: Record<string, unknown> } };
2128
+ repositoryNode = parsed?.data?.repository ?? undefined;
2129
+ } catch { repositoryNode = undefined; }
2130
+ if (!repositoryNode || typeof repositoryNode !== 'object' || Array.isArray(repositoryNode)) { failedBatches += 1; return; }
2131
+ for (const number of batch) {
2132
+ const pr = repositoryNode[`p${number}`] as {
2133
+ number?: number; headRefOid?: string;
2134
+ files?: GraphqlConnection<{ path?: string }>; comments?: GraphqlConnection<{ body?: string }>;
2135
+ reviews?: GraphqlConnection<{ state?: string; body?: string }>; mergeCommit?: { oid?: string; message?: string } | null;
2136
+ } | null | undefined;
2137
+ if (!pr || pr.number !== number) continue;
2138
+ const files = completeNodes(pr.files);
2139
+ const comments = completeNodes(pr.comments);
2140
+ const reviews = completeNodes(pr.reviews);
2141
+ const detail: GraphqlPrDetail = {
2142
+ files: files && files.every((file) => typeof file?.path === 'string') ? files.map((file) => file.path!) : null,
2143
+ comments: comments && comments.every((comment) => typeof comment?.body === 'string') ? comments.map((comment) => comment.body!) : null,
2144
+ reviews: reviews && reviews.every((review) => review && typeof review === 'object')
2145
+ ? reviews.map((review) => ({ state: review.state, body: review.body })) : null,
2146
+ headSha: typeof pr.headRefOid === 'string' && pr.headRefOid ? pr.headRefOid : null,
2147
+ mergeCommit: pr.mergeCommit && typeof pr.mergeCommit.oid === 'string' && typeof pr.mergeCommit.message === 'string'
2148
+ ? { oid: pr.mergeCommit.oid, message: pr.mergeCommit.message } : null,
2149
+ };
2150
+ if (detail.files === null) incompleteFields.files += 1;
2151
+ if (detail.comments === null) incompleteFields.comments += 1;
2152
+ if (detail.reviews === null) incompleteFields.reviews += 1;
2153
+ details.set(number, detail);
2154
+ }
2155
+ });
2156
+ if (failedBatches > 0 || details.size < numbers.length || Object.values(incompleteFields).some((count) => count > 0)) {
2157
+ try {
2158
+ debug.log('self-dev.draft-sweep', 'graphql-fallback', { requested: numbers.length, resolved: details.size, failedBatches, incompleteFields });
2159
+ } catch { /* fail-soft */ }
2160
+ }
2161
+ return details;
2162
+ }
2163
+
2164
+ /** GitHub draft-sweep adapters plus the shared inventory the RUN-TTL step reuses (one open-PR listing per sweep). */
2165
+ export type GithubDraftSweepAdapters = DraftSweepAdapters & {
2166
+ /** The sweep's cached open PR listing — RUN-TTL reads it instead of paging the same list again. */
2167
+ openPullRequests(repository: string): Promise<GithubPull[]>;
2168
+ /** The sweep's cached run-ledger scan. */
2169
+ runLedgerMatches(): readonly RunLedgerMatch[];
2170
+ };
2171
+
2172
+ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: DraftSweepGitExecute = runGitCommand, cwd = process.cwd(),
2173
+ executeAsync: (args: string[]) => Promise<string> = execute === executeGh ? executeGhAsync : async (args) => execute(args)): GithubDraftSweepAdapters {
1911
2174
  const inventory = async (repository: string, state: 'open' | 'closed'): Promise<GithubPull[]> => {
1912
2175
  // ⛔ A large repository has tens of thousands of closed PRs (2026-09-28: 21k → «exceeded pagination limit», no sweep at all).
1913
2176
  // Merged twins only matter after the oldest open draft was opened, so the closed listing stops at that time.
@@ -1936,13 +2199,14 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
1936
2199
  if (!cache.has(repository)) cache.set(repository, inventory(repository, state));
1937
2200
  return cache.get(repository)!;
1938
2201
  };
2202
+ const ghJsonAsync = async <T>(args: string[]): Promise<T> => JSON.parse(await executeAsync(args)) as T;
1939
2203
  // GitHub stops listing a pull request's files at 3,000, and the cut looks like a normal last page.
1940
2204
  // A list that reaches the cap cannot prove it is complete, so it is reported as unknown (undefined):
1941
2205
  // a draft without a provable file list never qualifies for the all-files-landed close.
1942
- const filesFor = (repository: string, number: number): string[] | undefined => {
2206
+ const filesFor = async (repository: string, number: number): Promise<string[] | undefined> => {
1943
2207
  const files: string[] = [];
1944
2208
  for (let page = 1; page <= 100; page++) {
1945
- const batch = ghJson<Array<{ filename: string }>>(['api', `repos/${repository}/pulls/${number}/files?per_page=100&page=${page}`], execute);
2209
+ const batch = await ghJsonAsync<Array<{ filename: string }>>(['api', `repos/${repository}/pulls/${number}/files?per_page=100&page=${page}`]);
1946
2210
  if (!Array.isArray(batch) || batch.length > 100 || batch.some((file) => typeof file.filename !== 'string')) throw new Error('Incomplete PR file inventory');
1947
2211
  files.push(...batch.map((file) => file.filename));
1948
2212
  if (files.length >= GITHUB_PR_FILES_LIST_CAP) {
@@ -1953,18 +2217,31 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
1953
2217
  }
1954
2218
  throw new Error('PR file inventory exceeded pagination limit');
1955
2219
  };
1956
- const latestFileChangesFor = (draft: SweepDraft, repository: string): Record<string, string> | undefined => {
2220
+ // Batched evidence per PR, read once per adapter (= once per sweep) and shared by listing and review/gate.
2221
+ const detailCache = new Map<string, Promise<GraphqlPrDetail | undefined>>();
2222
+ const prefetchDetails = async (repository: string, numbers: readonly number[]): Promise<void> => {
2223
+ const missing = [...new Set(numbers)].filter((number) => !detailCache.has(`${repository}#${number}`));
2224
+ if (missing.length === 0) return;
2225
+ const fetched = graphqlPrDetails(repository, missing, executeAsync);
2226
+ for (const number of missing) detailCache.set(`${repository}#${number}`, fetched.then((map) => map.get(number)));
2227
+ await fetched;
2228
+ };
2229
+ const detailFor = (repository: string, number: number): Promise<GraphqlPrDetail | undefined> =>
2230
+ detailCache.get(`${repository}#${number}`) ?? Promise.resolve(undefined);
2231
+ const prFiles = async (repository: string, number: number): Promise<string[] | undefined> =>
2232
+ (await detailFor(repository, number))?.files ?? filesFor(repository, number);
2233
+ const latestFileChangesFor = async (draft: SweepDraft, repository: string): Promise<Record<string, string> | undefined> => {
1957
2234
  if (!draft.changedFiles?.length) return undefined;
1958
2235
  const changes: Record<string, string> = {};
1959
2236
  for (let page = 1; page <= 100; page++) {
1960
- const batch = ghJson<Array<{ sha: string; commit: { committer: { date: string } } }>>(
1961
- ['api', `repos/${repository}/pulls/${draft.number}/commits?per_page=100&page=${page}`], execute);
2237
+ const batch = await ghJsonAsync<Array<{ sha: string; commit: { committer: { date: string } } }>>(
2238
+ ['api', `repos/${repository}/pulls/${draft.number}/commits?per_page=100&page=${page}`]);
1962
2239
  if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete draft commit inventory');
1963
2240
  for (const commit of batch) {
1964
2241
  const changedAt = commit.commit?.committer?.date;
1965
2242
  if (!commit.sha || !changedAt || !Number.isFinite(Date.parse(changedAt))) return undefined;
1966
- const detail = ghJson<{ files?: Array<{ filename: string; previous_filename?: string }> }>(
1967
- ['api', `repos/${repository}/commits/${commit.sha}?per_page=100`], execute);
2243
+ const detail = await ghJsonAsync<{ files?: Array<{ filename: string; previous_filename?: string }> }>(
2244
+ ['api', `repos/${repository}/commits/${commit.sha}?per_page=100`]);
1968
2245
  // GitHub truncates very large commit file lists; never infer coverage from a partial list.
1969
2246
  if (!Array.isArray(detail.files) || detail.files.length >= 100) return undefined;
1970
2247
  for (const file of detail.files) {
@@ -1978,24 +2255,30 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
1978
2255
  }
1979
2256
  return undefined;
1980
2257
  };
1981
- const reviewGateForDraft = (draft: SweepDraft, repository: string): SweepReviewGate | undefined => {
1982
- const head = ghJson<{ head?: { sha?: string } }>(['api', `repos/${repository}/pulls/${draft.number}`], execute);
1983
- const sha = head?.head?.sha;
1984
- if (!sha) return undefined;
2258
+ const issueCommentsFor = async (repository: string, number: number, what: 'review' | 'landing'): Promise<string[]> => {
1985
2259
  const comments: string[] = [];
1986
2260
  for (let page = 1; page <= 100; page++) {
1987
- const batch = ghJson<Array<{ body?: string }>>(['api', `repos/${repository}/issues/${draft.number}/comments?per_page=100&page=${page}`], execute);
1988
- if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete review comment inventory');
2261
+ const batch = await ghJsonAsync<Array<{ body?: string }>>(['api', `repos/${repository}/issues/${number}/comments?per_page=100&page=${page}`]);
2262
+ if (!Array.isArray(batch) || batch.length > 100) throw new Error(`Incomplete ${what} comment inventory`);
1989
2263
  comments.push(...batch.map((comment) => comment.body ?? ''));
1990
- if (batch.length < 100) break;
1991
- if (page === 100) throw new Error('Review comment inventory exceeded pagination limit');
2264
+ if (batch.length < 100) return comments;
1992
2265
  }
1993
- const reviews = ghJson<Array<{ state?: string; body?: string }>>(['api', `repos/${repository}/pulls/${draft.number}/reviews?per_page=100`], execute);
2266
+ throw new Error(`${what === 'review' ? 'Review' : 'Landing'} comment inventory exceeded pagination limit`);
2267
+ };
2268
+ const reviewGateForDraft = async (draft: SweepDraft, repository: string): Promise<SweepReviewGate | undefined> => {
2269
+ const cached = await detailFor(repository, draft.number);
2270
+ const sha = cached?.headSha
2271
+ ?? (await ghJsonAsync<{ head?: { sha?: string } }>(['api', `repos/${repository}/pulls/${draft.number}`]))?.head?.sha;
2272
+ if (!sha) return undefined;
2273
+ const comments = cached?.comments ?? await issueCommentsFor(repository, draft.number, 'review');
2274
+ const reviews = cached?.reviews
2275
+ ?? await ghJsonAsync<Array<{ state?: string; body?: string }>>(['api', `repos/${repository}/pulls/${draft.number}/reviews?per_page=100`]);
1994
2276
  if (!Array.isArray(reviews) || reviews.length > 100) throw new Error('Incomplete review inventory');
1995
2277
  const text = [...comments, ...reviews.map((review) => review.body ?? '')].join('\n');
1996
- const status = ghJson<{ state?: string }>(['api', `repos/${repository}/commits/${sha}/status`], execute);
1997
- const checkRuns = ghJson<{ check_runs?: Array<{ conclusion?: string | null }> }>(
1998
- ['api', `repos/${repository}/commits/${sha}/check-runs?per_page=100`], execute);
2278
+ const [status, checkRuns] = await Promise.all([
2279
+ ghJsonAsync<{ state?: string }>(['api', `repos/${repository}/commits/${sha}/status`]),
2280
+ ghJsonAsync<{ check_runs?: Array<{ conclusion?: string | null }> }>(['api', `repos/${repository}/commits/${sha}/check-runs?per_page=100`]),
2281
+ ]);
1999
2282
  if (!checkRuns || !Array.isArray(checkRuns.check_runs)) throw new Error('Incomplete check-run inventory');
2000
2283
  const conclusions = checkRuns.check_runs.map((run) => run.conclusion ?? null);
2001
2284
  const gatePass = (status?.state === 'success' || status?.state === undefined)
@@ -2008,15 +2291,42 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
2008
2291
  ...(gatePass && !gateFail ? { gate: 'pass' as const } : gateFail ? { gate: 'fail' as const } : {}),
2009
2292
  };
2010
2293
  };
2011
- const landingCommentsFor = (repository: string, number: number): string[] => {
2012
- const comments: string[] = [];
2013
- for (let page = 1; page <= 100; page++) {
2014
- const batch = ghJson<Array<{ body?: string }>>(['api', `repos/${repository}/issues/${number}/comments?per_page=100&page=${page}`], execute);
2015
- if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete landing comment inventory');
2016
- comments.push(...batch.filter((comment) => /\blanding-verified\b/i.test(comment.body ?? '')).map((comment) => comment.body!));
2017
- if (batch.length < 100) return comments;
2018
- }
2019
- throw new Error('Landing comment inventory exceeded pagination limit');
2294
+ const landingCommentsFor = async (repository: string, number: number): Promise<string[]> =>
2295
+ ((await detailFor(repository, number))?.comments ?? await issueCommentsFor(repository, number, 'landing'))
2296
+ .filter((body) => /\blanding-verified\b/i.test(body));
2297
+ const mergeCommitMessageFor = async (repository: string, pr: GithubPull): Promise<string | undefined> => {
2298
+ if (!pr.merge_commit_sha) return undefined;
2299
+ const cached = (await detailFor(repository, pr.number))?.mergeCommit;
2300
+ if (cached && cached.oid === pr.merge_commit_sha) return cached.message;
2301
+ return (await ghJsonAsync<{ commit: { message: string } }>(['api', `repos/${repository}/commits/${pr.merge_commit_sha}`])).commit.message;
2302
+ };
2303
+ // The whole draft / merged inventory is enriched once (bounded concurrency) and then paged from memory.
2304
+ const draftRows = new Map<string, Promise<SweepDraft[]>>();
2305
+ const mergedRows = new Map<string, Promise<SweepMergedPr[]>>();
2306
+ const enrichedDrafts = (repository: string): Promise<SweepDraft[]> => {
2307
+ if (!draftRows.has(repository)) draftRows.set(repository, (async () => {
2308
+ const prs = (await listed(open, repository, 'open')).filter((pr) => pr.draft);
2309
+ await prefetchDetails(repository, prs.map((pr) => pr.number));
2310
+ const matches = ledgerMatches ??= listRunLedgers().matches;
2311
+ return mapBounded(prs, DRAFT_SWEEP_CONCURRENCY, async (pr): Promise<SweepDraft> => ({
2312
+ number: pr.number, title: pr.title, branch: pr.head.ref,
2313
+ labels: pr.labels.map((item) => item.name), createdAt: pr.created_at, body: pr.body ?? undefined,
2314
+ changedFiles: await prFiles(repository, pr.number), ...(pr.updated_at ? { updatedAt: pr.updated_at } : {}),
2315
+ runId: currentDraftSweepRunId(matches, repository, pr.number) }));
2316
+ })());
2317
+ return draftRows.get(repository)!;
2318
+ };
2319
+ const enrichedMerged = (repository: string): Promise<SweepMergedPr[]> => {
2320
+ if (!mergedRows.has(repository)) mergedRows.set(repository, (async () => {
2321
+ const prs = (await listed(closed, repository, 'closed')).filter((pr) => pr.merged_at !== null && pr.base?.ref === 'main');
2322
+ await prefetchDetails(repository, prs.map((pr) => pr.number));
2323
+ return mapBounded(prs, DRAFT_SWEEP_CONCURRENCY, async (pr): Promise<SweepMergedPr> => ({
2324
+ number: pr.number, title: pr.title, branch: pr.head.ref, body: pr.body ?? undefined,
2325
+ mergedAt: pr.merged_at ?? undefined, changedFiles: await prFiles(repository, pr.number),
2326
+ mergeCommitMessage: await mergeCommitMessageFor(repository, pr),
2327
+ landingVerifiedComments: await landingCommentsFor(repository, pr.number) }));
2328
+ })());
2329
+ return mergedRows.get(repository)!;
2020
2330
  };
2021
2331
  let ledgerMatches: readonly RunLedgerMatch[] | undefined;
2022
2332
  let running: ReadonlyMap<string, string> | undefined;
@@ -2030,22 +2340,35 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
2030
2340
  return draftSweepRunStatus(ledgerMatches, running, repository, draft.number);
2031
2341
  };
2032
2342
  return {
2033
- getPrFiles: async (repository, number) => filesFor(repository, number),
2343
+ openPullRequests: (repository) => listed(open, repository, 'open'),
2344
+ runLedgerMatches: () => (ledgerMatches ??= listRunLedgers().matches),
2345
+ getPrFiles: async (repository, number) => prFiles(repository, number),
2346
+ getPr: async (repository, number) => {
2347
+ const pr = ghJson<GithubPull>(['api', `repos/${repository}/pulls/${number}`], execute);
2348
+ if (!pr || pr.number !== number || typeof pr.head?.ref !== 'string' || !Array.isArray(pr.labels)) return undefined;
2349
+ return { number: pr.number, title: pr.title, branch: pr.head.ref, labels: pr.labels.map((label) => label.name),
2350
+ createdAt: pr.created_at, mergedAt: pr.merged_at ?? undefined };
2351
+ },
2352
+ listRecentClosed: async (repository, createdSince) => {
2353
+ const rows: SweepMergedPr[] = [];
2354
+ for (let page = 1; page <= 100; page++) {
2355
+ const batch = githubPullsPage(repository, 'closed', page, execute);
2356
+ if (!Array.isArray(batch) || batch.length > 100 || batch.some((pr) => !pr.updated_at || !Number.isFinite(Date.parse(pr.updated_at))
2357
+ || !Array.isArray(pr.labels) || !pr.created_at || !Number.isFinite(Date.parse(pr.created_at)))) throw new Error('Incomplete closed metric inventory');
2358
+ for (const pr of batch) if (pr.base?.ref === 'main' && (pr.head?.ref?.startsWith('self-impl/')
2359
+ || pr.labels.some((label) => label.name?.startsWith('elanous:')))) rows.push({
2360
+ number: pr.number, title: pr.title, branch: pr.head.ref,
2361
+ labels: pr.labels.map((label) => label.name),
2362
+ createdAt: pr.created_at, mergedAt: pr.merged_at ?? undefined,
2363
+ });
2364
+ if (batch.length < 100 || Date.parse(batch.at(-1)!.updated_at!) < createdSince.getTime()) return rows;
2365
+ }
2366
+ throw new Error('Closed metric inventory exceeded pagination limit');
2367
+ },
2034
2368
  listDrafts: async (page, perPage, repository): Promise<SweepDraft[]> =>
2035
- (await listed(open, repository, 'open')).filter((pr) => pr.draft)
2036
- .slice((page - 1) * perPage, page * perPage)
2037
- .map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref,
2038
- labels: pr.labels.map((item) => item.name), createdAt: pr.created_at, body: pr.body ?? undefined,
2039
- changedFiles: filesFor(repository, pr.number), ...(pr.updated_at ? { updatedAt: pr.updated_at } : {}),
2040
- runId: currentDraftSweepRunId(ledgerMatches ??= listRunLedgers().matches, repository, pr.number) })),
2369
+ (await enrichedDrafts(repository)).slice((page - 1) * perPage, page * perPage),
2041
2370
  listMerged: async (page, perPage, repository): Promise<SweepMergedPr[]> =>
2042
- (await listed(closed, repository, 'closed')).filter((pr) => pr.merged_at !== null && pr.base?.ref === 'main')
2043
- .slice((page - 1) * perPage, page * perPage)
2044
- .map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref, body: pr.body ?? undefined,
2045
- mergedAt: pr.merged_at ?? undefined, changedFiles: filesFor(repository, pr.number),
2046
- mergeCommitMessage: pr.merge_commit_sha ? ghJson<{ commit: { message: string } }>(
2047
- ['api', `repos/${repository}/commits/${pr.merge_commit_sha}`], execute).commit.message : undefined,
2048
- landingVerifiedComments: landingCommentsFor(repository, pr.number) })),
2371
+ (await enrichedMerged(repository)).slice((page - 1) * perPage, page * perPage),
2049
2372
  getRunStatus: async (draft, repository) => runStatusFor(draft, repository),
2050
2373
  getLatestFileChanges: async (draft, repository) => latestFileChangesFor(draft, repository),
2051
2374
  hasFinalRunResult: async (draft, repository) => {
@@ -2096,6 +2419,17 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
2096
2419
  execute(['pr', 'close', String(number), '--repo', repository, '--comment', comment]);
2097
2420
  },
2098
2421
  getClaimOwner: async (repository, number) => claimCommentOwner(repository, number, execute),
2422
+ getActiveClaimOwner: async (repository, number) => {
2423
+ const pages = ghJson<Array<Array<{ body: string; created_at: string }>>>(['api', '--paginate', '--slurp',
2424
+ `repos/${repository}/issues/${number}/comments?per_page=100`], execute);
2425
+ if (!Array.isArray(pages) || pages.some((page) => !Array.isArray(page)
2426
+ || page.some((comment) => typeof comment.body !== 'string' || !Number.isFinite(Date.parse(comment.created_at)))))
2427
+ throw new Error('Incomplete claim comments');
2428
+ const last = pages.flat().sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at))
2429
+ .map((comment) => comment.body)
2430
+ .find((body) => draftClaimOwner(body) !== undefined || body.startsWith('🔧 처리 끝 —'));
2431
+ return last ? draftClaimOwner(last) : undefined;
2432
+ },
2099
2433
  getReviewGate: async (draft, repository) => reviewGateForDraft(draft, repository),
2100
2434
  };
2101
2435
  }
@@ -2106,6 +2440,28 @@ export interface HarnessDraftSweepDeps {
2106
2440
  readonly write?: (line: string) => void;
2107
2441
  readonly execute?: GhExecute;
2108
2442
  readonly now?: () => Date;
2443
+ /** RUN-TTL step adapters. Omitted with injected `adapters` ⇒ the step is skipped (fakes never reach real gh). */
2444
+ readonly runTtl?: RunTtlAdapters;
2445
+ }
2446
+
2447
+ /** RUN-TTL step inside the draft sweep — fail-soft: its failure never changes the sweep verdict. */
2448
+ async function draftSweepRunTtl(repository: string, deps: HarnessDraftSweepDeps,
2449
+ shared?: GithubDraftSweepAdapters): Promise<RunTtlResult | { error: string } | undefined> {
2450
+ if (deps.adapters && !deps.runTtl) return undefined;
2451
+ try {
2452
+ const { runRunTtlSweep } = await import('../self-implement/run-ttl.js');
2453
+ const config = (() => { try { return getUserConfig().tools?.selfImplement; } catch { return undefined; } })();
2454
+ const adapters = deps.runTtl ?? await githubRunTtlAdapters(repository, deps.execute ?? executeGh,
2455
+ deps.execute ? async (args) => deps.execute!(args) : executeGhAsync, undefined, shared ?? {});
2456
+ return await runRunTtlSweep({
2457
+ adapters, mode: config?.runTtl?.mode ?? 'shadow', ttlHours: config?.runTtlHours, cap: config?.runTtl?.cap,
2458
+ ...(deps.now ? { now: () => deps.now!().getTime() } : {}),
2459
+ });
2460
+ } catch (error) {
2461
+ const reason = sweepFailureReason(error);
2462
+ try { debug.log('self-implement.run-ttl', 'failed', { reason }); } catch { /* fail-soft */ }
2463
+ return { error: reason };
2464
+ }
2109
2465
  }
2110
2466
 
2111
2467
  function draftClaimOwner(body: string): string | undefined {
@@ -2179,10 +2535,19 @@ function installHarnessDraftSweepCommand(harnessCmd: Command, deps: HarnessDraft
2179
2535
  if (!DRAFT_SWEEP_REPOSITORY.test(repository) || repository.includes('..')) {
2180
2536
  throw new HarnessCliInputError(`invalid --repo (expected owner/name): ${repository}`);
2181
2537
  }
2182
- const result: DraftSweepResult = await runDraftSweep({ repository, apply: opts.apply === true, adapters: deps.adapters ?? githubDraftSweepAdapters(deps.execute) });
2538
+ // One adapter instance per sweep: its open listing and ledger scan are shared with the RUN-TTL step.
2539
+ const github = deps.adapters ? undefined : githubDraftSweepAdapters(deps.execute);
2540
+ const result: DraftSweepResult = await runDraftSweep({ repository, apply: opts.apply === true, adapters: deps.adapters ?? github! });
2183
2541
  const reason = result.error ? sweepFailureReason(result.error) : undefined;
2184
- if (opts.json) write(JSON.stringify(reason ? { ...result, error: reason } : result));
2542
+ const ttlStarted = Date.now();
2543
+ const runTtl = await draftSweepRunTtl(repository, deps, github);
2544
+ try { debug.log('self-dev.draft-sweep', 'timing', { phase: 'run-ttl', ms: Date.now() - ttlStarted, count: runTtl && !('error' in runTtl) ? runTtl.considered : 0 }); } catch { /* fail-soft */ }
2545
+ if (opts.json) write(JSON.stringify({ ...(reason ? { ...result, error: reason } : result), ...(runTtl ? { runTtl } : {}) }));
2185
2546
  else {
2547
+ if (runTtl) {
2548
+ write('error' in runTtl ? `run-ttl: 실패 (${runTtl.error})`
2549
+ : `run-ttl(${runTtl.mode}, ${runTtl.ttlHours}h): 대상 ${runTtl.considered} · 낡음 ${runTtl.stale.length} · 메모 ${runTtl.memo.length} · 표식 ${runTtl.label.length} · 이미 표식 ${runTtl.alreadyMarked.length} · 상한 초과 ${runTtl.skippedOverCap.length}${runTtl.mode === 'shadow' ? ' (shadow — 쓰기 0)' : ''}`);
2550
+ }
2186
2551
  write(`draft sweep ${result.repository}: ${result.complete ? 'complete' : `incomplete (${reason})`} · ${result.apply ? 'apply' : 'dry-run'}`);
2187
2552
  for (const entry of result.entries) write(`#${entry.number} ${entry.action}: ${entry.reason}${entry.statusLabel ? ` → ${entry.statusLabel}` : ''}${entry.error ? ` ERROR: ${sweepFailureReason(entry.error)}` : ''}`);
2188
2553
  if (result.complete) {
@@ -2511,6 +2876,8 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2511
2876
  installDeliverableVerifyCliCommand(harnessCmd, deps.deliverableVerify);
2512
2877
  installHarnessProcessObservationCommand(harnessCmd, deps.processObservation);
2513
2878
  installHarnessBudgetCommand(harnessCmd);
2879
+ installHarnessSalvageCommand(harnessCmd);
2880
+ installHarnessSalvageRetentionCommand(harnessCmd);
2514
2881
  installHarnessGoalCommand(harnessCmd, deps.goalLookup, deps.goalArchive);
2515
2882
  installHarnessDraftSweepCommand(harnessCmd, deps.draftSweep);
2516
2883
  const queue = harnessCmd.command('queue').description('자리별 영속 발사 대기열');
@@ -2565,9 +2932,11 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2565
2932
  queue.command('reconcile <id>').description('불확정 발사를 확인하고 종료 또는 미발사 증거가 있으면 예약 해소').action((id: string) => queueAction(async () => {
2566
2933
  console.log(`${id} ${await reconcileHarnessQueue(id, queueDeps)}`);
2567
2934
  }));
2568
- queue.command('tick').description('자리별 맨 앞 한 건을 라운드로빈으로 돌며 자리 몫·Pod 풀 여유·마무리 관문을 확인하고 한 건 발사').action(() => queueAction(async () => {
2935
+ queue.command('tick').description('자리별 맨 앞 항목을 라운드로빈으로 돌며 자리 몫·Pod 풀 여유·마무리 관문을 확인하고 발사 — 여유가 있으면 한 tick 에 여러 건(30초 간격 · harness.queue.burstMax 기본 5)').action(() => queueAction(async () => {
2569
2936
  const result = await tickHarnessQueue(queueDeps);
2570
2937
  console.log(`${result.outcome}${result.item ? ` ${result.item.id}` : ''}: ${result.reason}`);
2938
+ // QUEUE-BURST: the first line stays as before; every further launch of the burst gets its own line.
2939
+ for (const row of result.launched?.slice(1) ?? []) console.log(`launched ${row.id}: spawned`);
2571
2940
  }));
2572
2941
 
2573
2942
  const ask = deps.ask;