elanous 0.2.18 → 0.2.19

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 (404) 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/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
  16. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
  28. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
  49. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
  110. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  111. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
  112. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  113. package/apps/pwa/out/approvals/index.html +1 -1
  114. package/apps/pwa/out/approvals/index.txt +23 -23
  115. package/apps/pwa/out/autopilot/index.html +1 -1
  116. package/apps/pwa/out/autopilot/index.txt +23 -23
  117. package/apps/pwa/out/board/index.html +1 -1
  118. package/apps/pwa/out/board/index.txt +23 -23
  119. package/apps/pwa/out/botlab/index.html +1 -1
  120. package/apps/pwa/out/botlab/index.txt +22 -22
  121. package/apps/pwa/out/bots/index.html +1 -1
  122. package/apps/pwa/out/bots/index.txt +22 -22
  123. package/apps/pwa/out/ceo/index.html +1 -1
  124. package/apps/pwa/out/ceo/index.txt +23 -23
  125. package/apps/pwa/out/chat/index.html +1 -1
  126. package/apps/pwa/out/chat/index.txt +22 -22
  127. package/apps/pwa/out/consult/index.html +1 -1
  128. package/apps/pwa/out/consult/index.txt +23 -23
  129. package/apps/pwa/out/control/index.html +1 -1
  130. package/apps/pwa/out/control/index.txt +23 -23
  131. package/apps/pwa/out/decisions/index.html +1 -1
  132. package/apps/pwa/out/decisions/index.txt +22 -22
  133. package/apps/pwa/out/design-check/index.html +1 -1
  134. package/apps/pwa/out/design-check/index.txt +23 -23
  135. package/apps/pwa/out/editor/index.html +1 -1
  136. package/apps/pwa/out/editor/index.txt +25 -24
  137. package/apps/pwa/out/exec/index.html +1 -1
  138. package/apps/pwa/out/exec/index.txt +23 -23
  139. package/apps/pwa/out/field/index.html +1 -1
  140. package/apps/pwa/out/field/index.txt +23 -23
  141. package/apps/pwa/out/healthz +1 -1
  142. package/apps/pwa/out/index.html +1 -1
  143. package/apps/pwa/out/index.txt +22 -22
  144. package/apps/pwa/out/inside/index.html +1 -1
  145. package/apps/pwa/out/inside/index.txt +24 -23
  146. package/apps/pwa/out/intake/index.html +1 -1
  147. package/apps/pwa/out/intake/index.txt +23 -23
  148. package/apps/pwa/out/live/index.html +1 -1
  149. package/apps/pwa/out/live/index.txt +23 -23
  150. package/apps/pwa/out/loops/index.html +1 -1
  151. package/apps/pwa/out/loops/index.txt +28 -27
  152. package/apps/pwa/out/market/index.html +1 -1
  153. package/apps/pwa/out/market/index.txt +22 -22
  154. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  155. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  156. package/apps/pwa/out/morning/index.html +1 -1
  157. package/apps/pwa/out/morning/index.txt +23 -23
  158. package/apps/pwa/out/observatory/index.html +1 -1
  159. package/apps/pwa/out/observatory/index.txt +23 -23
  160. package/apps/pwa/out/ops/checklist/index.html +1 -1
  161. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  162. package/apps/pwa/out/ops/release/index.html +1 -1
  163. package/apps/pwa/out/ops/release/index.txt +22 -22
  164. package/apps/pwa/out/ops/seats/index.html +1 -1
  165. package/apps/pwa/out/ops/seats/index.txt +22 -22
  166. package/apps/pwa/out/outputs/index.html +1 -1
  167. package/apps/pwa/out/outputs/index.txt +22 -22
  168. package/apps/pwa/out/reflection/index.html +1 -1
  169. package/apps/pwa/out/reflection/index.txt +23 -23
  170. package/apps/pwa/out/scheduler/index.html +1 -1
  171. package/apps/pwa/out/scheduler/index.txt +23 -23
  172. package/apps/pwa/out/sessions/index.html +1 -1
  173. package/apps/pwa/out/sessions/index.txt +22 -22
  174. package/apps/pwa/out/settings/devices/index.html +1 -1
  175. package/apps/pwa/out/settings/devices/index.txt +23 -23
  176. package/apps/pwa/out/settings/index.html +1 -1
  177. package/apps/pwa/out/settings/index.txt +23 -23
  178. package/apps/pwa/out/setup/done/index.html +1 -1
  179. package/apps/pwa/out/setup/done/index.txt +23 -23
  180. package/apps/pwa/out/setup/index.html +1 -1
  181. package/apps/pwa/out/setup/index.txt +23 -23
  182. package/apps/pwa/out/share/index.html +1 -1
  183. package/apps/pwa/out/share/index.txt +23 -23
  184. package/apps/pwa/out/showroom/index.html +1 -1
  185. package/apps/pwa/out/showroom/index.txt +22 -22
  186. package/apps/pwa/out/tasks/index.html +1 -1
  187. package/apps/pwa/out/tasks/index.txt +23 -23
  188. package/apps/pwa/out/term/index.html +1 -1
  189. package/apps/pwa/out/term/index.txt +23 -23
  190. package/apps/pwa/out/today/index.html +1 -1
  191. package/apps/pwa/out/today/index.txt +23 -23
  192. package/apps/pwa/out/trace/index.html +1 -1
  193. package/apps/pwa/out/trace/index.txt +23 -23
  194. package/apps/pwa/out/vault/index.html +1 -1
  195. package/apps/pwa/out/vault/index.txt +23 -23
  196. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  197. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  198. package/apps/pwa/out/workflows/index.html +1 -1
  199. package/apps/pwa/out/workflows/index.txt +23 -23
  200. package/apps/pwa/out/workspace/index.html +1 -1
  201. package/apps/pwa/out/workspace/index.txt +23 -23
  202. package/apps/pwa/out/worktrees/index.html +1 -1
  203. package/apps/pwa/out/worktrees/index.txt +23 -23
  204. package/graphs/ops/guardian.yaml +26 -0
  205. package/graphs/ops/recipes.yaml +3 -0
  206. package/package.json +1 -1
  207. package/scripts/coord-channel-watch.sh +2 -2
  208. package/scripts/discord-seat-probe.ts +3 -1
  209. package/scripts/lib/pch-regress-map.ts +23 -0
  210. package/scripts/morning-report.ts +18 -4
  211. package/scripts/pch-regress.ts +88 -0
  212. package/scripts/release-loop/gate-diff.ts +18 -0
  213. package/scripts/release-loop/gate-node.ts +338 -46
  214. package/scripts/release-loop/gate-partial.ts +106 -0
  215. package/scripts/release-loop/gate-timing.ts +120 -0
  216. package/scripts/release-loop/resume-release.ts +64 -5
  217. package/scripts/release-loop/shard-plan.ts +65 -0
  218. package/scripts/release-loop/unattended-release.ts +2 -0
  219. package/scripts/video/scene-table.ts +159 -0
  220. package/src/acp/core-turn-bridge.ts +39 -2
  221. package/src/away/presence.ts +40 -0
  222. package/src/away/release-watch.ts +255 -0
  223. package/src/chat/index.ts +2 -1
  224. package/src/chat/short-question.ts +3 -40
  225. package/src/cli/away-cli.ts +68 -0
  226. package/src/cli/guardian-cli.ts +49 -0
  227. package/src/cli/intake-cli.ts +13 -7
  228. package/src/cli/log-event-names.ts +1 -0
  229. package/src/cli/pod-cli.ts +5 -1
  230. package/src/cli/pty-drive-cli.ts +14 -3
  231. package/src/cli/release-cli.ts +32 -7
  232. package/src/cli/setup-cli.ts +31 -1
  233. package/src/cli/tasks-cli.ts +104 -11
  234. package/src/context-bus/context-now-public.ts +1 -1
  235. package/src/context-bus/context-now-surfaces.ts +16 -6
  236. package/src/context-bus/context-now.ts +97 -3
  237. package/src/core-turn/short-question.ts +100 -0
  238. package/src/dashboard/dashboard-status-lines.ts +24 -4
  239. package/src/dashboard/index.ts +50 -5
  240. package/src/dashboard/input/slash-executor.ts +6 -2
  241. package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
  242. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  243. package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
  244. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  245. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  246. package/src/domains/guardian-tick.ts +138 -0
  247. package/src/domains/outbound-alert.ts +44 -13
  248. package/src/domains/schedule-registry.ts +23 -0
  249. package/src/domains/telegram-kind-route.ts +39 -0
  250. package/src/domains/watch-default-scheduler.ts +95 -0
  251. package/src/esc-abort-gate.ts +44 -0
  252. package/src/graph-runner/runner.ts +25 -5
  253. package/src/harness/harness-cli-command.ts +2 -0
  254. package/src/harness/harness-salvage-cli.ts +78 -0
  255. package/src/harness/harness-worktree-add.ts +68 -0
  256. package/src/harness/ledger-write-guard.ts +6 -16
  257. package/src/hq/hq.ts +6 -1
  258. package/src/index.ts +52 -3
  259. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  260. package/src/instance/test-write-guard.ts +200 -0
  261. package/src/intake-plane/digest.ts +39 -15
  262. package/src/intake-plane/harness-target.ts +4 -3
  263. package/src/intake-plane/intake-sources.ts +1 -0
  264. package/src/intake-plane/items.ts +1 -0
  265. package/src/loops/orchestrator/degradation.ts +57 -0
  266. package/src/loops/orchestrator/finish-rate.ts +33 -3
  267. package/src/loops/orchestrator/tick.ts +166 -45
  268. package/src/maturity/feature-maturity.ts +6 -4
  269. package/src/maturity/tui-slash-maturity.ts +1 -0
  270. package/src/mss/logging/log-store.ts +3 -0
  271. package/src/nexus/api/http-server.ts +16 -5
  272. package/src/nexus/api/loop-edges.ts +290 -17
  273. package/src/nexus/api/ops-api.ts +96 -28
  274. package/src/nexus/index.ts +7 -0
  275. package/src/release-loop/checklist.ts +2 -0
  276. package/src/release-loop/feature-store.ts +54 -7
  277. package/src/release-loop/light-rc.ts +173 -0
  278. package/src/release-loop/placement.ts +51 -17
  279. package/src/release-loop/rubric.ts +57 -1
  280. package/src/self-dev/dev-cli.ts +36 -0
  281. package/src/self-dev/orchestrate.ts +1 -0
  282. package/src/self-dev/pr-preview.ts +179 -0
  283. package/src/self-dev/shard-run.ts +23 -7
  284. package/src/self-implement/gate-cli.ts +29 -8
  285. package/src/self-implement/goal-author-clarification.ts +88 -13
  286. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  287. package/src/self-implement/orchestrator.ts +2 -1
  288. package/src/self-implement/salvage-branches.ts +254 -0
  289. package/src/task-agent/actions.ts +63 -2
  290. package/src/task-agent/board.ts +132 -0
  291. package/src/task-agent/card-evidence.ts +203 -0
  292. package/src/task-agent/completion-evidence.ts +126 -0
  293. package/src/task-agent/judge.ts +18 -0
  294. package/src/task-agent/live-moves.ts +176 -0
  295. package/src/task-agent/mission.ts +32 -6
  296. package/src/task-agent/shadow.ts +183 -15
  297. package/src/task-agent/task-hand.ts +117 -15
  298. package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
  299. package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
  300. package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
  301. package/src/task-orchestrator/surfaces/self-implement-pod.ts +2 -2
  302. package/src/telegram-away-command.ts +38 -0
  303. package/src/telegram-commands.ts +11 -0
  304. package/src/telegram-report-log-probe.ts +21 -0
  305. package/src/telegram-report.ts +27 -3
  306. package/src/tui-client/dashboard-session.ts +1 -0
  307. package/src/user-config.ts +134 -2
  308. package/src/version/packed-revision.json +1 -1
  309. package/apps/pwa/out/_next/static/chunks/1307-30dff22c9e1f3073.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
  314. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +0 -2
  335. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  344. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-4733c967b56a4277.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/bots/page-02b073a2ac793b83.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/design-check/page-5d97b2901e0d1ea6.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/exec/page-8c3d8e63ae2e1c98.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/field/page-9b72a07d424701c6.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/intake/page-a572f58cd3ca5864.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/morning/page-83a700d4c754d802.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/not-found-7b3170f374f77b9d.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/reflection/page-1467047104141297.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/app/share/page-3cc47dca0922ab9a.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/app/tasks/page-b17cd46cf0e9b8cf.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/workspace/page-134da468538baf48.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/webpack-c2782b330447ffb6.js +0 -1
  403. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  404. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
@@ -1,20 +1,22 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
- import { randomUUID } from 'node:crypto';
5
+ import { createHash, randomUUID } from 'node:crypto';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
  import { debug } from '../../src/debug/log.js';
8
+ import { registerStandaloneLogSink } from '../../src/domains/standalone-log-sink.js';
8
9
  import { landingFreezeMessage, readLandingFreeze } from '../../src/release-loop/landing-freeze.js';
9
10
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
10
11
  import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
11
- import { diffFailures, junitFailures, parseFailures } from './gate-diff';
12
+ import { diffFailures, junitFailures, parseFailures, parseTimedOutFailures } from './gate-diff';
12
13
  import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
13
14
  import { baselineFromCutLogs } from './baseline-from-logs';
14
- import { planShards, readFileDurations, readFileMemory } from './shard-plan';
15
+ import { mergePartialFailures, readPartialPlan, testFileOfId } from './gate-partial';
16
+ import { logPlanWalltime, planDurations, planShards, readFileMemory } from './shard-plan';
15
17
  import { deriveCdpTestPatterns } from '../test-deterministic';
16
18
  import { emitNodeResult, readGraphContext } from './node-verdict.js';
17
- import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
19
+ import { runPodCommand, parsePodCpu, resolvePodCpu, type RunPodCommandOptions, type PodCommandResult, type PodCpu } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
18
20
  import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from '../../src/task-orchestrator/surfaces/pod-bun-cache.js';
19
21
  import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
20
22
  import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
@@ -23,7 +25,8 @@ interface CommandResult { rc: number; output: string; passedIds?: string[]; stal
23
25
  export interface GateRunner {
24
26
  command(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
25
27
  localCommand(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
26
- sweep(tree: string, logDir?: string, pod?: GateOptions['pod']): Promise<CommandResult>;
28
+ /** `only` (GATE-PARTIAL) restricts the sweep to these test files; absent = every test file. */
29
+ sweep(tree: string, logDir?: string, pod?: GateOptions['pod'], only?: readonly string[]): Promise<CommandResult>;
27
30
  add(tree: string, commit: string): Promise<void>;
28
31
  remove(tree: string): Promise<void>;
29
32
  snapshot(tree: string, commit: string): Promise<void>;
@@ -42,24 +45,38 @@ export interface GateOptions {
42
45
  instanceRoot?: string;
43
46
  ledgerRoot?: string;
44
47
  repo?: string;
45
- pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string; bunCache?: string };
48
+ /** freshShards — GATE-SHARD-RESUME off: every shard starts a Pod even when this logDir already holds its result. */
49
+ pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string; bunCache?: string; freshShards?: boolean;
50
+ /** GATE-SKIP-KNOWN-RETRY — failure ids the previous release already had (and env-known ones): a shard that fails only
51
+ * with these is not re-run in isolation; the verdict still classifies them against the baseline as before. */
52
+ knownFailures?: readonly string[];
53
+ /** GATE-SPEED A3① — shard Pod CPU request/limit (`release.loop.gatePodCpu`); omitted = request 1 / limit 4. */
54
+ cpu?: PodCpu };
46
55
  }
47
56
  export interface GateResult {
48
57
  outcome: 'ok' | 'regression' | 'error';
49
58
  commit: string;
50
59
  introduced: string[];
51
60
  preexisting: number;
52
- fixed: number;
61
+ /** null = not counted (GATE-SPEED A4: the swept baseline ran only the cut's failing files). */
62
+ fixed: number | null;
53
63
  knownEnv: number;
54
64
  knownEnvCleared: string[];
55
65
  stalledEnv: Array<{ file: string; reason: 'no-output' | 'isolated-timeout'; local: 'passed' | 'failed' | 'timeout' }>;
56
66
  baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
57
67
  /** GATE-BASELINE-CACHE — isolated baseline files answered from the ledger (hits) or run now (misses). */
58
68
  baselineCache?: { hits: number; misses: number };
69
+ /** GATE-SPEED A4 — a swept baseline ran only the cut's failing files that exist at the previous release. */
70
+ baselineDeferred?: { cutFiles: number; swept: number };
59
71
  durationMs: number;
60
72
  error?: string;
61
73
  stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'unattributed'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string }>;
62
74
  partialSummary?: { pass: number; fail: number; errors: number; ran: number; files: number };
75
+ /** GATE-PARTIAL — this verdict re-ran only these files and carried the prior gate's results for the rest. */
76
+ partial?: { priorCommit: string; files: number };
77
+ /** GATE-INTRO-RECHECK — new failures that were only the «timed out» shape and passed one isolated re-run with a
78
+ * {@link GATE_TIMEOUT_RECHECK_MS} per-test limit: a warning (flaky under load), not an introduced regression. */
79
+ loadFlaky?: string[];
63
80
  }
64
81
 
65
82
  const sha = /^[0-9a-f]{7,40}$/i;
@@ -116,6 +133,65 @@ const lastMatch = (output: string, pattern: RegExp): RegExpExecArray | null => {
116
133
  const summaryCount = (output: string, label: string) => Number(lastMatch(output, new RegExp(`(?:^|\\n)\\s*(\\d+) ${label}s?\\s*(?=\\n|$)`))?.[1] ?? NaN);
117
134
  /** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
118
135
  const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
136
+ /** A Pod path (`~/repo/src/…` · `../..~/repo/src/…`) as a repository-relative one. */
137
+ const podRelative = (path: string) => path.replace(/^(?:\.\.\/)*\/?home\/ubuntu\/repo\//, '');
138
+ /** `file > [error]` ids of bun's «Unhandled error between tests» blocks, or undefined when one cannot be attributed. */
139
+ function unhandledErrorIds(output: string): string[] | undefined {
140
+ const errors: string[] = [];
141
+ let file: string | undefined;
142
+ for (const raw of output.split(/\r?\n/)) {
143
+ const header = /^(?:\.\/)?((?:[\w.-]+\/)+[\w.-]+\.test\.tsx?):/.exec(raw.trim());
144
+ if (header) file = header[1];
145
+ if (/^# Unhandled error between tests/.test(raw.trim())) {
146
+ if (!file) return undefined;
147
+ errors.push(`${file} > [error]`);
148
+ file = undefined;
149
+ }
150
+ }
151
+ return errors;
152
+ }
153
+ /**
154
+ * GATE-SKIP-KNOWN-RETRY / GATE-ISOLATE-FAILED-FILES — the failing ids of one Pod shard, but only when every failure the
155
+ * summary counts is named (junit first, else the console `(fail)` lines) and every unhandled error is attributed.
156
+ * Anything less returns undefined, and the shard keeps the old retry path.
157
+ */
158
+ export function shardFailureIds(clean: string, junit: string | undefined, fails: number, errors: number): { failures: string[]; errors: string[] } | undefined {
159
+ if (!Number.isFinite(fails)) return undefined;
160
+ const stripped = stripPodRepoRoot(clean);
161
+ let failures: string[];
162
+ try { failures = junit !== undefined ? [...new Set(junitFailures(junit).map(podRelative))].sort() : parseFailures(stripped); }
163
+ catch { return undefined; }
164
+ if (failures.length !== fails) return undefined;
165
+ const errorIds = unhandledErrorIds(stripped);
166
+ if (!errorIds || errorIds.length !== (Number.isNaN(errors) ? 0 : errors)) return undefined;
167
+ return { failures, errors: [...new Set(errorIds)].sort() };
168
+ }
169
+ /** The console form `failuresOf` reads back, for failures named from junit (same shape as `pod-shard-junit-attribution`). */
170
+ const failureOutput = (failures: readonly string[], errors: readonly string[]) => [
171
+ ...failures.map((id) => { const cut = id.indexOf(' > '); return `${id.slice(0, cut)}:\n(fail) ${id.slice(cut + 3)}`; }),
172
+ ...errors.map((id) => `${testFileOfId(id)}:\n# Unhandled error between tests`),
173
+ ].join('\n');
174
+ /** Repository-relative test files with at least one passed or failed junit testcase (an empty suite is no result). */
175
+ function junitFiles(xml: string): Set<string> {
176
+ const files = new Set<string>();
177
+ for (const id of [...junitPassedIds(xml), ...junitFailures(xml)]) {
178
+ try { files.add(testFileOfId(podRelative(id))); } catch { /* an unreadable id proves nothing about its file */ }
179
+ }
180
+ return files;
181
+ }
182
+ /**
183
+ * GATE-ISOLATE-FAILED-FILES — the files of a failed shard worth re-running: those with a failing test or an unhandled
184
+ * error, and those with no junit result at all (crashed or never reached). undefined = cannot tell (fallback: whole shard).
185
+ */
186
+ export function retryFiles(paths: readonly string[], clean: string | undefined, junit: string | undefined, fails: number, errors: number): string[] | undefined {
187
+ if (junit === undefined || clean === undefined) return undefined;
188
+ const ids = shardFailureIds(clean, junit, fails, errors);
189
+ if (!ids) return undefined;
190
+ const failing = new Set([...ids.failures, ...ids.errors].map((id) => { try { return testFileOfId(id); } catch { return id; } }));
191
+ if ([...failing].some((file) => !paths.includes(file))) return undefined;
192
+ const reported = junitFiles(junit);
193
+ return paths.filter((file) => failing.has(file) || !reported.has(file));
194
+ }
119
195
  export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
120
196
  /** GT1b — a file whose measured peak is at least this runs alone at the isolation limit from the start: one such file
121
197
  * sank a whole 203-file root shard at 16Gi (0.2.6 shard 12 · `unwired-exports` 9.9 GB) and cost two halving rounds. */
@@ -150,8 +226,64 @@ const lastStartedTestFile = (output: string, paths: string[]): string | undefine
150
226
  return last;
151
227
  };
152
228
 
229
+ /**
230
+ * GATE-SHARD-RESUME (0.2.19) — the result a previous attempt left in this log directory for exactly this shard: same name,
231
+ * same commit, the same files in the same order and a test exit (rc 0|1) with its log. 0.2.18: the second gate stopped
232
+ * after 33 minutes on a tool error (unsafe test path) and the third ran all 80 minutes again although the cut commit and
233
+ * every finished shard were unchanged. The caller re-reads the log with the current tool code; anything else is a miss.
234
+ */
235
+ /** GATE-SHARD-RESUME — how a previous attempt went past this shard without a verdict of its own: it re-ran in a fresh Job
236
+ * after no output (`-retry`), or it failed here and was split (`-c0` cap chunks · `-0` halves · `-file-0` isolation). The
237
+ * split shape follows from depth and size alone, so the same plan reaches the same child names (TC 09:30 review ②).
238
+ * `runKey` hashes where and how the Pod ran the shard (pool · command · ignores · memory · deadline · Bun cache): a fix on the Pod side
239
+ * makes every old result a miss instead of re-reading an old failure as introduced (TC 09:48 review). */
240
+ export function previousShardDescent(logDir: string, name: string, commit: string, files: readonly string[], runKey: string, parentAttempt?: string, splitChild?: string): { way: 'retry' | 'split'; attemptId: string } | undefined {
241
+ const meta = (file: string): { commit?: unknown; files?: unknown; rc?: unknown; runKey?: unknown; attemptId?: unknown; parentAttempt?: unknown } | undefined => {
242
+ try { return JSON.parse(readFileSync(join(logDir, `${file}.json`), 'utf8')); } catch { return undefined; }
243
+ };
244
+ const own = meta(name);
245
+ if (own?.commit !== commit || own.runKey !== runKey || !Array.isArray(own.files) || own.files.length !== files.length || own.files.some((file, index) => file !== files[index])) return undefined;
246
+ if (typeof own.attemptId !== 'string' || (parentAttempt !== undefined && own.parentAttempt !== parentAttempt)) return undefined;
247
+ const attemptId = own.attemptId;
248
+ // Children are checked whatever the parent's rc was: a parent can exit 0|1 and still have gone down (no output · an
249
+ // unreadable summary). A child counts only for this commit and only with files taken from this shard.
250
+ const parent = new Set(files);
251
+ const child = (file: string, whole: boolean) => {
252
+ const row = meta(file);
253
+ // A child counts only when the parent's latest attempt wrote it — a child left by an older attempt is not this way down.
254
+ return row?.commit === commit && row.parentAttempt === attemptId && Array.isArray(row.files) && row.files.length > 0 && row.files.every((path) => typeof path === 'string' && parent.has(path))
255
+ && (!whole || row.files.length === files.length);
256
+ };
257
+ if (child(`${name}-retry`, true)) return { way: 'retry', attemptId };
258
+ // Only the first child of the split the caller takes now (`-file-0` · `-c0` · `-0` · review r3): a record of another
259
+ // split shape is not followed.
260
+ return splitChild !== undefined && child(`${name}${splitChild}`, false) ? { way: 'split', attemptId } : undefined;
261
+ }
262
+
263
+ export function previousShardResult(logDir: string, name: string, commit: string, files: readonly string[], runKey: string, parentAttempt?: string): { output: string; junit?: string; rc: 0 | 1; durationMs?: number } | undefined {
264
+ try {
265
+ const meta = JSON.parse(readFileSync(join(logDir, `${name}.json`), 'utf8')) as { commit?: unknown; files?: unknown; rc?: unknown; durationMs?: unknown; runKey?: unknown; parentAttempt?: unknown; jobExitCode?: unknown; jobFailed?: unknown };
266
+ // Only a Job that itself exited 0 — a crashed Job can still leave rc 0 and a clean-looking log (review r6).
267
+ if (meta.jobExitCode !== 0 || meta.jobFailed !== false) return undefined;
268
+ if (meta.commit !== commit || meta.runKey !== runKey || (parentAttempt !== undefined && meta.parentAttempt !== parentAttempt) || (meta.rc !== 0 && meta.rc !== 1) || !Array.isArray(meta.files)
269
+ || meta.files.length !== files.length || meta.files.some((file, index) => file !== files[index])) return undefined;
270
+ const logPath = join(logDir, `${name}.log`);
271
+ if (!existsSync(logPath)) return undefined;
272
+ const junitPath = join(logDir, `${name}.junit.xml`);
273
+ return { output: readFileSync(logPath, 'utf8'), rc: meta.rc, ...(existsSync(junitPath) ? { junit: readFileSync(junitPath, 'utf8') } : {}),
274
+ ...(typeof meta.durationMs === 'number' ? { durationMs: meta.durationMs } : {}) };
275
+ } catch { return undefined; }
276
+ }
277
+
278
+ /** 0.2.18 gate: a test that changed process.cwd made bun print later test files relative to that cwd
279
+ * (`../..~/repo/src/…`), and the safe-path check stopped the whole gate. The Pod clone root is
280
+ * `~/repo`; strip it back to a repository-relative path before any id is read. */
281
+ export function stripPodRepoRoot(output: string): string {
282
+ return output.replace(/^(\s*(?:\(fail\)\s+)?)(?:\.\.\/)*(?:\/)?home\/ubuntu\/repo\//gm, '$1');
283
+ }
284
+
153
285
  function failuresOf(run: CommandResult, label: string): SweepFailures {
154
- const output = run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
286
+ const output = stripPodRepoRoot(run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, ''));
155
287
  const ran = lastMatch(output, /Ran (\d+) tests? across ([1-9]\d*) files?/);
156
288
  const reportedErrors = summaryCount(output, 'error');
157
289
  const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
@@ -193,6 +325,11 @@ class StalledPodShards extends Error {
193
325
 
194
326
  /** Isolated re-runs (host stall re-run · cut and baseline re-checks) are time-limited; sweeps and installs keep their full time. */
195
327
  export const GATE_ISOLATED_TIMEOUT_MS = 300_000;
328
+ /** GATE-INTRO-RECHECK (0.2.19) — the per-test limit of the one re-run given to a «timed out after 5000ms» new failure.
329
+ * 0.2.18: both first-gate introductions were 5 s limits missed at 6.04/6.06 s, and they alone cost an 84-minute re-gate. */
330
+ export const GATE_TIMEOUT_RECHECK_MS = 60_000;
331
+ /** TEST-CENSUS intake: one row per load-flaky test, in the machine ledger (read by the test census, never by the gate). */
332
+ export const loadFlakyCensusPath = (ledger: string) => join(ledger, 'release', 'test-census', 'load-flaky.jsonl');
196
333
 
197
334
  /**
198
335
  * Runs a limited local command in its own process group. On timeout: SIGTERM the group (test-deterministic forwards it to its own
@@ -252,7 +389,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
252
389
  return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}`, ...(timedOut ? { timedOut } : {}) };
253
390
  }
254
391
  : localCommand);
255
- const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>): Promise<CommandResult> => {
392
+ const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>, only?: readonly string[]): Promise<CommandResult> => {
256
393
  const shardCount = pod.shards ?? 24;
257
394
  const deadlineSeconds = pod.shardTimeoutSeconds ?? 1200;
258
395
  if (!pod.pool || !Number.isSafeInteger(shardCount) || shardCount < 1
@@ -263,7 +400,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
263
400
  if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error('invalid cut tree HEAD');
264
401
  const listed = await command('git', ['ls-files', '*.test.*'], tree);
265
402
  check(listed, 'git ls-files tests');
266
- const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file)).sort();
403
+ const allFiles = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file)).sort();
404
+ const onlySet = only ? new Set(only) : undefined;
405
+ // GATE-PARTIAL: every planned file must exist at this commit — a deleted/renamed failing file would otherwise «pass»
406
+ // by not running. Fail closed (gate error), never a smaller sweep than planned.
407
+ const missing = only?.filter((file) => !allFiles.includes(file)) ?? [];
408
+ if (missing.length) throw new Error(`partial plan file missing at this commit: ${missing.slice(0, 5).join(', ')}`);
409
+ const files = onlySet ? allFiles.filter((file) => onlySet.has(file)) : allFiles;
267
410
  if (files.some((file) => file.startsWith('-') || file.startsWith('/') || file.startsWith('./')
268
411
  || file.split('/').some((part) => !part || part === '.' || part === '..') || /[\r\n]/.test(file))) throw new Error('unsafe pod test path');
269
412
  if (new Set(files).size !== files.length) throw new Error('duplicate pod test path');
@@ -278,7 +421,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
278
421
  const integrationOnly = files.filter((file) => POD_SWEEP_INTEGRATION_ONLY.includes(file));
279
422
  if (integrationOnly.length) debug.log('release-loop.gate', 'pod-sweep-integration-only', { files: integrationOnly });
280
423
  if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
281
- const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
424
+ const { durations, wall: planWall } = planDurations(pod.durationSource);
282
425
  const memory = readFileMemory(join(tree, POD_MEMORY_SOURCE));
283
426
  // The TD1 measurements predate the fixture-only gate; do not isolate those five files at their old 2–13 GB peaks.
284
427
  const fixtureOnly = new Set([...GATE_NIGHTLY_AUDITS, 'test/guardian/dispatch-surface-contract.test.ts', 'test/user-config-mcp.test.ts']);
@@ -288,6 +431,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
288
431
  ...(light.length ? planShards(light, durations, shardCount) : []),
289
432
  ...[...heavy].map((file) => ({ files: [file], plannedSeconds: durations.get(file) ?? 0 })),
290
433
  ];
434
+ logPlanWalltime(planWall, shards);
291
435
  if (heavy.size) debug.log('release-loop.gate', 'pod-heavy-alone', { files: [...heavy], thresholdMb: POD_HEAVY_FILE_MB, memoryLimit: POD_ISOLATION_MEMORY_LIMIT });
292
436
  const known = assignable.filter((file) => durations.has(file)).length;
293
437
  debug.log('release-loop.gate', 'pod-shard-plan', {
@@ -295,6 +439,9 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
295
439
  maxPlannedSeconds: Math.max(...shards.map((shard) => shard.plannedSeconds)),
296
440
  source: durations.size ? pod.durationSource : 'none',
297
441
  });
442
+ // GATE-SPEED A3①: shard Pod CPU (`release.loop.gatePodCpu` · default request 1 / limit 4) — shards × request must fit the pool.
443
+ const cpu = resolvePodCpu(pod.cpu);
444
+ debug.log('release-loop.gate', 'shard-resources', { shards: shards.length, cpuRequest: cpu.request, cpuLimit: cpu.limit, cpuSource: pod.cpu ? 'config' : 'default' });
298
445
  const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), defaultKubectl);
299
446
  if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
300
447
  const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
@@ -315,18 +462,22 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
315
462
  type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
316
463
  // 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
317
464
  const isolationRemaining = shards.map((item) => item.files.length);
465
+ const knownFailures = new Set(pod.knownFailures ?? []);
318
466
  const estimated = (paths: string[]) => paths.reduce((sum, file) => {
319
467
  const value = durations.get(file);
320
468
  return sum + (value !== undefined && Number.isFinite(value) && value >= 0 ? value : unknownSeconds);
321
469
  }, 0);
322
470
  const knownTimes = assignable.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
323
471
  const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
324
- const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
472
+ const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false, fresh = pod.freshShards === true, parentAttempt?: string): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
325
473
  const ignores = cdpPatterns.filter((pattern) => paths.includes(pattern))
326
474
  .flatMap((pattern) => ['--path-ignore-patterns', pattern]);
327
475
  // 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
328
476
  const args = ['bun', 'run', 'test:deterministic', ...ignores, ...paths.map(asPath)].map(quote).join(' ')
329
477
  + ' --reporter=junit --reporter-outfile="$HOME/outbox/junit.xml"';
478
+ const memoryLimit = paths.length === 1 && (depth >= 1 || heavy.has(paths[0]!)) ? POD_ISOLATION_MEMORY_LIMIT : undefined;
479
+ const podShell = `mkdir -p "$HOME/outbox"; ${cachePrefix}(cd .. && cd repo && bun install && (cd apps/pwa && bun install) && ${args}) 2>&1 | tee "$HOME/outbox/shard.log"; echo \${PIPESTATUS[0]} > "$HOME/outbox/shard.rc"`;
480
+ const runKey = createHash('sha256').update(JSON.stringify({ pool: pod.pool, podShell, memoryLimit: memoryLimit ?? null, deadlineSeconds, bunCache: bunCache ?? null })).digest('hex');
330
481
  const start = Date.now();
331
482
  let output: string | undefined;
332
483
  let junit: string | undefined;
@@ -334,14 +485,41 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
334
485
  let jobExitCode: number | undefined;
335
486
  let lastFile: string | undefined;
336
487
  let jobFailed = false;
337
- try {
488
+ // The way down a previous attempt took wins over its own log: that log was not a verdict, or it would have no children.
489
+ // The split this shard would take if it failed here — the same rule as the split code below (isolation at depth 2 ·
490
+ // cap chunks for a big root shard · halves otherwise · none for one file).
491
+ // Isolation follows only while this shard's isolation budget still covers every file (review r7) — the budget is the
492
+ // shard's file count and isolations take disjoint files, so this holds unless the plan changed; if not, run the Pod.
493
+ const splitChild = depth === 2 && paths.length > 1 ? (isolationRemaining[shard]! >= paths.length ? '-file-0' : undefined)
494
+ : depth < 2 && paths.length > 1 ? (depth === 0 && paths.length > POD_SHARD_FILE_CAP ? '-c0' : '-0') : undefined;
495
+ const found = !fresh && logDir ? previousShardDescent(logDir, `pod-${shard}${branch}`, commit, paths, runKey, parentAttempt, splitChild) : undefined;
496
+ const descent = found?.way;
497
+ const reused = !fresh && !descent && logDir ? previousShardResult(logDir, `pod-${shard}${branch}`, commit, paths, runKey, parentAttempt) : undefined;
498
+ // This shard's attempt id: the recorded one when its way down is followed, a new one when a Pod runs. Children carry it.
499
+ const attemptId = found?.attemptId ?? randomUUID();
500
+ if (descent === 'retry') {
501
+ // «descend» is only the way down; «resumed» comes from each leaf whose log reads cleanly (review r4).
502
+ debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: 'retry' });
503
+ return runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId);
504
+ }
505
+ if (descent === 'split') {
506
+ output = undefined;
507
+ debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: 'split' });
508
+ } else if (reused) {
509
+ output = reused.output;
510
+ junit = reused.junit;
511
+ rc = reused.rc;
512
+ jobExitCode = 0;
513
+ lastFile = lastStartedTestFile(output, paths);
514
+ } else try {
338
515
  const job = await podCommand({
339
516
  pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
340
517
  ...(bunCache ? { bunCache } : {}),
518
+ ...(pod.cpu ? { cpu: pod.cpu } : {}),
341
519
  // 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
342
- ...(paths.length === 1 && (depth >= 1 || heavy.has(paths[0]!)) ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
520
+ ...(memoryLimit ? { memoryLimit } : {}),
343
521
  name: `gate-${randomUUID()}`,
344
- command: ['bash', '-lc', `mkdir -p "$HOME/outbox"; ${cachePrefix}(cd .. && cd repo && bun install && (cd apps/pwa && bun install) && ${args}) 2>&1 | tee "$HOME/outbox/shard.log"; echo \${PIPESTATUS[0]} > "$HOME/outbox/shard.rc"`],
522
+ command: ['bash', '-lc', podShell],
345
523
  });
346
524
  jobExitCode = job.exitCode;
347
525
  const logPath = join(job.artifactsDir, 'shard.log');
@@ -360,17 +538,21 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
360
538
  jobFailed = true;
361
539
  }
362
540
  const durationMs = Date.now() - start;
363
- if (logDir) {
541
+ // A reused shard keeps its files as they are — rewriting would replace the measured durationMs the next release plans by.
542
+ if (logDir && !reused && !descent) {
364
543
  mkdirSync(logDir, { recursive: true });
544
+ // GATE-SHARD-RESUME: the json is what makes a log reusable, so it goes first and comes back last — a stop between
545
+ // the new log and the new json leaves no json, never the old Job's exit paired with the new log (review r8).
546
+ rmSync(join(logDir, `pod-${shard}${branch}.json`), { force: true });
365
547
  const destination = join(logDir, `pod-${shard}${branch}.log`);
366
548
  if (output !== undefined) writeFileSync(destination, output, { mode: 0o600 });
367
549
  else rmSync(destination, { force: true });
368
- writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths, plannedSeconds: estimated(paths), shardCount: shards.length, commit }) + '\n', { mode: 0o600 });
369
550
  const junitDestination = join(logDir, `pod-${shard}${branch}.junit.xml`);
370
551
  if (junit !== undefined) writeFileSync(junitDestination, junit, { mode: 0o600 });
371
552
  else rmSync(junitDestination, { force: true });
553
+ writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null, files: paths, plannedSeconds: estimated(paths), shardCount: shards.length, commit, runKey, attemptId, ...(parentAttempt ? { parentAttempt } : {}), jobExitCode: jobExitCode ?? null, jobFailed }) + '\n', { mode: 0o600 });
372
554
  }
373
- debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds: parseInstallSeconds(output ?? '') });
555
+ if (!reused && !descent) debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds: parseInstallSeconds(output ?? '') });
374
556
  let clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
375
557
  const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
376
558
  const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
@@ -379,7 +561,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
379
561
  let reason: ShardReason | undefined;
380
562
  let namedFailures: number | undefined;
381
563
  let unattributedDetail: string | undefined;
382
- if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
564
+ if (descent === 'split') reason = 'incomplete';
565
+ else if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
383
566
  else if (!clean || rc === undefined) reason = 'no-output';
384
567
  else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
385
568
  || Number(ran[2]) === 0 || Number(ran[2]) > paths.length
@@ -410,19 +593,51 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
410
593
  }
411
594
  }
412
595
  }
596
+ // A reused result that the current tool cannot read cleanly is not a verdict: run that shard for real.
597
+ if (reused && reason) {
598
+ debug.log('release-loop.gate', 'pod-shard-resume-miss', { shard, branch, reason });
599
+ return runShard(paths, shard, depth, branch, retriedNoOutput, true, parentAttempt);
600
+ }
413
601
  if (reason === 'no-output' && !retriedNoOutput) {
414
- const retry = await runShard(paths, shard, depth, `${branch}-retry`, true);
602
+ const retry = await runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId);
415
603
  debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
416
604
  return retry;
417
605
  }
418
606
  if (reason) {
607
+ // GATE-SKIP-KNOWN-RETRY — a shard whose summary is complete but whose failures could not be read back (unattributed)
608
+ // and whose every named failure is one the previous release already had is not re-run: re-running only re-measures
609
+ // known failures (0.2.18 cut: 109 retry Pods · 1,840 shard-minutes, 4x the first pass). Those failures go to the
610
+ // verdict as they are, so it still classifies them against the baseline (preexisting) — never silently dropped.
611
+ if (reason === 'unattributed' && knownFailures.size && clean && ran) {
612
+ const ids = shardFailureIds(clean, junit, fails, errors);
613
+ const all = ids ? [...ids.failures, ...ids.errors] : [];
614
+ if (ids && all.length && all.every((id) => knownFailures.has(id))) {
615
+ debug.log('release-loop.gate', 'retry-skipped-known', { shard, failures: all.length });
616
+ return { runs: [{ output: `${failureOutput(ids.failures, ids.errors)}\n`, rc: 1, pass: Number.isFinite(passes) ? passes : 0,
617
+ fail: ids.failures.length, errors: ids.errors.length, ran: Number(ran[1]), files: Number(ran[2]),
618
+ passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
619
+ }
620
+ }
621
+ // GATE-ISOLATE-FAILED-FILES — with this shard's junit, only the files with a failing test or an unhandled error and
622
+ // the files with no junit result (crashed · never reached) are re-run; the rest passed here and keep that result.
623
+ // The narrowed set runs under its own branch (`-n`) at the same depth, so the usual split/isolation rules still apply
624
+ // to it, and a resumed gate finds this shard's own log (not a `-c0`/`-0` child) and narrows the same way again.
625
+ const narrowed = !descent ? retryFiles(paths, clean, junit, fails, errors) : undefined;
626
+ if (narrowed && narrowed.length && narrowed.length < paths.length) {
627
+ debug.log('release-loop.gate', 'retry-narrowed', { shard, before: paths.length, after: narrowed.length });
628
+ const kept = new Set(paths.filter((file) => !narrowed.includes(file)));
629
+ const passedIds = junitPassedIds(junit!).map(podRelative).filter((id) => { try { return kept.has(testFileOfId(id)); } catch { return false; } });
630
+ const child = await runShard(narrowed, shard, depth, `${branch}-n`, false, undefined, attemptId);
631
+ return { runs: [{ output: '', rc: 0, pass: passedIds.length, fail: 0, errors: 0, ran: passedIds.length, files: kept.size, passedIds },
632
+ ...child.runs], stalled: child.stalled };
633
+ }
419
634
  // 깊이 2 에서 «이름 없는 실패»·«불완전»도 파일 단위로 가른다 — 아니면 149파일 조각의 실패 하나가 끝까지 주인 없이 남는다(09-30 G1e 7번 조각: 요약 28 · 이름 27).
420
635
  // 로그 없이 죽은 조각(`no-output` · OOM)도 같다 — 09-30 G1f ② 4-0-0 은 150파일이 격리 0 으로 남았다.
421
636
  if (depth === 2 && paths.length > 1) {
422
637
  const count = Math.min(paths.length, isolationRemaining[shard]!);
423
638
  isolationRemaining[shard]! -= count;
424
639
  const children = await Promise.all(paths.slice(0, count).map(async (file, index) => {
425
- const child = await runShard([file], shard, depth + 1, `${branch}-file-${index}`);
640
+ const child = await runShard([file], shard, depth + 1, `${branch}-file-${index}`, false, undefined, attemptId);
426
641
  debug.log('release-loop.gate', 'pod-shard-isolate', { shard, file, outcome: child.stalled[0]?.reason ?? 'ok' });
427
642
  return child;
428
643
  }));
@@ -443,17 +658,18 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
443
658
  if (depth === 0 && paths.length > POD_SHARD_FILE_CAP) {
444
659
  const chunks = Array.from({ length: Math.ceil(paths.length / POD_SHARD_FILE_CAP) }, (_, i) => paths.slice(i * POD_SHARD_FILE_CAP, (i + 1) * POD_SHARD_FILE_CAP));
445
660
  debug.log('release-loop.gate', 'pod-shard-cap-split', { shard, files: paths.length, chunks: chunks.length, cap: POD_SHARD_FILE_CAP, reason });
446
- const children = await Promise.all(chunks.map((chunk, i) => runShard(chunk, shard, 2, `${branch}-c${i}`)));
661
+ const children = await Promise.all(chunks.map((chunk, i) => runShard(chunk, shard, 2, `${branch}-c${i}`, false, undefined, attemptId)));
447
662
  return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
448
663
  }
449
664
  debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
450
665
  const middle = Math.ceil(paths.length / 2);
451
666
  const children = await Promise.all([
452
- runShard(paths.slice(0, middle), shard, depth + 1, `${branch}-0`),
453
- runShard(paths.slice(middle), shard, depth + 1, `${branch}-1`),
667
+ runShard(paths.slice(0, middle), shard, depth + 1, `${branch}-0`, false, undefined, attemptId),
668
+ runShard(paths.slice(middle), shard, depth + 1, `${branch}-1`, false, undefined, attemptId),
454
669
  ]);
455
670
  return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
456
671
  }
672
+ if (reused) debug.log('release-loop.gate', 'pod-shard-resumed', { shard, branch, files: paths.length, rc, durationMs: reused.durationMs ?? null });
457
673
  return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
458
674
  errors: Number.isNaN(errors) ? 0 : errors,
459
675
  ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
@@ -480,12 +696,16 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
480
696
  localCommand: commandOverride && !remote ? commandOverride : localCommand,
481
697
  get remoteMirror() { return remoteMirror; },
482
698
  set remoteMirror(path) { remoteMirror = path; },
483
- async sweep(tree, logDir, pod) {
484
- if (pod) return podSweep(tree, logDir, pod);
699
+ async sweep(tree, logDir, pod, only) {
700
+ if (pod) return podSweep(tree, logDir, pod, only);
485
701
  const listed = await command('git', ['ls-files', '*.test.*'], tree);
486
702
  check(listed, 'git ls-files tests');
487
- const files = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file));
488
- const groups = [
703
+ const allFiles = listed.output.split(/\r?\n/).filter((file) => /\.test\.(?:tsx?|jsx?|mts|cts)$/.test(file));
704
+ const onlySet = only ? new Set(only) : undefined;
705
+ const missing = only?.filter((file) => !allFiles.includes(file)) ?? [];
706
+ if (missing.length) throw new Error(`partial plan file missing at this commit: ${missing.slice(0, 5).join(', ')}`);
707
+ const files = onlySet ? allFiles.filter((file) => onlySet.has(file)) : allFiles;
708
+ const groups = onlySet ? [{ name: 'partial', paths: files }] : [
489
709
  { name: 'src-cli', paths: ['src/cli'] },
490
710
  { name: 'src-rest', paths: [...new Set(files.filter((f) => f.startsWith('src/') && !f.startsWith('src/cli/')).map((f) => f.split('/').length === 2 ? f : f.split('/').slice(0, 2).join('/')))] },
491
711
  { name: 'test', paths: ['test'] },
@@ -579,11 +799,7 @@ export function baselineCommit(root: string, version: string): string {
579
799
  return commit;
580
800
  }
581
801
 
582
- function fileOf(id: string): string {
583
- const file = id.split(' > ', 1)[0]!;
584
- if (!id.includes(' > ') || !/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
585
- return file;
586
- }
802
+ const fileOf = testFileOfId;
587
803
 
588
804
  /**
589
805
  * GATE-BASELINE-CACHE (0.2.18) — the isolated `bun run test:deterministic <file>` result of one test file at one commit,
@@ -716,8 +932,25 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
716
932
  trees.push(cutTree);
717
933
  await runner.add(cutTree, opts.commit);
718
934
  for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
719
- const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
720
- opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
935
+ // GATE-PARTIAL: a plan made by `release resume --from gate --partial` for exactly this commit re-runs only its files.
936
+ // Its logs go to cut-partial/ — the full cut/ logs stay whole (the next release reads them as a baseline source).
937
+ const partial = readPartialPlan(roots, opts.version, opts.commit);
938
+ if (partial) debug.log('release-loop.gate', 'partial-regate', { version: opts.version, priorCommit: partial.priorCommit, commit: opts.commit, files: partial.files.length });
939
+ // GATE-SKIP-KNOWN-RETRY — the ids the verdict will not count as introduced (the recorded baseline ⊕ env-known), read
940
+ // before the sweep so a shard failing only with them skips its isolation retries. No recorded baseline = no skip.
941
+ let knownFailures: string[] = [];
942
+ if (opts.pod) {
943
+ try {
944
+ const knownSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
945
+ const saved = cachedBaseline(opts.baselineVersion, knownSha)?.saved;
946
+ const recorded = saved ? undefined : baselineFromCutLogs(join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), knownSha);
947
+ const trustedKnown = saved ?? (recorded?.complete ? recorded : undefined);
948
+ if (trustedKnown) knownFailures = [...trustedKnown.failures, ...(trustedKnown.errors ?? []), ...loadEnvKnownFailures(repo)];
949
+ } catch (error) { debug.log('release-loop.gate', 'known-failures-unavailable', { error: String(error) }); }
950
+ }
951
+ const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', partial ? 'cut-partial' : 'cut'),
952
+ opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), ...(knownFailures.length ? { knownFailures } : {}) } : undefined,
953
+ partial?.files);
721
954
  const localStalled = async (run: CommandResult, tree: string, commit: string, label: string) => {
722
955
  const stalled = run.stalledEnv ?? [];
723
956
  const noCompletedShard = stalled.length && run.rc === 0
@@ -757,7 +990,9 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
757
990
  debug.log('release-loop.gate', 'pod-shard-stalled-env', { files: stalled.map(({ file, reason }) => ({ file, reason })), local });
758
991
  return measured;
759
992
  };
760
- const cut = await localStalled(cutRun, cutTree, opts.commit, 'cut');
993
+ const measuredCut = await localStalled(cutRun, cutTree, opts.commit, 'cut');
994
+ const cut = partial ? mergePartialFailures(partial, measuredCut) : measuredCut;
995
+ if (partial) result.partial = { priorCommit: partial.priorCommit, files: partial.files.length };
761
996
  cutFailures = cut;
762
997
  const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
763
998
  const cached = cachedBaseline(opts.baselineVersion, baseSha);
@@ -782,11 +1017,28 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
782
1017
  };
783
1018
  let baseline: SweepFailures | undefined = trusted ? { failures: trusted.failures, errors: trusted.errors ?? [] } : undefined;
784
1019
  if (!baseline) {
785
- // K9b — with no reusable ledger the baseline is swept on the same Pod pool as the cut, not on this host (0.2.6: an
786
- // hour of local baseline after a 23-minute Pod cut).
787
- const baseRun = await runner.sweep(await getBaseTree(), join(root, 'release', opts.version, 'gate-logs', 'baseline'),
788
- opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
789
- baseline = await localStalled(baseRun, baseTree!, baseSha, 'baseline');
1020
+ // GATE-SPEED A4 (RFC-release-gate-under-30min §A4) — with no recorded baseline only the files that failed at the cut
1021
+ // are swept at the previous release; a passing cut file needs no comparison. Files absent there stay new failures.
1022
+ const cutFiles = [...new Set([...cut.failures, ...cut.errors].map(fileOf))].sort();
1023
+ let present: string[] = [];
1024
+ if (cutFiles.length) {
1025
+ const tree = await getBaseTree();
1026
+ // -z: the default output quotes paths with special characters, which would read as absent.
1027
+ const listed = await runner.command('git', ['ls-files', '-z', '--', ...cutFiles], tree);
1028
+ check(listed, 'baseline ls-files');
1029
+ const known = new Set(listed.output.split('\0'));
1030
+ present = cutFiles.filter((file) => known.has(file));
1031
+ }
1032
+ result.baselineDeferred = { cutFiles: cutFiles.length, swept: present.length };
1033
+ debug.log('release-loop.gate', 'baseline-deferred', { version: opts.baselineVersion, commit: baseSha, cutFiles: cutFiles.length, swept: present.length });
1034
+ if (present.length) {
1035
+ // K9b — the baseline is swept on the same Pod pool as the cut, not on this host (0.2.6: an hour of local baseline
1036
+ // after a 23-minute Pod cut).
1037
+ const baseRun = await runner.sweep(baseTree!, join(root, 'release', opts.version, 'gate-logs', 'baseline'),
1038
+ opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined,
1039
+ present);
1040
+ baseline = await localStalled(baseRun, baseTree!, baseSha, 'baseline');
1041
+ } else baseline = { failures: [], errors: [] };
790
1042
  }
791
1043
  for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
792
1044
  const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
@@ -798,7 +1050,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
798
1050
  result.knownEnv = countedCut.knownEnv.length;
799
1051
  result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id) && !cutRun.stalledEnv?.some(({ file }) => fileOf(id) === file)).sort();
800
1052
  const diff = diffFailures(countedCut.counted, countedBaseline.counted);
801
- result.fixed = diff.fixed.length;
1053
+ result.fixed = result.baselineDeferred ? null : diff.fixed.length;
802
1054
  result.preexisting = diff.common.length;
803
1055
  // GATE-BASELINE-CACHE — only the files whose new failures reproduce in isolation reach the baseline, and each
804
1056
  // (commit, file, host) is run at most once across gates: this gate's cut results also seed the next gate's baseline.
@@ -829,7 +1081,36 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
829
1081
  : failuresOf(cutCheck, `cut isolated ${file}`);
830
1082
  if (cutCheck !== undefined) remember({ commit: opts.commit, file, host: cacheHost, failures: isolatedCut.failures, errors: isolatedCut.errors, missing: false });
831
1083
  const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
832
- const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
1084
+ let candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
1085
+ // GATE-INTRO-RECHECK — a new failure that is only bun's «timed out» shape gets one more isolated run with a generous
1086
+ // per-test limit, on the same runner (node-b when the gate is remote). Passing there = flaky under load (warning);
1087
+ // an assertion failure, a failure again, or a run that cannot be read stays introduced.
1088
+ const timedOut = cutCheck === undefined ? new Set<string>() : new Set(parseTimedOutFailures(stripPodRepoRoot(cutCheck.output)));
1089
+ const shaped = candidates.filter((id) => timedOut.has(id) && isolatedCut.failures.includes(id));
1090
+ if (shaped.length) {
1091
+ const relaxed = await runner.command('bun', ['run', 'test:deterministic', asPath(file), '--timeout', String(GATE_TIMEOUT_RECHECK_MS)], cutTree, GATE_ISOLATED_TIMEOUT_MS);
1092
+ let flaky: string[] = [];
1093
+ let recheck: 'read' | 'timeout' | 'unreadable' = 'read';
1094
+ if (relaxed.timedOut) recheck = 'timeout';
1095
+ else {
1096
+ try {
1097
+ const again = failuresOf(relaxed, `cut timeout recheck ${file}`);
1098
+ const still = new Set([...again.failures, ...again.errors]);
1099
+ flaky = again.errors.length ? [] : shaped.filter((id) => !still.has(id));
1100
+ } catch { recheck = 'unreadable'; }
1101
+ }
1102
+ debug.log('release-loop.gate', 'timeout-recheck', { file, shaped: shaped.length, flaky: flaky.length, recheck, remote: opts.remote ?? 'local', limitMs: GATE_TIMEOUT_RECHECK_MS });
1103
+ if (flaky.length) {
1104
+ (result.loadFlaky ??= []).push(...flaky);
1105
+ candidates = candidates.filter((id) => !flaky.includes(id));
1106
+ try {
1107
+ const path = loadFlakyCensusPath(ledger);
1108
+ mkdirSync(dirname(path), { recursive: true });
1109
+ const at = new Date().toISOString();
1110
+ appendFileSync(path, flaky.map((id) => JSON.stringify({ at, version: opts.version, commit: opts.commit, id, host: cacheHost, limitMs: GATE_TIMEOUT_RECHECK_MS }) + '\n').join(''), { mode: 0o600 });
1111
+ } catch (error) { debug.log('release-loop.gate', 'load-flaky-census-write-failed', { file, error: String(error) }); }
1112
+ }
1113
+ }
833
1114
  if (candidates.length === 0) continue;
834
1115
  let oldFailures: Set<string>;
835
1116
  const hit = baseCache.get(`${file}\0${cacheHost}`);
@@ -912,7 +1193,9 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
912
1193
  export function graphGateResult(result: GateResult, graph: boolean) {
913
1194
  return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
914
1195
  verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
915
- summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
1196
+ summary: (result.partial ? `부분 재검 ${result.partial.files}파일(앞 게이트 ${result.partial.priorCommit.slice(0, 9)} 결과 이음) · ` : '')
1197
+ + (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed ?? '못 셈(기준선 미루기)'}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
1198
+ + (result.loadFlaky?.length ? ` · ⚠ 부하 흔들림 ${result.loadFlaky.length}(시간 초과 꼴 · ${GATE_TIMEOUT_RECHECK_MS / 1000}s 재실행 통과 · 막지 않음)` : '')
916
1199
  + (result.stalledEnv.length ? ` · 환경 멈춤 ${result.stalledEnv.length} (Pod 밖 통과 ${result.stalledEnv.filter((item) => item.local === 'passed').length})` : '') };
917
1200
  }
918
1201
 
@@ -936,12 +1219,17 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
936
1219
  ...(fromGraph.gatePodShards !== undefined ? { shards: Number(fromGraph.gatePodShards) } : {}),
937
1220
  ...(fromGraph.gatePodShardTimeoutSeconds !== undefined ? { shardTimeoutSeconds: Number(fromGraph.gatePodShardTimeoutSeconds) } : {}),
938
1221
  ...(typeof fromGraph.gatePodBunCache === 'string' && fromGraph.gatePodBunCache.trim() ? { bunCache: fromGraph.gatePodBunCache } : {}),
1222
+ ...(fromGraph.gatePodFreshShards === true ? { freshShards: true } : {}),
1223
+ ...((cpu) => cpu ? { cpu } : {})(parsePodCpu(fromGraph.gatePodCpu, 'release.loop.gatePodCpu')),
939
1224
  } : undefined,
940
1225
  };
1226
+ let freshShards = false;
941
1227
  for (let i = 0; i < args.length; i++) {
942
1228
  const arg = args[i];
943
1229
  if (arg === '--help') return 'help';
944
1230
  if (arg === '--json') continue;
1231
+ // Applied after the loop and only to a Pod gate — on its own it must not turn a local gate into a Pod one (review r5).
1232
+ if (arg === '--fresh-shards') { freshShards = true; continue; }
945
1233
  const key = ({ '--commit': 'commit', '--version': 'version', '--baseline-version': 'baselineVersion', '--baseline-commit': 'baselineCommit', '--remote': 'remote', '--remote-mirror': 'remoteMirror', '--pod-pool': 'pod', '--pod-shards': 'pod', '--pod-shard-timeout-seconds': 'pod' } as Record<string, keyof GateOptions>)[arg!];
946
1234
  if (!key) throw new Error(`unknown option: ${arg}`);
947
1235
  const value = args[++i];
@@ -956,17 +1244,21 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
956
1244
  else if (key === 'remote') opts.remote = value;
957
1245
  else if (key === 'remoteMirror') opts.remoteMirror = value;
958
1246
  }
1247
+ if (freshShards && opts.pod) opts.pod = { ...opts.pod, freshShards: true };
959
1248
  return opts;
960
1249
  }
961
1250
 
962
1251
  if (import.meta.main) {
963
1252
  const start = Date.now();
1253
+ // The graph spawns this file as its own process, which inherits no daemon sink — without this every
1254
+ // `release-loop.gate`/`release.gate`/`pod.pool` event stayed in the file trail and `elanous logs` read 0 rows.
1255
+ await registerStandaloneLogSink('release-loop');
964
1256
  let result: GateResult;
965
1257
  let version = '';
966
1258
  try {
967
1259
  const opts = parseOptions(process.argv.slice(2), process.env);
968
1260
  if (opts === 'help') {
969
- console.log('Usage: bun scripts/release-loop/gate-node.ts --commit <sha> --version <v> [--baseline-version <prev>] [--baseline-commit <sha>] [--remote <ssh-host>] [--remote-mirror <path>] [--pod-pool <pool>] [--pod-shards <n>] [--pod-shard-timeout-seconds <n>] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.');
1261
+ console.log('Usage: bun scripts/release-loop/gate-node.ts --commit <sha> --version <v> [--baseline-version <prev>] [--baseline-commit <sha>] [--remote <ssh-host>] [--remote-mirror <path>] [--pod-pool <pool>] [--pod-shards <n>] [--pod-shard-timeout-seconds <n>] [--fresh-shards] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.');
970
1262
  process.exit(0);
971
1263
  }
972
1264
  version = opts.version;