elanous 0.2.19 → 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 (388) 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/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
  11. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
  35. package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
  36. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
  41. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/reflection/page-db03ae3589a81c9e.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/settings/page-31d5b875760bb62d.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/trace/page-0fd895fab9b62c1b.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workspace/page-34882c84f84021c5.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
  103. package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  104. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
  110. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  111. package/apps/pwa/out/approvals/index.html +1 -1
  112. package/apps/pwa/out/approvals/index.txt +23 -23
  113. package/apps/pwa/out/autopilot/index.html +1 -1
  114. package/apps/pwa/out/autopilot/index.txt +23 -23
  115. package/apps/pwa/out/board/index.html +1 -1
  116. package/apps/pwa/out/board/index.txt +23 -23
  117. package/apps/pwa/out/botlab/index.html +1 -1
  118. package/apps/pwa/out/botlab/index.txt +22 -22
  119. package/apps/pwa/out/bots/index.html +1 -1
  120. package/apps/pwa/out/bots/index.txt +22 -22
  121. package/apps/pwa/out/ceo/index.html +1 -1
  122. package/apps/pwa/out/ceo/index.txt +23 -23
  123. package/apps/pwa/out/chat/index.html +1 -1
  124. package/apps/pwa/out/chat/index.txt +22 -22
  125. package/apps/pwa/out/consult/index.html +1 -1
  126. package/apps/pwa/out/consult/index.txt +23 -23
  127. package/apps/pwa/out/control/index.html +1 -1
  128. package/apps/pwa/out/control/index.txt +23 -23
  129. package/apps/pwa/out/decisions/index.html +1 -1
  130. package/apps/pwa/out/decisions/index.txt +22 -22
  131. package/apps/pwa/out/design-check/index.html +1 -1
  132. package/apps/pwa/out/design-check/index.txt +23 -23
  133. package/apps/pwa/out/editor/index.html +1 -1
  134. package/apps/pwa/out/editor/index.txt +23 -23
  135. package/apps/pwa/out/exec/index.html +1 -1
  136. package/apps/pwa/out/exec/index.txt +23 -23
  137. package/apps/pwa/out/field/index.html +1 -1
  138. package/apps/pwa/out/field/index.txt +23 -23
  139. package/apps/pwa/out/healthz +1 -1
  140. package/apps/pwa/out/index.html +1 -1
  141. package/apps/pwa/out/index.txt +22 -22
  142. package/apps/pwa/out/inside/index.html +1 -1
  143. package/apps/pwa/out/inside/index.txt +22 -22
  144. package/apps/pwa/out/intake/index.html +1 -1
  145. package/apps/pwa/out/intake/index.txt +23 -23
  146. package/apps/pwa/out/live/index.html +1 -1
  147. package/apps/pwa/out/live/index.txt +23 -23
  148. package/apps/pwa/out/loops/index.html +1 -1
  149. package/apps/pwa/out/loops/index.txt +22 -22
  150. package/apps/pwa/out/market/index.html +1 -1
  151. package/apps/pwa/out/market/index.txt +22 -22
  152. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  153. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  154. package/apps/pwa/out/morning/index.html +1 -1
  155. package/apps/pwa/out/morning/index.txt +23 -23
  156. package/apps/pwa/out/observatory/index.html +1 -1
  157. package/apps/pwa/out/observatory/index.txt +23 -23
  158. package/apps/pwa/out/ops/checklist/index.html +1 -1
  159. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  160. package/apps/pwa/out/ops/release/index.html +1 -1
  161. package/apps/pwa/out/ops/release/index.txt +22 -22
  162. package/apps/pwa/out/ops/seats/index.html +1 -1
  163. package/apps/pwa/out/ops/seats/index.txt +22 -22
  164. package/apps/pwa/out/outputs/index.html +1 -1
  165. package/apps/pwa/out/outputs/index.txt +22 -22
  166. package/apps/pwa/out/reflection/index.html +1 -1
  167. package/apps/pwa/out/reflection/index.txt +23 -23
  168. package/apps/pwa/out/scheduler/index.html +1 -1
  169. package/apps/pwa/out/scheduler/index.txt +23 -23
  170. package/apps/pwa/out/sessions/index.html +1 -1
  171. package/apps/pwa/out/sessions/index.txt +22 -22
  172. package/apps/pwa/out/settings/devices/index.html +1 -1
  173. package/apps/pwa/out/settings/devices/index.txt +23 -23
  174. package/apps/pwa/out/settings/index.html +1 -1
  175. package/apps/pwa/out/settings/index.txt +23 -23
  176. package/apps/pwa/out/setup/done/index.html +1 -1
  177. package/apps/pwa/out/setup/done/index.txt +23 -23
  178. package/apps/pwa/out/setup/index.html +1 -1
  179. package/apps/pwa/out/setup/index.txt +23 -23
  180. package/apps/pwa/out/share/index.html +1 -1
  181. package/apps/pwa/out/share/index.txt +23 -23
  182. package/apps/pwa/out/showroom/index.html +1 -1
  183. package/apps/pwa/out/showroom/index.txt +22 -22
  184. package/apps/pwa/out/tasks/index.html +1 -1
  185. package/apps/pwa/out/tasks/index.txt +23 -23
  186. package/apps/pwa/out/term/index.html +1 -1
  187. package/apps/pwa/out/term/index.txt +23 -23
  188. package/apps/pwa/out/today/index.html +1 -1
  189. package/apps/pwa/out/today/index.txt +23 -23
  190. package/apps/pwa/out/trace/index.html +1 -1
  191. package/apps/pwa/out/trace/index.txt +23 -23
  192. package/apps/pwa/out/vault/index.html +1 -1
  193. package/apps/pwa/out/vault/index.txt +23 -23
  194. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  195. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  196. package/apps/pwa/out/workflows/index.html +1 -1
  197. package/apps/pwa/out/workflows/index.txt +23 -23
  198. package/apps/pwa/out/workspace/index.html +1 -1
  199. package/apps/pwa/out/workspace/index.txt +23 -23
  200. package/apps/pwa/out/worktrees/index.html +1 -1
  201. package/apps/pwa/out/worktrees/index.txt +23 -23
  202. package/graphs/demo/editor-recipes.yaml +22 -0
  203. package/graphs/retro/daily.yaml +25 -0
  204. package/graphs/retro/recipes.yaml +24 -0
  205. package/package.json +1 -1
  206. package/scripts/ask-marker-check.ts +3 -1
  207. package/scripts/land-global-checks.ts +89 -0
  208. package/scripts/lib/pch-regress-map.ts +2 -1
  209. package/scripts/release-loop/gate-node.ts +296 -25
  210. package/scripts/release-loop/gate-timing.ts +116 -7
  211. package/scripts/release-loop/preflight.ts +6 -4
  212. package/scripts/release-loop/publish-window.ts +141 -0
  213. package/scripts/release-loop/release-docs.ts +1 -4
  214. package/scripts/release-loop/release-universe.ts +38 -0
  215. package/scripts/release-loop/resume-release.ts +4 -1
  216. package/scripts/release-loop/unattended-release.ts +10 -2
  217. package/scripts/release-story/draft.ts +5 -1
  218. package/scripts/retro/shadow-compare.ts +71 -0
  219. package/scripts/rhythm/daily.ts +6 -1
  220. package/src/agent-mission/driver.ts +34 -6
  221. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  222. package/src/autopilot/build/merge-intent.ts +135 -7
  223. package/src/away/release-watch.ts +20 -3
  224. package/src/cli/config-drift.ts +513 -0
  225. package/src/cli/log-event-names.ts +4 -0
  226. package/src/cli/pr-cli.ts +20 -0
  227. package/src/cli/release-cli.ts +20 -5
  228. package/src/cli/retro-cli.ts +78 -0
  229. package/src/cli/tasks-cli.ts +94 -8
  230. package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
  231. package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
  232. package/src/domains/outbound-log-probe.ts +6 -1
  233. package/src/graph-runner/graph-run-api.ts +205 -0
  234. package/src/graph-runner/runner.ts +11 -1
  235. package/src/harness/harness-cli-command.ts +419 -52
  236. package/src/harness/harness-queue.ts +315 -25
  237. package/src/harness/harness-salvage-cli.ts +62 -0
  238. package/src/hq/fence-outcomes.ts +63 -0
  239. package/src/hq/fence-wrapper.ts +33 -0
  240. package/src/hq/hq.ts +11 -1
  241. package/src/index.ts +69 -6
  242. package/src/maturity/feature-maturity.ts +2 -2
  243. package/src/nexus/api/graph-kinds.ts +10 -4
  244. package/src/nexus/api/graphs-api.ts +178 -6
  245. package/src/nexus/api/http-server.ts +38 -3
  246. package/src/nexus/api/loop-edges.ts +4 -2
  247. package/src/nexus/api/ops-api.ts +22 -7
  248. package/src/nexus/api/ops-board-route.ts +48 -0
  249. package/src/onboarding/entry-hints.ts +5 -0
  250. package/src/onboarding.ts +24 -6
  251. package/src/release-loop/feature-store.ts +14 -11
  252. package/src/release-loop/gate-shards.ts +158 -0
  253. package/src/release-loop/release-schedule.ts +6 -2
  254. package/src/retro/daily.ts +372 -0
  255. package/src/seat-loop/op-decide.ts +173 -0
  256. package/src/seat-loop/seat-loop.ts +60 -3
  257. package/src/seat-loop/seat-report.ts +58 -3
  258. package/src/self-dev/ask-launch-flow.ts +5 -4
  259. package/src/self-dev/ask-launch-io.ts +49 -1
  260. package/src/self-dev/ask-path-candidates.ts +40 -2
  261. package/src/self-dev/draft-sweep.ts +371 -16
  262. package/src/self-dev/draft-triage-rules.ts +19 -4
  263. package/src/self-dev/pr-preview.ts +84 -16
  264. package/src/self-implement/blocked-draft-policy.ts +101 -0
  265. package/src/self-implement/execution-clarification.ts +11 -0
  266. package/src/self-implement/gate-env-retry.ts +83 -0
  267. package/src/self-implement/orchestrator.ts +218 -5
  268. package/src/self-implement/run-stop.ts +123 -0
  269. package/src/self-implement/run-ttl.ts +258 -0
  270. package/src/self-implement/salvage-retention.ts +123 -0
  271. package/src/self-implement/seams.ts +4 -0
  272. package/src/self-implement/self-implement-cli.ts +2 -0
  273. package/src/self-implement/sibling-resync.ts +223 -0
  274. package/src/self-implement/stop-autoheal.ts +204 -0
  275. package/src/task-agent/board.ts +1 -1
  276. package/src/task-agent/card-evidence.ts +99 -10
  277. package/src/task-agent/judge.ts +7 -0
  278. package/src/task-agent/live-moves.ts +120 -20
  279. package/src/task-agent/mission.ts +1 -1
  280. package/src/task-agent/shadow.ts +10 -1
  281. package/src/task-agent/task-hand.ts +7 -2
  282. package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
  283. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  284. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  285. package/src/task-orchestrator/surfaces/pod-lease.ts +93 -10
  286. package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
  287. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
  288. package/src/telegram-commands.ts +41 -0
  289. package/src/testing/real-cli-spawn.ts +106 -0
  290. package/src/user-config.ts +56 -3
  291. package/src/version/packed-revision.json +1 -1
  292. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
  301. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  304. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  307. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
  308. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  309. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
  387. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
  388. /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → 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,14 +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';
26
- import { installHarnessSalvageCommand } from './harness-salvage-cli.js';
28
+ import { installHarnessSalvageCommand, installHarnessSalvageRetentionCommand } from './harness-salvage-cli.js';
27
29
  import { decideNestedElanousLaunch, readNestedElanousDepth } from './nested-elanous-policy.js';
28
30
  import { dispatchTask, type DispatchTaskInput, type DispatchTaskDeps } from '../execution-loop/dispatch-task.js';
29
31
  import { launchRequestId, preLaunchGate, type PreLaunchGateDeps } from '../execution-loop/launch-gate.js';
@@ -32,6 +34,9 @@ import { CODEX_PROVIDER, GROK_PROVIDER, decideBudget, decideLaunchBudget, readBu
32
34
  import { DEFAULT_FALLBACK_CHAIN } from '../oauth/fallback-chain.js';
33
35
  import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDeps } from './deliverable-verify-cli.js';
34
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';
35
40
  import { resolveRepositoryName } from './repository-name.js';
36
41
  import { installHarnessCliSinkHook } from './harness-cli-sink.js';
37
42
  import { addHarnessQueue, HarnessQueueDuplicateError, harnessQueueReceiptPath, listHarnessQueue, queueSeatForCwd, reconcileHarnessQueue, removeHarnessQueue, setHarnessQueuePriority, tickHarnessQueue, type HarnessQueueDeps, type QueueItem, type QueueSeat } from './harness-queue.js';
@@ -670,11 +675,40 @@ async function onPod(opts: unknown, entrance: 'cli-harness-ask' | 'cli-harness-s
670
675
  const classified = classifyHarnessPodExit({ status }, output, { ...(runId ? { runId } : {}) });
671
676
  recordClassifiedHarnessPodExit(runId ?? harnessPodRunId(output), classified.reason, status,
672
677
  { entrance, seat: o.seat, output });
678
+ recordHarnessPodStop(runId ?? harnessPodRunId(output), status, classified);
673
679
  for (const line of classified.lines) console.error(line);
674
680
  process.exitCode = status;
675
681
  }
676
682
  }
677
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
+
678
712
  const LAUNCH_BUDGET_PROVIDER: Readonly<Record<string, string>> = { 'codex-rotate': CODEX_PROVIDER, grok: GROK_PROVIDER };
679
713
 
680
714
  /**
@@ -1801,7 +1835,7 @@ type GithubPull = {
1801
1835
  title: string;
1802
1836
  draft: boolean;
1803
1837
  state: 'open' | 'closed';
1804
- head: { ref: string };
1838
+ head: { ref: string; sha?: string };
1805
1839
  base?: { ref: string };
1806
1840
  labels: Array<{ name: string }>;
1807
1841
  created_at: string;
@@ -1844,6 +1878,147 @@ export async function supersedeMergedGoalDrafts(
1844
1878
  await supersedeDraftsOnMerge({ repository, merged, adapters: githubDraftSweepAdapters(execute, git, cwd) });
1845
1879
  }
1846
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
+
1847
2022
  function ghJson<T>(args: string[], execute: GhExecute): T {
1848
2023
  return JSON.parse(execute(args)) as T;
1849
2024
  }
@@ -1908,7 +2083,94 @@ function githubRemoteRepository(url: string): string | undefined {
1908
2083
  return match?.[1];
1909
2084
  }
1910
2085
 
1911
- 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 {
1912
2174
  const inventory = async (repository: string, state: 'open' | 'closed'): Promise<GithubPull[]> => {
1913
2175
  // ⛔ A large repository has tens of thousands of closed PRs (2026-09-28: 21k → «exceeded pagination limit», no sweep at all).
1914
2176
  // Merged twins only matter after the oldest open draft was opened, so the closed listing stops at that time.
@@ -1937,13 +2199,14 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
1937
2199
  if (!cache.has(repository)) cache.set(repository, inventory(repository, state));
1938
2200
  return cache.get(repository)!;
1939
2201
  };
2202
+ const ghJsonAsync = async <T>(args: string[]): Promise<T> => JSON.parse(await executeAsync(args)) as T;
1940
2203
  // GitHub stops listing a pull request's files at 3,000, and the cut looks like a normal last page.
1941
2204
  // A list that reaches the cap cannot prove it is complete, so it is reported as unknown (undefined):
1942
2205
  // a draft without a provable file list never qualifies for the all-files-landed close.
1943
- const filesFor = (repository: string, number: number): string[] | undefined => {
2206
+ const filesFor = async (repository: string, number: number): Promise<string[] | undefined> => {
1944
2207
  const files: string[] = [];
1945
2208
  for (let page = 1; page <= 100; page++) {
1946
- 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}`]);
1947
2210
  if (!Array.isArray(batch) || batch.length > 100 || batch.some((file) => typeof file.filename !== 'string')) throw new Error('Incomplete PR file inventory');
1948
2211
  files.push(...batch.map((file) => file.filename));
1949
2212
  if (files.length >= GITHUB_PR_FILES_LIST_CAP) {
@@ -1954,18 +2217,31 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
1954
2217
  }
1955
2218
  throw new Error('PR file inventory exceeded pagination limit');
1956
2219
  };
1957
- 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> => {
1958
2234
  if (!draft.changedFiles?.length) return undefined;
1959
2235
  const changes: Record<string, string> = {};
1960
2236
  for (let page = 1; page <= 100; page++) {
1961
- const batch = ghJson<Array<{ sha: string; commit: { committer: { date: string } } }>>(
1962
- ['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}`]);
1963
2239
  if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete draft commit inventory');
1964
2240
  for (const commit of batch) {
1965
2241
  const changedAt = commit.commit?.committer?.date;
1966
2242
  if (!commit.sha || !changedAt || !Number.isFinite(Date.parse(changedAt))) return undefined;
1967
- const detail = ghJson<{ files?: Array<{ filename: string; previous_filename?: string }> }>(
1968
- ['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`]);
1969
2245
  // GitHub truncates very large commit file lists; never infer coverage from a partial list.
1970
2246
  if (!Array.isArray(detail.files) || detail.files.length >= 100) return undefined;
1971
2247
  for (const file of detail.files) {
@@ -1979,24 +2255,30 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
1979
2255
  }
1980
2256
  return undefined;
1981
2257
  };
1982
- const reviewGateForDraft = (draft: SweepDraft, repository: string): SweepReviewGate | undefined => {
1983
- const head = ghJson<{ head?: { sha?: string } }>(['api', `repos/${repository}/pulls/${draft.number}`], execute);
1984
- const sha = head?.head?.sha;
1985
- if (!sha) return undefined;
2258
+ const issueCommentsFor = async (repository: string, number: number, what: 'review' | 'landing'): Promise<string[]> => {
1986
2259
  const comments: string[] = [];
1987
2260
  for (let page = 1; page <= 100; page++) {
1988
- const batch = ghJson<Array<{ body?: string }>>(['api', `repos/${repository}/issues/${draft.number}/comments?per_page=100&page=${page}`], execute);
1989
- 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`);
1990
2263
  comments.push(...batch.map((comment) => comment.body ?? ''));
1991
- if (batch.length < 100) break;
1992
- if (page === 100) throw new Error('Review comment inventory exceeded pagination limit');
2264
+ if (batch.length < 100) return comments;
1993
2265
  }
1994
- 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`]);
1995
2276
  if (!Array.isArray(reviews) || reviews.length > 100) throw new Error('Incomplete review inventory');
1996
2277
  const text = [...comments, ...reviews.map((review) => review.body ?? '')].join('\n');
1997
- const status = ghJson<{ state?: string }>(['api', `repos/${repository}/commits/${sha}/status`], execute);
1998
- const checkRuns = ghJson<{ check_runs?: Array<{ conclusion?: string | null }> }>(
1999
- ['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
+ ]);
2000
2282
  if (!checkRuns || !Array.isArray(checkRuns.check_runs)) throw new Error('Incomplete check-run inventory');
2001
2283
  const conclusions = checkRuns.check_runs.map((run) => run.conclusion ?? null);
2002
2284
  const gatePass = (status?.state === 'success' || status?.state === undefined)
@@ -2009,15 +2291,42 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
2009
2291
  ...(gatePass && !gateFail ? { gate: 'pass' as const } : gateFail ? { gate: 'fail' as const } : {}),
2010
2292
  };
2011
2293
  };
2012
- const landingCommentsFor = (repository: string, number: number): string[] => {
2013
- const comments: string[] = [];
2014
- for (let page = 1; page <= 100; page++) {
2015
- const batch = ghJson<Array<{ body?: string }>>(['api', `repos/${repository}/issues/${number}/comments?per_page=100&page=${page}`], execute);
2016
- if (!Array.isArray(batch) || batch.length > 100) throw new Error('Incomplete landing comment inventory');
2017
- comments.push(...batch.filter((comment) => /\blanding-verified\b/i.test(comment.body ?? '')).map((comment) => comment.body!));
2018
- if (batch.length < 100) return comments;
2019
- }
2020
- 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)!;
2021
2330
  };
2022
2331
  let ledgerMatches: readonly RunLedgerMatch[] | undefined;
2023
2332
  let running: ReadonlyMap<string, string> | undefined;
@@ -2031,22 +2340,35 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
2031
2340
  return draftSweepRunStatus(ledgerMatches, running, repository, draft.number);
2032
2341
  };
2033
2342
  return {
2034
- 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
+ },
2035
2368
  listDrafts: async (page, perPage, repository): Promise<SweepDraft[]> =>
2036
- (await listed(open, repository, 'open')).filter((pr) => pr.draft)
2037
- .slice((page - 1) * perPage, page * perPage)
2038
- .map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref,
2039
- labels: pr.labels.map((item) => item.name), createdAt: pr.created_at, body: pr.body ?? undefined,
2040
- changedFiles: filesFor(repository, pr.number), ...(pr.updated_at ? { updatedAt: pr.updated_at } : {}),
2041
- runId: currentDraftSweepRunId(ledgerMatches ??= listRunLedgers().matches, repository, pr.number) })),
2369
+ (await enrichedDrafts(repository)).slice((page - 1) * perPage, page * perPage),
2042
2370
  listMerged: async (page, perPage, repository): Promise<SweepMergedPr[]> =>
2043
- (await listed(closed, repository, 'closed')).filter((pr) => pr.merged_at !== null && pr.base?.ref === 'main')
2044
- .slice((page - 1) * perPage, page * perPage)
2045
- .map((pr) => ({ number: pr.number, title: pr.title, branch: pr.head.ref, body: pr.body ?? undefined,
2046
- mergedAt: pr.merged_at ?? undefined, changedFiles: filesFor(repository, pr.number),
2047
- mergeCommitMessage: pr.merge_commit_sha ? ghJson<{ commit: { message: string } }>(
2048
- ['api', `repos/${repository}/commits/${pr.merge_commit_sha}`], execute).commit.message : undefined,
2049
- landingVerifiedComments: landingCommentsFor(repository, pr.number) })),
2371
+ (await enrichedMerged(repository)).slice((page - 1) * perPage, page * perPage),
2050
2372
  getRunStatus: async (draft, repository) => runStatusFor(draft, repository),
2051
2373
  getLatestFileChanges: async (draft, repository) => latestFileChangesFor(draft, repository),
2052
2374
  hasFinalRunResult: async (draft, repository) => {
@@ -2097,6 +2419,17 @@ export function githubDraftSweepAdapters(execute: GhExecute = executeGh, git: Dr
2097
2419
  execute(['pr', 'close', String(number), '--repo', repository, '--comment', comment]);
2098
2420
  },
2099
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
+ },
2100
2433
  getReviewGate: async (draft, repository) => reviewGateForDraft(draft, repository),
2101
2434
  };
2102
2435
  }
@@ -2107,6 +2440,28 @@ export interface HarnessDraftSweepDeps {
2107
2440
  readonly write?: (line: string) => void;
2108
2441
  readonly execute?: GhExecute;
2109
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
+ }
2110
2465
  }
2111
2466
 
2112
2467
  function draftClaimOwner(body: string): string | undefined {
@@ -2180,10 +2535,19 @@ function installHarnessDraftSweepCommand(harnessCmd: Command, deps: HarnessDraft
2180
2535
  if (!DRAFT_SWEEP_REPOSITORY.test(repository) || repository.includes('..')) {
2181
2536
  throw new HarnessCliInputError(`invalid --repo (expected owner/name): ${repository}`);
2182
2537
  }
2183
- 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! });
2184
2541
  const reason = result.error ? sweepFailureReason(result.error) : undefined;
2185
- 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 } : {}) }));
2186
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
+ }
2187
2551
  write(`draft sweep ${result.repository}: ${result.complete ? 'complete' : `incomplete (${reason})`} · ${result.apply ? 'apply' : 'dry-run'}`);
2188
2552
  for (const entry of result.entries) write(`#${entry.number} ${entry.action}: ${entry.reason}${entry.statusLabel ? ` → ${entry.statusLabel}` : ''}${entry.error ? ` ERROR: ${sweepFailureReason(entry.error)}` : ''}`);
2189
2553
  if (result.complete) {
@@ -2513,6 +2877,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2513
2877
  installHarnessProcessObservationCommand(harnessCmd, deps.processObservation);
2514
2878
  installHarnessBudgetCommand(harnessCmd);
2515
2879
  installHarnessSalvageCommand(harnessCmd);
2880
+ installHarnessSalvageRetentionCommand(harnessCmd);
2516
2881
  installHarnessGoalCommand(harnessCmd, deps.goalLookup, deps.goalArchive);
2517
2882
  installHarnessDraftSweepCommand(harnessCmd, deps.draftSweep);
2518
2883
  const queue = harnessCmd.command('queue').description('자리별 영속 발사 대기열');
@@ -2567,9 +2932,11 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
2567
2932
  queue.command('reconcile <id>').description('불확정 발사를 확인하고 종료 또는 미발사 증거가 있으면 예약 해소').action((id: string) => queueAction(async () => {
2568
2933
  console.log(`${id} ${await reconcileHarnessQueue(id, queueDeps)}`);
2569
2934
  }));
2570
- queue.command('tick').description('자리별 맨 앞 한 건을 라운드로빈으로 돌며 자리 몫·Pod 풀 여유·마무리 관문을 확인하고 한 건 발사').action(() => queueAction(async () => {
2935
+ queue.command('tick').description('자리별 맨 앞 항목을 라운드로빈으로 돌며 자리 몫·Pod 풀 여유·마무리 관문을 확인하고 발사 — 여유가 있으면 한 tick 에 여러 건(30초 간격 · harness.queue.burstMax 기본 5)').action(() => queueAction(async () => {
2571
2936
  const result = await tickHarnessQueue(queueDeps);
2572
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`);
2573
2940
  }));
2574
2941
 
2575
2942
  const ask = deps.ask;