elanous 0.2.37 → 0.2.40

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 (727) 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/{8kITumG9GAZ1LwT9POrAR → HjeK7zONHfR7op1hKXp4k}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{1746.3563617ea64023a3.js → 1060.014f1f01a17aec26.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/{9151.a2c51e93f9156dae.js → 1281.d81877e35b9068f4.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/1350.59f40e39e02d91b1.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1415.79821ec7ae99acdb.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1452.162bb2d9d699e82a.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/147.f92f63bbc5e85fa5.js +286 -0
  10. package/apps/pwa/out/_next/static/chunks/1537-6bb97b5214928ea9.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/1661.fb425a1d8a065b72.js +149 -0
  12. package/apps/pwa/out/_next/static/chunks/1671-1c22b40b50b208f2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/1758.0204d3f48783c49d.js +90 -0
  14. package/apps/pwa/out/_next/static/chunks/1806.027bf78b6a94d340.js +104 -0
  15. package/apps/pwa/out/_next/static/chunks/{3578.4e9f9948a9a6f2f4.js → 2047.8f23e210995d4548.js} +2 -2
  16. package/apps/pwa/out/_next/static/chunks/2061-7f2bbc53ae96c88b.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/{7424.c0fe2a595d4232b5.js → 2066.a77257c8c350770a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/2096-a938496bc996dcbd.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/{4931.95cc6dbd230774ca.js → 2177.df8547f9d7215c4e.js} +16 -16
  20. package/apps/pwa/out/_next/static/chunks/2200-9a453b8f99687d49.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/{6680.6a693c28aae4cd19.js → 2226.2cf61ec781827219.js} +2 -2
  22. package/apps/pwa/out/_next/static/chunks/2281-b8cede695c6c650e.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/2499.02fbfd18126c10c0.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/{6252.240578f10952e091.js → 2522.f2766b76d22c8bdf.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/{1262.a89bb27a5b2f844e.js → 2566.beaffc3a5e1fcd2e.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/261.0d471e8aded799f3.js +226 -0
  27. package/apps/pwa/out/_next/static/chunks/2615.3c9738153de3ef3f.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/2727.e703d99ac414916d.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/2752.7904b8df0254d0ae.js +27 -0
  30. package/apps/pwa/out/_next/static/chunks/2762-5051a862dfa40e99.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/2767-31784bfedf8e245d.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/{2318-450e75d07605b8fe.js → 2774-719820e3f5b2a3eb.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/2812-f016aa0dda149287.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{4725-34bed25720cc326b.js → 2829-734fe8bf2eee7347.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/311-32dcac79970985e8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/3175-f93f7495cd28253c.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/3184.8da993f5502d2140.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/3231-d7c981db0ff8bdcc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/3250.151309c5176d6b4b.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/3330.7443183b7e6fdbdf.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/3517.a5aa1797a9dbf2ee.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/3567-45f676877ed9e8a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/3746-f80a78f220bf37b5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/3819.8a1de79b2e8c7298.js +24 -0
  45. package/apps/pwa/out/_next/static/chunks/39.9b37b0be4c128a83.js +77 -0
  46. package/apps/pwa/out/_next/static/chunks/{4337e36d.f42c3b391b3d8f20.js → 3b355b3d.6f73c98bf6f9c749.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/4023.a19045970a9ce16a.js +36 -0
  48. package/apps/pwa/out/_next/static/chunks/{58edce7d.f1b0c72acf13b123.js → 409c787b.e48c5bec8ec10456.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/413-24c8284bda08241d.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/4135.e45d6ebc29e20647.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/4283.65502f5d0e6ff6c0.js +287 -0
  52. package/apps/pwa/out/_next/static/chunks/4305-4e28fa92a7b34a5b.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/4335-50efa61fd7613142.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/4351.0f37a6db2b34d50b.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/{5046.7fd5cbd859299448.js → 470.51a72a2348306455.js} +2 -2
  56. package/apps/pwa/out/_next/static/chunks/4720.ecbd1693fa151068.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/{2375.bf364c85dbb6f085.js → 4769.9fd582071301f382.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/483.725e4a61be922788.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/{de8446d3.31b73eb239fecd7b.js → 4864a2ca.1c1c280a7772c5e5.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/4969-4b209d7307410c00.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/5068-c7f53862f83c0917.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/5315.bc96711c24dfefe2.js +74 -0
  63. package/apps/pwa/out/_next/static/chunks/{4383.18e5c54db3a22c88.js → 5383.bdfa5d886e39a826.js} +1 -1
  64. package/apps/pwa/out/_next/static/chunks/5436.fdf6fa8ae3e9588d.js +64 -0
  65. package/apps/pwa/out/_next/static/chunks/5542.efbf14f16baed4dd.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/5622.94893f5aea7185d0.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/5644.8f401a25601d05fc.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/5658.35f70eb9871a54bf.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/5721.7bdc0e904c76ab51.js +8 -0
  70. package/apps/pwa/out/_next/static/chunks/5790-eef0ca20884a6bdf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/5802.ef0d8598ca68fe84.js +62 -0
  72. package/apps/pwa/out/_next/static/chunks/5890.fb974729788dcb71.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/5924.5ef25cddd36d5dbf.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/5979-e59c099ebf8d1f35.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/6020.714cfeada2315e2e.js +94 -0
  76. package/apps/pwa/out/_next/static/chunks/6027-dfe0349d8c50f698.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/{6745-5a63492933959dd9.js → 6051-0ae955eedb10adb9.js} +3 -3
  78. package/apps/pwa/out/_next/static/chunks/6178.bc4730c86cf74536.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/{2283.edd9627b7bdfb546.js → 6257.c0037b8949239f5e.js} +2 -2
  80. package/apps/pwa/out/_next/static/chunks/6273.d808b0dcb3c2b155.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/6292.6ef1df64fa6a4c8a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/647-af8dd920ba46533c.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/6541-170dc5c59e135c0c.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/6624.981ef7caeb680560.js +32 -0
  85. package/apps/pwa/out/_next/static/chunks/6742-a1ad4a94ba7bc37a.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/{6819.be5c9c4d67f22922.js → 6749.0744ce6eb08d40e9.js} +16 -16
  87. package/apps/pwa/out/_next/static/chunks/6836-493dc55c39187949.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/6856-53ac421c06039a18.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/6908.06008c67f6380ea9.js +148 -0
  90. package/apps/pwa/out/_next/static/chunks/6915.096931864206f522.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/6925.5e181fb7dcf90193.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/6cc1d1d6.9a98dd4622f1afc6.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/7203-800e11037adc41eb.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/7219-15bdd9a80661af8e.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/7279.6170f591105936bb.js +93 -0
  96. package/apps/pwa/out/_next/static/chunks/7349-373165c7fbdcc3df.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/7514-a533ba0ca1cae4f4.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/7515.1ad6efe157fa4448.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/7521.d919cc1fab80c1d9.js +24 -0
  100. package/apps/pwa/out/_next/static/chunks/7538.90e97bbdf9178cff.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/7611.85f045bf232053ac.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/7825-39203196a12579b2.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/7869.5e1a9a12fd70d6c7.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/8086.1ab9103c41be884f.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/8099.1cccf31c1baa7e1c.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/8179-e61fffbdb70f1152.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/8265.bdeb991b34db6a06.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/{2002-a43a3f380df873ec.js → 8272-025653884f9645d2.js} +1 -1
  109. package/apps/pwa/out/_next/static/chunks/{7867.e8725745ddd1d940.js → 8357.1aaa713b052a78b1.js} +4 -4
  110. package/apps/pwa/out/_next/static/chunks/8638-e42615f2166165b5.js +1 -0
  111. package/apps/pwa/out/_next/static/chunks/8639-92c11d069792429e.js +1 -0
  112. package/apps/pwa/out/_next/static/chunks/8691.54e22831d5e8ccf8.js +1 -0
  113. package/apps/pwa/out/_next/static/chunks/8d173e38.18a2f7b4e1848bdb.js +1 -0
  114. package/apps/pwa/out/_next/static/chunks/9069.657590a84afb8fac.js +41 -0
  115. package/apps/pwa/out/_next/static/chunks/9125-ae588d88a0293b93.js +1 -0
  116. package/apps/pwa/out/_next/static/chunks/{4313.76f232c304a15bde.js → 9175.2de5209a5ce6f68a.js} +1 -1
  117. package/apps/pwa/out/_next/static/chunks/9184.45afbf44fec87574.js +1 -0
  118. package/apps/pwa/out/_next/static/chunks/{8c58d09b.e8724a299658d86f.js → 9189505a.980427ac048c8c60.js} +1 -1
  119. package/apps/pwa/out/_next/static/chunks/9311.f9f641a46a508611.js +1 -0
  120. package/apps/pwa/out/_next/static/chunks/9371-a6fee8378ebfa3ee.js +1 -0
  121. package/apps/pwa/out/_next/static/chunks/9421-3aa9ff4fa7f63d8a.js +1 -0
  122. package/apps/pwa/out/_next/static/chunks/9992.07ce73c10bd5e6a1.js +79 -0
  123. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-76394dc618d322c8.js +1 -0
  124. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1b948637c37b3abd.js +1 -0
  125. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-c63400f1bcf3e47c.js +1 -0
  126. package/apps/pwa/out/_next/static/chunks/app/board/page-95d0957267819586.js +1 -0
  127. package/apps/pwa/out/_next/static/chunks/app/botlab/page-ba957ff875a026a0.js +1 -0
  128. package/apps/pwa/out/_next/static/chunks/app/bots/page-bb6fd0714879cb87.js +1 -0
  129. package/apps/pwa/out/_next/static/chunks/app/ceo/page-8f9038520d2acbef.js +1 -0
  130. package/apps/pwa/out/_next/static/chunks/app/chat/page-d4b48397e05fce2c.js +1 -0
  131. package/apps/pwa/out/_next/static/chunks/app/consult/page-72d5744245dc0c27.js +1 -0
  132. package/apps/pwa/out/_next/static/chunks/app/control/page-37625dc8300fa43a.js +1 -0
  133. package/apps/pwa/out/_next/static/chunks/app/decisions/page-fbef9d160272dbc1.js +1 -0
  134. package/apps/pwa/out/_next/static/chunks/app/design-check/page-58424168df84928e.js +1 -0
  135. package/apps/pwa/out/_next/static/chunks/app/editor/page-72c12ba430ac57c0.js +1 -0
  136. package/apps/pwa/out/_next/static/chunks/app/exec/page-d53f3e9ed806d7e0.js +1 -0
  137. package/apps/pwa/out/_next/static/chunks/app/field/page-4d2fbd45feee0829.js +1 -0
  138. package/apps/pwa/out/_next/static/chunks/app/healthz/route-76394dc618d322c8.js +1 -0
  139. package/apps/pwa/out/_next/static/chunks/app/inside/page-8783dc0ed7590a2c.js +1 -0
  140. package/apps/pwa/out/_next/static/chunks/app/intake/page-3706cb390606e930.js +1 -0
  141. package/apps/pwa/out/_next/static/chunks/app/layout-9b0a86598e0111fd.js +1 -0
  142. package/apps/pwa/out/_next/static/chunks/app/live/{page-3337c6a1328c2674.js → page-4edb8df90f4537ce.js} +1 -1
  143. package/apps/pwa/out/_next/static/chunks/app/live-run/page-83d27757e2b8060a.js +1 -0
  144. package/apps/pwa/out/_next/static/chunks/app/loops/page-47398b9ba1cc5e1b.js +1 -0
  145. package/apps/pwa/out/_next/static/chunks/app/market/page-fb86f55cfe2e681d.js +1 -0
  146. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e22345fc8fd47b1b.js +1 -0
  147. package/apps/pwa/out/_next/static/chunks/app/morning/{page-1e5da297557d2354.js → page-fca2e59a81744d9e.js} +1 -1
  148. package/apps/pwa/out/_next/static/chunks/app/not-found-6e96bda63100b31e.js +1 -0
  149. package/apps/pwa/out/_next/static/chunks/app/observatory/page-4d17ad8e0db8cea4.js +1 -0
  150. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-121f92766c33beaa.js +1 -0
  151. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-43f14212c545f92d.js +1 -0
  152. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ccec2023e7a974ec.js +1 -0
  153. package/apps/pwa/out/_next/static/chunks/app/outputs/page-b04f33f548b89a94.js +1 -0
  154. package/apps/pwa/out/_next/static/chunks/app/page-c8792823c0917021.js +1 -0
  155. package/apps/pwa/out/_next/static/chunks/app/reflection/page-8c0e32782ceef4cb.js +1 -0
  156. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-94bd3922eb230ce8.js +1 -0
  157. package/apps/pwa/out/_next/static/chunks/app/sessions/page-bca565887bafa03c.js +1 -0
  158. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-ac1dffafe649fc6c.js +1 -0
  159. package/apps/pwa/out/_next/static/chunks/app/settings/page-52374edb5e23e455.js +1 -0
  160. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-ad4ebca120d43ca7.js +1 -0
  161. package/apps/pwa/out/_next/static/chunks/app/setup/layout-76394dc618d322c8.js +1 -0
  162. package/apps/pwa/out/_next/static/chunks/app/setup/page-718b89ce4b4ae4e4.js +1 -0
  163. package/apps/pwa/out/_next/static/chunks/app/share/page-204eca33eeeaddec.js +1 -0
  164. package/apps/pwa/out/_next/static/chunks/app/showroom/page-e58ba9bcdb9ac0f9.js +1 -0
  165. package/apps/pwa/out/_next/static/chunks/app/tasks/page-98f6dd50bafd7734.js +1 -0
  166. package/apps/pwa/out/_next/static/chunks/app/term/{page-ac2c1e6169cfce57.js → page-139d722de1dcd6c1.js} +1 -1
  167. package/apps/pwa/out/_next/static/chunks/app/today/page-5a4bdd72bff27057.js +1 -0
  168. package/apps/pwa/out/_next/static/chunks/app/trace/page-6f6ab17d44f984ce.js +1 -0
  169. package/apps/pwa/out/_next/static/chunks/app/vault/page-aabb7bd15fb72da9.js +1 -0
  170. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-e4322319164a4436.js +1 -0
  171. package/apps/pwa/out/_next/static/chunks/app/workflows/page-e9efc0c8f32e2371.js +1 -0
  172. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-c1c80e345fa9d6f5.js → page-ccde99f07aaf71f7.js} +1 -1
  173. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-80464aced9614390.js +1 -0
  174. package/apps/pwa/out/_next/static/chunks/{d9815f84.4918f682a45a8502.js → b84cad00.594027ebe0531fe2.js} +3 -3
  175. package/apps/pwa/out/_next/static/chunks/c696986b.5dc8bb1143af16d1.js +1 -0
  176. package/apps/pwa/out/_next/static/chunks/{709a918c.4fd3b4e2a32cb4e3.js → ca3702f8.2913182e73889fa2.js} +1 -1
  177. package/apps/pwa/out/_next/static/chunks/d5d3cefc-373c4eb722ed848b.js +1 -0
  178. package/apps/pwa/out/_next/static/chunks/{5f6af561-f90b7a67f4f75d13.js → d8de0de2-dd0f4ac8e5ab1832.js} +1 -1
  179. package/apps/pwa/out/_next/static/chunks/framework-002b472a66935d31.js +1 -0
  180. package/apps/pwa/out/_next/static/chunks/main-49b88d47292c0476.js +1 -0
  181. package/apps/pwa/out/_next/static/chunks/main-app-fff2549cb10a133f.js +1 -0
  182. package/apps/pwa/out/_next/static/chunks/pages/_app-32abbbba3f3b76b7.js +1 -0
  183. package/apps/pwa/out/_next/static/chunks/pages/_error-9d9cfef60c60dd7a.js +1 -0
  184. package/apps/pwa/out/_next/static/chunks/webpack-cc7f483fa294034a.js +1 -0
  185. package/apps/pwa/out/approvals/index.html +1 -1
  186. package/apps/pwa/out/approvals/index.txt +22 -22
  187. package/apps/pwa/out/autopilot/index.html +1 -1
  188. package/apps/pwa/out/autopilot/index.txt +22 -22
  189. package/apps/pwa/out/board/index.html +1 -1
  190. package/apps/pwa/out/board/index.txt +22 -22
  191. package/apps/pwa/out/botlab/index.html +1 -1
  192. package/apps/pwa/out/botlab/index.txt +21 -21
  193. package/apps/pwa/out/bots/index.html +1 -1
  194. package/apps/pwa/out/bots/index.txt +21 -21
  195. package/apps/pwa/out/ceo/index.html +1 -1
  196. package/apps/pwa/out/ceo/index.txt +22 -22
  197. package/apps/pwa/out/chat/index.html +1 -1
  198. package/apps/pwa/out/chat/index.txt +21 -21
  199. package/apps/pwa/out/consult/index.html +1 -1
  200. package/apps/pwa/out/consult/index.txt +22 -22
  201. package/apps/pwa/out/control/index.html +1 -1
  202. package/apps/pwa/out/control/index.txt +22 -22
  203. package/apps/pwa/out/decisions/index.html +1 -1
  204. package/apps/pwa/out/decisions/index.txt +21 -21
  205. package/apps/pwa/out/design-check/index.html +1 -1
  206. package/apps/pwa/out/design-check/index.txt +22 -22
  207. package/apps/pwa/out/editor/index.html +1 -1
  208. package/apps/pwa/out/editor/index.txt +22 -22
  209. package/apps/pwa/out/exec/index.html +1 -1
  210. package/apps/pwa/out/exec/index.txt +22 -22
  211. package/apps/pwa/out/field/index.html +1 -1
  212. package/apps/pwa/out/field/index.txt +22 -22
  213. package/apps/pwa/out/healthz +1 -1
  214. package/apps/pwa/out/index.html +1 -1
  215. package/apps/pwa/out/index.txt +21 -21
  216. package/apps/pwa/out/inside/index.html +1 -1
  217. package/apps/pwa/out/inside/index.txt +21 -21
  218. package/apps/pwa/out/intake/index.html +1 -1
  219. package/apps/pwa/out/intake/index.txt +22 -22
  220. package/apps/pwa/out/live/index.html +1 -1
  221. package/apps/pwa/out/live/index.txt +22 -22
  222. package/apps/pwa/out/live-run/index.html +1 -1
  223. package/apps/pwa/out/live-run/index.txt +22 -22
  224. package/apps/pwa/out/loops/index.html +1 -1
  225. package/apps/pwa/out/loops/index.txt +21 -21
  226. package/apps/pwa/out/market/index.html +1 -1
  227. package/apps/pwa/out/market/index.txt +21 -21
  228. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  229. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  230. package/apps/pwa/out/morning/index.html +1 -1
  231. package/apps/pwa/out/morning/index.txt +22 -22
  232. package/apps/pwa/out/observatory/index.html +1 -1
  233. package/apps/pwa/out/observatory/index.txt +22 -22
  234. package/apps/pwa/out/ops/checklist/index.html +1 -1
  235. package/apps/pwa/out/ops/checklist/index.txt +21 -21
  236. package/apps/pwa/out/ops/release/index.html +1 -1
  237. package/apps/pwa/out/ops/release/index.txt +21 -21
  238. package/apps/pwa/out/ops/seats/index.html +1 -1
  239. package/apps/pwa/out/ops/seats/index.txt +21 -21
  240. package/apps/pwa/out/outputs/index.html +1 -1
  241. package/apps/pwa/out/outputs/index.txt +21 -21
  242. package/apps/pwa/out/reflection/index.html +1 -1
  243. package/apps/pwa/out/reflection/index.txt +22 -22
  244. package/apps/pwa/out/scheduler/index.html +1 -1
  245. package/apps/pwa/out/scheduler/index.txt +22 -22
  246. package/apps/pwa/out/sessions/index.html +1 -1
  247. package/apps/pwa/out/sessions/index.txt +21 -21
  248. package/apps/pwa/out/settings/devices/index.html +1 -1
  249. package/apps/pwa/out/settings/devices/index.txt +22 -22
  250. package/apps/pwa/out/settings/index.html +1 -1
  251. package/apps/pwa/out/settings/index.txt +22 -22
  252. package/apps/pwa/out/setup/done/index.html +1 -1
  253. package/apps/pwa/out/setup/done/index.txt +22 -22
  254. package/apps/pwa/out/setup/index.html +1 -1
  255. package/apps/pwa/out/setup/index.txt +22 -22
  256. package/apps/pwa/out/share/index.html +1 -1
  257. package/apps/pwa/out/share/index.txt +22 -22
  258. package/apps/pwa/out/showroom/index.html +1 -1
  259. package/apps/pwa/out/showroom/index.txt +21 -21
  260. package/apps/pwa/out/tasks/index.html +1 -1
  261. package/apps/pwa/out/tasks/index.txt +22 -22
  262. package/apps/pwa/out/term/index.html +1 -1
  263. package/apps/pwa/out/term/index.txt +22 -22
  264. package/apps/pwa/out/today/index.html +1 -1
  265. package/apps/pwa/out/today/index.txt +22 -22
  266. package/apps/pwa/out/trace/index.html +1 -1
  267. package/apps/pwa/out/trace/index.txt +22 -22
  268. package/apps/pwa/out/vault/index.html +1 -1
  269. package/apps/pwa/out/vault/index.txt +22 -22
  270. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  271. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  272. package/apps/pwa/out/workflows/index.html +1 -1
  273. package/apps/pwa/out/workflows/index.txt +22 -22
  274. package/apps/pwa/out/workspace/index.html +1 -1
  275. package/apps/pwa/out/workspace/index.txt +22 -22
  276. package/apps/pwa/out/worktrees/index.html +1 -1
  277. package/apps/pwa/out/worktrees/index.txt +22 -22
  278. package/graphs/demo/persona-marketer/fixtures/marketer-scenario.yaml +10 -0
  279. package/graphs/demo/persona-marketer/persona-marketer.yaml +25 -0
  280. package/graphs/demo/persona-marketer/recipes.yaml +19 -0
  281. package/graphs/release/recipes.yaml +9 -0
  282. package/graphs/release/release-loop.yaml +26 -18
  283. package/graphs/video/explainer-line.yaml +4 -1
  284. package/package.json +1 -1
  285. package/scripts/ask-op-inbox-once.ts +17 -0
  286. package/scripts/brand/check.ts +13 -4
  287. package/scripts/dead-code/report.ts +169 -0
  288. package/scripts/gate-0226-accepted-remaining-90.txt +91 -0
  289. package/scripts/gate-0226-failure-census.ts +39 -0
  290. package/scripts/intake-collect-telegram-saved-stage.ts +48 -0
  291. package/scripts/lib/morning-report-options.ts +3 -4
  292. package/scripts/lib/tui-regress-evidence.ts +56 -0
  293. package/scripts/morning-report.ts +5 -1
  294. package/scripts/provision-export-redactions.ts +15 -0
  295. package/scripts/public-copy-lint.ts +122 -0
  296. package/scripts/public-export.ts +77 -2
  297. package/scripts/ratchets/next-tui-entry-budget.ts +124 -0
  298. package/scripts/ratchets/train-side-invariants.sh +7 -1
  299. package/scripts/release-loop/cut-branch.ts +61 -12
  300. package/scripts/release-loop/export-check-node.ts +21 -3
  301. package/scripts/release-loop/gate-node.ts +527 -60
  302. package/scripts/release-loop/gate-partial.ts +82 -3
  303. package/scripts/release-loop/gate-prewarm-plan.ts +35 -0
  304. package/scripts/release-loop/gate-radius-live.ts +4 -2
  305. package/scripts/release-loop/gate-shard-dispatch.ts +128 -0
  306. package/scripts/release-loop/gate-shard-receipts.ts +89 -0
  307. package/scripts/release-loop/gate-wall.ts +5 -5
  308. package/scripts/release-loop/preflight.ts +6 -2
  309. package/scripts/release-loop/resume-cap.ts +2 -1
  310. package/scripts/release-loop/resume-release.ts +317 -13
  311. package/scripts/release-loop/resume.ts +61 -5
  312. package/scripts/release-loop/traffic-node.ts +12 -4
  313. package/scripts/release-loop/traffic-transition.ts +53 -13
  314. package/scripts/release-loop/version-node.ts +35 -5
  315. package/scripts/release-story/draft.ts +1 -1
  316. package/scripts/research-bench/README.md +53 -0
  317. package/scripts/research-bench/questions.json +66 -0
  318. package/scripts/research-bench/rubric.md +33 -0
  319. package/scripts/research-bench/run.ts +523 -0
  320. package/scripts/tui-regress.ts +85 -10
  321. package/scripts/ux-sim.ts +1 -1
  322. package/scripts/video-pipeline.ts +2 -2
  323. package/src/acp/core-turn-bridge.ts +14 -1
  324. package/src/agent/autonomous-tools.ts +10 -3
  325. package/src/agent/monad-agent-turn.ts +4 -1
  326. package/src/agent/registry.ts +26 -2
  327. package/src/agent/run-context.ts +1 -1
  328. package/src/agent/shared-app-tools.ts +1 -1
  329. package/src/agent/surface-ux/build.ts +12 -3
  330. package/src/agent/surface-ux/types.ts +1 -1
  331. package/src/agent/task-notification.ts +78 -7
  332. package/src/agent/types.ts +3 -0
  333. package/src/agent-mission/driver.ts +29 -3
  334. package/src/agent-mission/mission-cli.ts +2 -1
  335. package/src/agent-substrate/review-context-pack.ts +365 -0
  336. package/src/agent-substrate/review-observation.ts +10 -0
  337. package/src/agent-substrate/self-review-cli.ts +419 -42
  338. package/src/ask-user-question/index.ts +6 -0
  339. package/src/ask-user-question/inline-resolver.ts +49 -0
  340. package/src/auto-research/question-ladder-contract.ts +156 -0
  341. package/src/auto-research/research-turn.ts +467 -0
  342. package/src/autopilot/agent-loop.ts +1 -1
  343. package/src/autopilot/discovery/doc-lint.ts +1 -1
  344. package/src/autopilot/pty-control-loop.ts +8 -1
  345. package/src/boot/daemon-tools/index.ts +4 -3
  346. package/src/boot/daemon-tools/self-implement.ts +270 -2
  347. package/src/boot/daemon-tools/types.ts +2 -0
  348. package/src/bots/screen-probe.ts +1 -1
  349. package/src/budget/llm-cost.ts +44 -5
  350. package/src/budget/unified-usage.ts +17 -1
  351. package/src/capture/tui-self-report.ts +24 -2
  352. package/src/chat/index.ts +34 -7
  353. package/src/chat/tool-render/dispatch.ts +8 -1
  354. package/src/cli/agent-cli.ts +43 -8
  355. package/src/cli/guardian-cli.ts +3 -1
  356. package/src/cli/intake-cli.ts +70 -13
  357. package/src/cli/lesson-cli.ts +86 -10
  358. package/src/cli/log-event-names.ts +1 -0
  359. package/src/cli/pod-cli.ts +17 -2
  360. package/src/cli/pr-land-overlap-evidence.ts +38 -5
  361. package/src/cli/pr-land-refusal.ts +1 -1
  362. package/src/cli/pty-takeover-cli.ts +125 -12
  363. package/src/cli/release-checklist-read-cli.ts +5 -2
  364. package/src/cli/release-cli.ts +99 -15
  365. package/src/cli/repo-cli.ts +2 -1
  366. package/src/cli/review-carry.ts +77 -0
  367. package/src/cli/self-update.ts +1 -1
  368. package/src/cli/tasks-card-show.ts +31 -1
  369. package/src/cli/tasks-cli.ts +98 -32
  370. package/src/cli/tasks-read-cli.ts +13 -5
  371. package/src/context-bus/context-now.ts +86 -2
  372. package/src/control-plane/ledger.ts +21 -4
  373. package/src/core-turn/run-core-turn.ts +51 -3
  374. package/src/core-turn/run-goal-loop.ts +152 -8
  375. package/src/core-turn/types.ts +12 -0
  376. package/src/dashboard/chat/acp-chat.ts +2 -2
  377. package/src/dashboard/classic-sigquit.ts +16 -0
  378. package/src/dashboard/index.ts +339 -44
  379. package/src/dashboard/input/chat-main-interaction-opts.ts +3 -0
  380. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +7 -2
  381. package/src/dashboard/input/chat-main-slash-command.ts +7 -0
  382. package/src/dashboard/input/dashboard-plain-turn-stream.ts +4 -0
  383. package/src/dashboard/input/dashboard-priority-key-route.ts +21 -5
  384. package/src/dashboard/input/external-input-host.ts +2 -0
  385. package/src/dashboard/input/terminal-modal-router.ts +8 -159
  386. package/src/dashboard/side-basic-runtime.ts +61 -0
  387. package/src/dashboard/sidetab-runtime.ts +228 -0
  388. package/src/dashboard/slash-runtime/ask-clarification.ts +7 -1
  389. package/src/dashboard/slash-runtime/dashboard-handlers.ts +22 -8
  390. package/src/dashboard/slash-runtime/help-from-registry.ts +2 -2
  391. package/src/dashboard/term-slash-runtime.ts +18 -18
  392. package/src/dashboard/turn-preamble.ts +30 -4
  393. package/src/domains/guardian-tick.ts +9 -0
  394. package/src/domains/knowledge.ts +1 -1
  395. package/src/esc-abort-gate.ts +33 -1
  396. package/src/execution-loop/launch-gate.ts +206 -1
  397. package/src/git-fs/watcher.ts +27 -22
  398. package/src/git-fs/worktree.ts +19 -3
  399. package/src/graph-runner/graph-cli.ts +6 -4
  400. package/src/graph-runner/graph-grow.ts +37 -9
  401. package/src/graph-runner/runner.ts +48 -11
  402. package/src/graph-wizard/generate.ts +16 -7
  403. package/src/graph-wizard/mission-compose.ts +201 -0
  404. package/src/grok/agent-search.ts +8 -2
  405. package/src/grok/credential.ts +112 -36
  406. package/src/grounding/ref-index.ts +11 -0
  407. package/src/grounding/ref-vector.ts +61 -0
  408. package/src/grounding/sources-cli.ts +2 -2
  409. package/src/grounding/sources.ts +15 -1
  410. package/src/harness/harness-cli-command.ts +38 -9
  411. package/src/harness/harness-pod-dispatch.ts +7 -1
  412. package/src/hq/fence-wrapper.ts +1 -1
  413. package/src/hq/transfer.ts +1 -1
  414. package/src/index.ts +60 -16
  415. package/src/input/external-input-queue.ts +10 -0
  416. package/src/intake-plane/collect-telegram-saved.ts +90 -6
  417. package/src/intake-plane/intake-to-tasks.ts +246 -3
  418. package/src/interactive-terminal-modal.ts +3 -0
  419. package/src/keybindings.ts +17 -22
  420. package/src/lessons/friction-ledger.ts +154 -0
  421. package/src/live-supply/collector.ts +149 -0
  422. package/src/live-supply/probes.ts +52 -0
  423. package/src/live-supply/snapshot.ts +40 -0
  424. package/src/llm/provider-credentials.ts +1 -1
  425. package/src/llm-tool-schema.ts +20 -0
  426. package/src/llm.ts +486 -61
  427. package/src/loops/orchestrator/inbox-once.ts +28 -0
  428. package/src/loops/orchestrator/inbox.ts +130 -1
  429. package/src/loops/orchestrator/tick.ts +14 -3
  430. package/src/maturity/feature-maturity.ts +1 -0
  431. package/src/nexus/api/channel-ask-op.ts +96 -0
  432. package/src/nexus/api/discord-trigger-bot.ts +28 -1
  433. package/src/nexus/api/harness-api.ts +2 -1
  434. package/src/nexus/api/telegram-trigger-bot.ts +24 -0
  435. package/src/nexus/api/terminals.ts +28 -6
  436. package/src/patents/ledger.ts +18 -1
  437. package/src/preview/terminal.ts +4 -2
  438. package/src/prompt-cache/anthropic.ts +2 -0
  439. package/src/prompt-enhance/enhance.ts +5 -0
  440. package/src/pty/snapshot.py +132 -0
  441. package/src/pty-shell/pty-control-ipc.ts +29 -2
  442. package/src/pty-shell/pty-manifest.ts +75 -5
  443. package/src/pty-shell/registry.ts +2 -0
  444. package/src/pty-shell/tui-run-state.ts +80 -3
  445. package/src/release-loop/gate-host-reservation.ts +13 -4
  446. package/src/release-loop/gate-shard-ledger.ts +68 -0
  447. package/src/release-loop/gate-shards.ts +61 -0
  448. package/src/release-loop/light-rc.ts +7 -4
  449. package/src/release-loop/merged-pr-checklist.ts +25 -2
  450. package/src/release-loop/rehearsal-shard-plan.ts +6 -1
  451. package/src/release-loop/rehearsal.ts +28 -1
  452. package/src/repl/index.ts +10 -0
  453. package/src/research/axis-fanout.ts +133 -0
  454. package/src/research/edge-decision.ts +197 -0
  455. package/src/research/gap-followup.ts +249 -0
  456. package/src/research/lenses/counter-evidence.ts +100 -0
  457. package/src/research/lenses/working-backwards.ts +89 -0
  458. package/src/research/output-router.ts +127 -0
  459. package/src/research/plugin-discovery.ts +242 -0
  460. package/src/research-bridge/format.ts +16 -1
  461. package/src/research-bridge/index.ts +1 -0
  462. package/src/research-bridge/invoke.ts +45 -7
  463. package/src/research-bridge/types.ts +2 -0
  464. package/src/self-dev/ask-launch-flow.ts +34 -106
  465. package/src/self-dev/decompose.ts +45 -9
  466. package/src/self-dev/dev-cli.ts +2 -0
  467. package/src/self-dev/dev-pipeline.ts +8 -4
  468. package/src/self-dev/draft-residue.ts +221 -2
  469. package/src/self-dev/draft-sweep.ts +29 -16
  470. package/src/self-dev/graph-journey-route.ts +10 -2
  471. package/src/self-dev/launch-preflight-text.ts +2 -2
  472. package/src/self-dev/launch-preflight.ts +38 -7
  473. package/src/self-dev/orchestrate-cli.ts +1 -0
  474. package/src/self-dev/orchestrate.ts +1 -0
  475. package/src/self-dev/run-supervisor.ts +41 -7
  476. package/src/self-dev/shard-run.ts +260 -3
  477. package/src/self-implement/comment-only-source.ts +54 -0
  478. package/src/self-implement/device-first.ts +8 -2
  479. package/src/self-implement/doctor-visit.ts +100 -57
  480. package/src/self-implement/gate-artifacts.ts +5 -2
  481. package/src/self-implement/gate-baseline.ts +119 -13
  482. package/src/self-implement/gate-cli.ts +466 -93
  483. package/src/self-implement/gate-failure-signature.ts +44 -0
  484. package/src/self-implement/gate-receipt-registry.ts +180 -0
  485. package/src/self-implement/gate-remote.ts +147 -22
  486. package/src/self-implement/goal-author-clarification.ts +178 -24
  487. package/src/self-implement/goal-author-decompose.ts +24 -15
  488. package/src/self-implement/goal-author.ts +82 -7
  489. package/src/self-implement/goal-doc/section.ts +22 -0
  490. package/src/self-implement/goal-supervisor-fill.ts +38 -4
  491. package/src/self-implement/host-regate.ts +120 -45
  492. package/src/self-implement/impl-findings-feedback.ts +188 -0
  493. package/src/self-implement/knowhow-pack.ts +301 -9
  494. package/src/self-implement/l8-merge-queue.ts +198 -32
  495. package/src/self-implement/off-diff-evidence.ts +147 -0
  496. package/src/self-implement/orchestrator.ts +137 -16
  497. package/src/self-implement/pod-inplace-worktree.ts +82 -0
  498. package/src/self-implement/pod-warm-pool-profile.ts +2 -2
  499. package/src/self-implement/premise-review.ts +7 -5
  500. package/src/self-implement/run-ledger.ts +304 -8
  501. package/src/self-implement/run-stop.ts +12 -0
  502. package/src/self-implement/seams.ts +39 -13
  503. package/src/self-implement/self-implement-cli.ts +30 -4
  504. package/src/session/channel-ask-op-envelope.ts +36 -0
  505. package/src/session/channel-op-reply-route.ts +200 -0
  506. package/src/session/chat.ts +92 -15
  507. package/src/session/index.ts +2 -0
  508. package/src/session-runtime/index.ts +9 -22
  509. package/src/session-runtime/provider-circuit.ts +140 -0
  510. package/src/shell-runner/file-engine.ts +1 -0
  511. package/src/shell-runner/types.ts +4 -0
  512. package/src/sidetab-core/model.ts +176 -0
  513. package/src/skills/runner.ts +7 -3
  514. package/src/skills/tools/dev-harness.ts +2 -2
  515. package/src/skills/tools/schedule-wakeup.ts +24 -7
  516. package/src/skills/tools/shell.ts +51 -5
  517. package/src/status/bar.ts +3 -3
  518. package/src/steward/directive-audit.ts +354 -0
  519. package/src/storage/file-lock.ts +17 -4
  520. package/src/surface/child-process.ts +31 -0
  521. package/src/surface/tui-stderr-owner.ts +48 -0
  522. package/src/task-agent/adopt-pr.ts +63 -2
  523. package/src/task-agent/cover.ts +30 -12
  524. package/src/task-agent/evidence-command-normalize.ts +42 -0
  525. package/src/task-agent/export-redactions-provision.ts +43 -0
  526. package/src/task-agent/judge.ts +56 -0
  527. package/src/task-agent/parent-host.ts +59 -12
  528. package/src/task-agent/postland-card.ts +35 -2
  529. package/src/task-agent/rejudge-on-head.ts +46 -2
  530. package/src/task-agent/repair-cycle.ts +68 -9
  531. package/src/task-agent/repair-worker.ts +37 -1
  532. package/src/task-agent/task-hand.ts +226 -33
  533. package/src/task-agent/tier-split.ts +142 -24
  534. package/src/task-orchestrator/surfaces/pod-command-job.ts +330 -2
  535. package/src/task-orchestrator/surfaces/pod-config-snapshot.ts +123 -0
  536. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +57 -5
  537. package/src/task-orchestrator/surfaces/pod-mirror-seed.ts +11 -8
  538. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -4
  539. package/src/task-orchestrator/surfaces/pod-si-warm-protocol.ts +1 -1
  540. package/src/task-orchestrator/surfaces/pod-source-receive.ts +74 -6
  541. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +2 -1
  542. package/src/task-orchestrator/surfaces/pod-warm-worker-provider.ts +56 -15
  543. package/src/task-orchestrator/surfaces/self-implement-pod.ts +310 -79
  544. package/src/task-orchestrator/surfaces/self-implement.ts +33 -2
  545. package/src/terminal/session-registry.ts +1 -0
  546. package/src/terminal/terminal-modal-router.ts +156 -0
  547. package/src/tool-hints/gate.ts +6 -4
  548. package/src/tool-hints/signals.ts +33 -1
  549. package/src/tool-runtime/skill-exec-runtime.ts +95 -5
  550. package/src/tui/chat/background-job-status.ts +8 -2
  551. package/src/tui/entry.ts +483 -32
  552. package/src/tui/inline-choice.ts +188 -0
  553. package/src/tui/sidetab/attach.ts +304 -0
  554. package/src/tui/sidetab/list.ts +2 -0
  555. package/src/tui/sidetab/nl-tool.ts +136 -0
  556. package/src/tui/slash/resume-command.ts +23 -0
  557. package/src/tui/slash/resume.ts +3 -23
  558. package/src/tui/slash/term-next.ts +2 -1
  559. package/src/tui/slash/term.ts +12 -1
  560. package/src/tui-client/dashboard-session.ts +7 -1
  561. package/src/tui-client/local-chat-turn.ts +29 -8
  562. package/src/user-config.ts +42 -3
  563. package/src/ux-sim/classic-tui-seat-scenarios.ts +1 -7
  564. package/src/ux-sim/fake-land-queue.ts +88 -0
  565. package/src/ux-sim/fake-llm.ts +11 -5
  566. package/src/ux-sim/gate-shard-host-reroute.ts +82 -0
  567. package/src/ux-sim/goal-interim-trace.ts +24 -0
  568. package/src/ux-sim/next-tui-seat-scenarios.ts +61 -14
  569. package/src/ux-sim/task-orchestrator-surfaces-self-implement-pod-device.ts +4 -1
  570. package/src/version/packed-revision.json +1 -1
  571. package/src/video-pipeline/recipes/explainer.ts +54 -2
  572. package/src/web-search/grok.ts +32 -6
  573. package/src/web-search/index.ts +8 -7
  574. package/src/workflow-runtime/node-catalog.ts +53 -4
  575. package/apps/pwa/out/_next/static/chunks/1041-5841aae848fb6403.js +0 -1
  576. package/apps/pwa/out/_next/static/chunks/1087.3177e54f672dc526.js +0 -1
  577. package/apps/pwa/out/_next/static/chunks/1105.efe1e13503ac7a33.js +0 -1
  578. package/apps/pwa/out/_next/static/chunks/1224.2ceca6c180d5130b.js +0 -62
  579. package/apps/pwa/out/_next/static/chunks/1417-1768233ac92f8a5d.js +0 -1
  580. package/apps/pwa/out/_next/static/chunks/1612.81bbc19af06af16c.js +0 -1
  581. package/apps/pwa/out/_next/static/chunks/1766.728d42053070bb2d.js +0 -1
  582. package/apps/pwa/out/_next/static/chunks/1773.d38e945481e04125.js +0 -1
  583. package/apps/pwa/out/_next/static/chunks/1974-c12f9600da3d3c26.js +0 -1
  584. package/apps/pwa/out/_next/static/chunks/1ed5e8af.cf424815c7c0efad.js +0 -1
  585. package/apps/pwa/out/_next/static/chunks/2061.7232dc9dc058417f.js +0 -1
  586. package/apps/pwa/out/_next/static/chunks/210.90b2cbd64926cf34.js +0 -32
  587. package/apps/pwa/out/_next/static/chunks/244-a810d282a20f1783.js +0 -1
  588. package/apps/pwa/out/_next/static/chunks/2479-fe7e188bd9402d14.js +0 -1
  589. package/apps/pwa/out/_next/static/chunks/2514.03b60f266531a015.js +0 -148
  590. package/apps/pwa/out/_next/static/chunks/2526-a657538869888604.js +0 -1
  591. package/apps/pwa/out/_next/static/chunks/2533-6f0341ff075809e9.js +0 -1
  592. package/apps/pwa/out/_next/static/chunks/2676.e7994c695ef0f30d.js +0 -1
  593. package/apps/pwa/out/_next/static/chunks/2734.3ffdf54254fb6f99.js +0 -27
  594. package/apps/pwa/out/_next/static/chunks/2770-23252bf3a37880b9.js +0 -1
  595. package/apps/pwa/out/_next/static/chunks/2969.66b8b545139d1544.js +0 -1
  596. package/apps/pwa/out/_next/static/chunks/3128.a9210d08800ea6a8.js +0 -1
  597. package/apps/pwa/out/_next/static/chunks/3199.6a1a9b0e6cd01dc6.js +0 -1
  598. package/apps/pwa/out/_next/static/chunks/3212.12179fd3eee8cef1.js +0 -1
  599. package/apps/pwa/out/_next/static/chunks/3760-2909ddb9279281cd.js +0 -1
  600. package/apps/pwa/out/_next/static/chunks/3825.1921b5901bf33dce.js +0 -36
  601. package/apps/pwa/out/_next/static/chunks/3994-63eb884ed0547011.js +0 -1
  602. package/apps/pwa/out/_next/static/chunks/4036.d54e55e1ebf5acdc.js +0 -90
  603. package/apps/pwa/out/_next/static/chunks/4115.5b438b99f0ae729f.js +0 -226
  604. package/apps/pwa/out/_next/static/chunks/4269.d8f9f34bb0fc6c43.js +0 -287
  605. package/apps/pwa/out/_next/static/chunks/431-4eed5bd2c5fe1719.js +0 -1
  606. package/apps/pwa/out/_next/static/chunks/4324.c99ba3e83eb1b691.js +0 -1
  607. package/apps/pwa/out/_next/static/chunks/4599-96eff3f2a9c242c1.js +0 -1
  608. package/apps/pwa/out/_next/static/chunks/4825-a9d9d4e9f5892309.js +0 -1
  609. package/apps/pwa/out/_next/static/chunks/4833-7934c592c5569f00.js +0 -1
  610. package/apps/pwa/out/_next/static/chunks/4879-68518f547a207aff.js +0 -1
  611. package/apps/pwa/out/_next/static/chunks/4896.f9b70285363b27b2.js +0 -24
  612. package/apps/pwa/out/_next/static/chunks/4937-28d437999b77c9dd.js +0 -1
  613. package/apps/pwa/out/_next/static/chunks/49aa54ab.80f39398e0ed0f30.js +0 -1
  614. package/apps/pwa/out/_next/static/chunks/5056.482687319b82638e.js +0 -104
  615. package/apps/pwa/out/_next/static/chunks/506.16b83d56a437a9ee.js +0 -1
  616. package/apps/pwa/out/_next/static/chunks/5260-20593b0a25d0afbc.js +0 -1
  617. package/apps/pwa/out/_next/static/chunks/5404.cc4e9fec26fd5925.js +0 -1
  618. package/apps/pwa/out/_next/static/chunks/576.485e83e3f3dec19a.js +0 -1
  619. package/apps/pwa/out/_next/static/chunks/5832-a6be14e663f65726.js +0 -1
  620. package/apps/pwa/out/_next/static/chunks/5854-56648a9fe646b9a0.js +0 -1
  621. package/apps/pwa/out/_next/static/chunks/5925.3b1f8b135710249b.js +0 -93
  622. package/apps/pwa/out/_next/static/chunks/5952.294fc574a8625184.js +0 -1
  623. package/apps/pwa/out/_next/static/chunks/6168.17749aa3905fa073.js +0 -74
  624. package/apps/pwa/out/_next/static/chunks/6416.5d50b8ec9986ca98.js +0 -1
  625. package/apps/pwa/out/_next/static/chunks/6758.e7fbf532953c0783.js +0 -1
  626. package/apps/pwa/out/_next/static/chunks/6795.0cb8a59730d99703.js +0 -149
  627. package/apps/pwa/out/_next/static/chunks/6877-84d47b7e85d0c41e.js +0 -1
  628. package/apps/pwa/out/_next/static/chunks/6971-fb5f9dc0a2cb5d2d.js +0 -1
  629. package/apps/pwa/out/_next/static/chunks/717.522b08685979b814.js +0 -1
  630. package/apps/pwa/out/_next/static/chunks/7191.e41eb8f455c7f089.js +0 -41
  631. package/apps/pwa/out/_next/static/chunks/7233.8e744b7513119409.js +0 -1
  632. package/apps/pwa/out/_next/static/chunks/7238-97c2226f80504bd8.js +0 -1
  633. package/apps/pwa/out/_next/static/chunks/7285.307963afcf7e7660.js +0 -24
  634. package/apps/pwa/out/_next/static/chunks/7299-fbe4b8c47a87614a.js +0 -1
  635. package/apps/pwa/out/_next/static/chunks/7314.df0d95930b0c3f50.js +0 -1
  636. package/apps/pwa/out/_next/static/chunks/7406.c0589fdf2ee66629.js +0 -1
  637. package/apps/pwa/out/_next/static/chunks/752-90e3e7daad3d8e39.js +0 -1
  638. package/apps/pwa/out/_next/static/chunks/7524-bcd3d9928c6842d9.js +0 -1
  639. package/apps/pwa/out/_next/static/chunks/7549.9a4de3bf231cfc52.js +0 -1
  640. package/apps/pwa/out/_next/static/chunks/757-4fbaf50942b8c424.js +0 -1
  641. package/apps/pwa/out/_next/static/chunks/765.76ae970ceb597b5b.js +0 -1
  642. package/apps/pwa/out/_next/static/chunks/7893-e444a1b0809cd1e4.js +0 -1
  643. package/apps/pwa/out/_next/static/chunks/7956-33748cabb3a05562.js +0 -1
  644. package/apps/pwa/out/_next/static/chunks/7c5ed21f-f286708e57cf88f7.js +0 -1
  645. package/apps/pwa/out/_next/static/chunks/8030-aa66b478b53dbfca.js +0 -1
  646. package/apps/pwa/out/_next/static/chunks/8051.c88c07dacbbc5f21.js +0 -8
  647. package/apps/pwa/out/_next/static/chunks/8080-40d781e7704d3671.js +0 -1
  648. package/apps/pwa/out/_next/static/chunks/8149.80e402d4345e8dae.js +0 -77
  649. package/apps/pwa/out/_next/static/chunks/8205.50a5268f4e12ba0b.js +0 -1
  650. package/apps/pwa/out/_next/static/chunks/8231.a128feaa6f8b6079.js +0 -1
  651. package/apps/pwa/out/_next/static/chunks/85.4ddb1dda2ea3ffa2.js +0 -286
  652. package/apps/pwa/out/_next/static/chunks/8537.26517a09e8247beb.js +0 -1
  653. package/apps/pwa/out/_next/static/chunks/8595-e9b7081ab773a61f.js +0 -1
  654. package/apps/pwa/out/_next/static/chunks/8821-30cc65a61a60951f.js +0 -1
  655. package/apps/pwa/out/_next/static/chunks/8905.b3385aadb7b133e6.js +0 -79
  656. package/apps/pwa/out/_next/static/chunks/8983.246bb3a1ac38f50f.js +0 -1
  657. package/apps/pwa/out/_next/static/chunks/9072.18342f5941a1f3ec.js +0 -1
  658. package/apps/pwa/out/_next/static/chunks/9123.9df04866b2d6e00f.js +0 -1
  659. package/apps/pwa/out/_next/static/chunks/9175-7d383df15a82e2be.js +0 -1
  660. package/apps/pwa/out/_next/static/chunks/9181-bf89c07ce5b8f599.js +0 -1
  661. package/apps/pwa/out/_next/static/chunks/920-429bbc4504b45f71.js +0 -1
  662. package/apps/pwa/out/_next/static/chunks/9207.625edf45d656151c.js +0 -1
  663. package/apps/pwa/out/_next/static/chunks/9378.d2575c6ffd6dd756.js +0 -94
  664. package/apps/pwa/out/_next/static/chunks/957.db5b5c4d32a35240.js +0 -1
  665. package/apps/pwa/out/_next/static/chunks/958.53a8dcf4729e6997.js +0 -64
  666. package/apps/pwa/out/_next/static/chunks/9647.8f0cd0898c592a29.js +0 -1
  667. package/apps/pwa/out/_next/static/chunks/9665-26abb02665704c2c.js +0 -1
  668. package/apps/pwa/out/_next/static/chunks/9703.e7d81b6faef1050a.js +0 -1
  669. package/apps/pwa/out/_next/static/chunks/9882-7a9164b3c2dacbe5.js +0 -1
  670. package/apps/pwa/out/_next/static/chunks/9998.b74e033efadecd10.js +0 -1
  671. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c604fb980a634d88.js +0 -1
  672. package/apps/pwa/out/_next/static/chunks/app/approvals/page-ec2d8a4979ca9075.js +0 -1
  673. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cb6add08ad3c7660.js +0 -1
  674. package/apps/pwa/out/_next/static/chunks/app/board/page-e2eaa321ec223fe6.js +0 -1
  675. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5b8a09c475b5f844.js +0 -1
  676. package/apps/pwa/out/_next/static/chunks/app/bots/page-91a77daedfbac191.js +0 -1
  677. package/apps/pwa/out/_next/static/chunks/app/ceo/page-f37fc5e974bda39b.js +0 -1
  678. package/apps/pwa/out/_next/static/chunks/app/chat/page-91429a0b78415a94.js +0 -1
  679. package/apps/pwa/out/_next/static/chunks/app/consult/page-b95798b58263b910.js +0 -1
  680. package/apps/pwa/out/_next/static/chunks/app/control/page-763d53e175e17aa1.js +0 -1
  681. package/apps/pwa/out/_next/static/chunks/app/decisions/page-d55023204055b12f.js +0 -1
  682. package/apps/pwa/out/_next/static/chunks/app/design-check/page-89d2140d78abc810.js +0 -1
  683. package/apps/pwa/out/_next/static/chunks/app/editor/page-82dc069abfe27f55.js +0 -1
  684. package/apps/pwa/out/_next/static/chunks/app/exec/page-25d9eee7beb552c0.js +0 -1
  685. package/apps/pwa/out/_next/static/chunks/app/field/page-770b4594c713ee97.js +0 -1
  686. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c604fb980a634d88.js +0 -1
  687. package/apps/pwa/out/_next/static/chunks/app/inside/page-ba8e0dcae4f83985.js +0 -1
  688. package/apps/pwa/out/_next/static/chunks/app/intake/page-5c112c6d7c33656e.js +0 -1
  689. package/apps/pwa/out/_next/static/chunks/app/layout-09788e1aa9a87db6.js +0 -1
  690. package/apps/pwa/out/_next/static/chunks/app/live-run/page-89acd5d68c0bc560.js +0 -1
  691. package/apps/pwa/out/_next/static/chunks/app/loops/page-5c13e2c76371d797.js +0 -1
  692. package/apps/pwa/out/_next/static/chunks/app/market/page-653a9f18642825c6.js +0 -1
  693. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-fe986fe79b107546.js +0 -1
  694. package/apps/pwa/out/_next/static/chunks/app/not-found-91ea9f5869a40943.js +0 -1
  695. package/apps/pwa/out/_next/static/chunks/app/observatory/page-7824e21b999962e2.js +0 -1
  696. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-53d00a60cc75a922.js +0 -1
  697. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-88a1edbff62eaf1d.js +0 -1
  698. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-4e931248b3f9574b.js +0 -1
  699. package/apps/pwa/out/_next/static/chunks/app/outputs/page-443364c9e4a23b31.js +0 -1
  700. package/apps/pwa/out/_next/static/chunks/app/page-b9af2ac1c4737ce8.js +0 -1
  701. package/apps/pwa/out/_next/static/chunks/app/reflection/page-25ab9c614c14129b.js +0 -1
  702. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-dbcdcfac0a395a8a.js +0 -1
  703. package/apps/pwa/out/_next/static/chunks/app/sessions/page-bf1de6645089ec58.js +0 -1
  704. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c4201761a5e839c9.js +0 -1
  705. package/apps/pwa/out/_next/static/chunks/app/settings/page-c304f469ea5d70e4.js +0 -1
  706. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8e5a238cf5e50c9c.js +0 -1
  707. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c604fb980a634d88.js +0 -1
  708. package/apps/pwa/out/_next/static/chunks/app/setup/page-32b526069ee50b3c.js +0 -1
  709. package/apps/pwa/out/_next/static/chunks/app/share/page-fb9e639c107f5f72.js +0 -1
  710. package/apps/pwa/out/_next/static/chunks/app/showroom/page-84220c776608cedc.js +0 -1
  711. package/apps/pwa/out/_next/static/chunks/app/tasks/page-e475618cbfd5ca2b.js +0 -1
  712. package/apps/pwa/out/_next/static/chunks/app/today/page-fccd3020af2f8806.js +0 -1
  713. package/apps/pwa/out/_next/static/chunks/app/trace/page-9c0d7198ff7b5a91.js +0 -1
  714. package/apps/pwa/out/_next/static/chunks/app/vault/page-d54b1ec9b9c6430d.js +0 -1
  715. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8c4bef1791d732ca.js +0 -1
  716. package/apps/pwa/out/_next/static/chunks/app/workflows/page-46fcc0b0bbc6fbfe.js +0 -1
  717. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-7dfda00afd172f5b.js +0 -1
  718. package/apps/pwa/out/_next/static/chunks/f21fd540.63ac75328926ed30.js +0 -1
  719. package/apps/pwa/out/_next/static/chunks/framework-77fdcc873519a6cd.js +0 -1
  720. package/apps/pwa/out/_next/static/chunks/main-app-d0d620d2ce235713.js +0 -1
  721. package/apps/pwa/out/_next/static/chunks/main-e0ef79c79337ba90.js +0 -1
  722. package/apps/pwa/out/_next/static/chunks/pages/_app-84b1bc71e3f9ce6a.js +0 -1
  723. package/apps/pwa/out/_next/static/chunks/pages/_error-2acb61f1363128cb.js +0 -1
  724. package/apps/pwa/out/_next/static/chunks/webpack-462ff5727ceb7824.js +0 -1
  725. package/scripts/migrate-monad-to-elanous.ts +0 -195
  726. package/scripts/rebrand-elanous.ts +0 -242
  727. /package/apps/pwa/out/_next/static/{8kITumG9GAZ1LwT9POrAR → HjeK7zONHfR7op1hKXp4k}/_ssgManifest.js +0 -0
@@ -3,7 +3,7 @@ import { spawn, spawnSync } from 'node:child_process';
3
3
  import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { hostname, tmpdir } from 'node:os';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
- import { dirname, join, resolve } from 'node:path';
6
+ import { basename, dirname, join, resolve } from 'node:path';
7
7
  import { debug, redactSecretText } from '../../src/debug/log.js';
8
8
  import { registerStandaloneLogSink } from '../../src/domains/standalone-log-sink.js';
9
9
  import { landingFreezeMessage, readLandingFreeze } from '../../src/release-loop/landing-freeze.js';
@@ -12,6 +12,7 @@ import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
12
12
  import { diffFailures, junitFailures, parseFailures, parseTimedOutFailures } from './gate-diff';
13
13
  import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
14
14
  import { loadGateBatchSafe } from './gate-batch-safe';
15
+ import { observeGatePrewarmCutShadow, type GatePrewarmInput } from './gate-prewarm-plan.js';
15
16
  import { gateHistory } from './gate-history.js';
16
17
  import { baselineFromCutLogs } from './baseline-from-logs';
17
18
  import { mergePartialFailures, readPartialPlanResult, testFileOfId, verifyDocsOnlyCarry } from './gate-partial';
@@ -31,7 +32,7 @@ import { emitNodeResult, readGraphContext } from './node-verdict.js';
31
32
  import { isGateExecutor, type GateExecutor } from './gate-executor.js';
32
33
  import { runPodCommand, parsePodCpu, podCpuMillis, resolvePodCpu, type RunPodCommandOptions, type PodCommandResult, type PodCpu } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
33
34
  import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from '../../src/task-orchestrator/surfaces/pod-bun-cache.js';
34
- import { PodPoolScheduler, parsePodPool, checkPodPool, defaultRemoteRun, type PodPoolMember } from '../../src/task-orchestrator/surfaces/pod-pool.js';
35
+ import { PodPoolScheduler, parsePodPool, checkPodPool, defaultRemoteRun, snapshotWarmPoolMembers, type PodPoolMember } from '../../src/task-orchestrator/surfaces/pod-pool.js';
35
36
  import { POD_INSTALL_SLOTS_DEFAULT, POD_INSTALL_SLOTS_HOST_PATH, installSlotScript } from '../../src/task-orchestrator/surfaces/pod-install-slots.js';
36
37
  import { gateDepsInstallScript, parseGateDeps } from '../../src/task-orchestrator/surfaces/pod-gate-deps.js';
37
38
  import { warmReferenceFromJobArtifacts, writeWarmJobManifest } from './gate-warm-artifacts.js';
@@ -41,7 +42,10 @@ import { requestWarmPodReplay, finalizeWarmPodReplay, quarantineWarmPodReplay, t
41
42
  import { resolveWarmPoolProfile } from '../../src/self-implement/pod-warm-pool-profile.js';
42
43
  import { decideWarmReuse, type WarmReuseSwitchConfig, type WarmReuseEvidence } from '../../src/task-orchestrator/surfaces/pod-warm-switch.js';
43
44
  import { getUserConfig } from '../../src/user-config.js';
44
- import { gateShardsPath, recordGateShardsVerdict, writeGateShards, type GateShard } from '../../src/release-loop/gate-shards.js';
45
+ import { beginGateShardAttempt, finalizeTerminalGateShards, gateShardsPath, recordGateShardsVerdict, writeGateShards, type GateJobTerminal, type GateShard } from '../../src/release-loop/gate-shards.js';
46
+ import { appendGateShardLedger, gateShardLedgerPath, type GateShardLedgerRow } from '../../src/release-loop/gate-shard-ledger.js';
47
+ import { markGateShardUnmeasured, nextGateShardAttempt, readGateShardManifest, readGateShardResult, saveGateShardResult } from './gate-shard-receipts.js';
48
+ import { planGateShardDispatch, type GateShardReceipt } from './gate-shard-dispatch.js';
45
49
  import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
46
50
  import { executeTuiNodeCheck, type ExecutedTuiCheck } from './tui-node-checks.js';
47
51
  import { runWithReleaseGateTraffic } from './gate-traffic-mode.js';
@@ -128,6 +132,72 @@ export interface GateRunner {
128
132
  /** GATE-FLOW-SIM — the clock this runner measures with (the injected fake clock in a scenario); absent = `Date.now`. */
129
133
  now?(): number;
130
134
  }
135
+
136
+ /** One watchdog tick. Batch Kubernetes reads per host and trust only the current attempt's result marker. */
137
+ export function reconcileStaleGateShardBoard(input: {
138
+ version: string;
139
+ board: GateShard[];
140
+ logDir?: string;
141
+ kubectl: typeof defaultKubectl;
142
+ now?: Date;
143
+ }): { board: GateShard[]; finalized: Array<{ shard: string; job?: string; state: GateShard['state']; source: 'result-json' | 'kubernetes-job-terminal'; endedAt: string }> } {
144
+ const terminalJobs = new Map<string, GateJobTerminal>();
145
+ const results = new Map<string, { completed?: boolean; jobName?: string; attemptId?: string; jobFailed?: boolean; rc?: number | null; reason?: 'timeout' | 'stall' }>();
146
+ const active = input.board.filter((shard) => shard.state === 'running' || shard.state === 'retry');
147
+ if (!active.length) return { board: input.board, finalized: [] };
148
+ for (const shard of active) {
149
+ if (input.logDir && shard.resultFile) {
150
+ try {
151
+ const raw = JSON.parse(readFileSync(join(input.logDir, basename(shard.resultFile)), 'utf8')) as Record<string, unknown>;
152
+ if (raw && typeof raw === 'object' && !Array.isArray(raw) && raw.completed === true && typeof shard.job === 'string'
153
+ && raw.jobName === shard.job && typeof shard.attemptId === 'string' && raw.attemptId === shard.attemptId) {
154
+ results.set(shard.id, { completed: true, jobName: shard.job, attemptId: shard.attemptId,
155
+ jobFailed: raw.jobFailed === true, rc: typeof raw.rc === 'number' ? raw.rc : null,
156
+ ...(raw.reason === 'timeout' || raw.reason === 'stall' ? { reason: raw.reason } : {}) });
157
+ }
158
+ } catch { /* this attempt has not produced its result file yet */ }
159
+ }
160
+ }
161
+ const byHost = new Map<string, GateShard[]>();
162
+ for (const shard of active) if (shard.job && shard.host) byHost.set(shard.host, [...(byHost.get(shard.host) ?? []), shard]);
163
+ for (const [host, shards] of byHost) {
164
+ const retryIndividually = new Set(shards.map((shard) => shard.job!));
165
+ try {
166
+ const status = input.kubectl(['--context', host, '-n', 'elanous-test', 'get', 'jobs', ...shards.map((shard) => shard.job!), '-o', 'json']);
167
+ const parsed = JSON.parse(status.stdout) as { items?: Array<{ metadata?: { name?: string }; status?: { conditions?: Array<{ type?: string; status?: string }> } }>; metadata?: { name?: string }; status?: { conditions?: Array<{ type?: string; status?: string }> } };
168
+ const jobs = parsed.items ?? (parsed.metadata?.name ? [parsed as { metadata?: { name?: string }; status?: { conditions?: Array<{ type?: string; status?: string }> } }] : []);
169
+ for (const shard of shards) {
170
+ if (jobs.some((job) => job.metadata?.name === shard.job)) retryIndividually.delete(shard.job!);
171
+ const conditions = jobs.find((job) => job.metadata?.name === shard.job)?.status?.conditions ?? [];
172
+ if (conditions.some((condition) => condition.status === 'True' && condition.type === 'Complete')) terminalJobs.set(shard.job!, 'complete');
173
+ else if (conditions.some((condition) => condition.status === 'True' && condition.type === 'Failed')) terminalJobs.set(shard.job!, 'failed');
174
+ }
175
+ } catch { /* a transient status read does not affect the gate verdict */ }
176
+ // One missing/deleted Job can make a multi-name kubectl request non-zero; retry each name so siblings still reconcile.
177
+ for (const shard of shards) {
178
+ if (!retryIndividually.has(shard.job!)) continue;
179
+ try {
180
+ const status = input.kubectl(['--context', host, '-n', 'elanous-test', 'get', 'job', shard.job!, '-o', 'json']);
181
+ const parsed = JSON.parse(status.stdout) as { metadata?: { name?: string }; status?: { conditions?: Array<{ type?: string; status?: string }> } };
182
+ const conditions = parsed.status?.conditions ?? [];
183
+ if (parsed.metadata?.name === shard.job && conditions.some((condition) => condition.status === 'True' && condition.type === 'Complete')) terminalJobs.set(shard.job!, 'complete');
184
+ else if (parsed.metadata?.name === shard.job && conditions.some((condition) => condition.status === 'True' && condition.type === 'Failed')) terminalJobs.set(shard.job!, 'failed');
185
+ } catch { /* this Job is still active, gone, or unreadable */ }
186
+ }
187
+ }
188
+ const reconciled = finalizeTerminalGateShards({ v: 1, version: input.version, updatedAt: (input.now ?? new Date()).toISOString(), shards: input.board },
189
+ terminalJobs, results, input.now ?? new Date());
190
+ const finalized = reconciled.shards.flatMap((shard) => {
191
+ if ((shard.state === 'running' || shard.state === 'retry') || !shard.endedAt) return [];
192
+ const previous = input.board.find((row) => row.id === shard.id);
193
+ if (!previous || (previous.state !== 'running' && previous.state !== 'retry')) return [];
194
+ const result = results.get(shard.id);
195
+ const source = result?.completed === true
196
+ ? 'result-json' as const : 'kubernetes-job-terminal' as const;
197
+ return [{ shard: shard.id, ...(shard.job ? { job: shard.job } : {}), state: shard.state, source, endedAt: shard.endedAt }];
198
+ });
199
+ return { board: reconciled.shards, finalized };
200
+ }
131
201
  export interface GateOptions {
132
202
  commit: string;
133
203
  /** The graph run that launched this gate; direct hard rehearsal instead pins source bytes explicitly. */
@@ -151,6 +221,12 @@ export interface GateOptions {
151
221
  recheckFileLimit?: number;
152
222
  /** freshShards — GATE-SHARD-RESUME off: every shard starts a Pod even when this logDir already holds its result. */
153
223
  pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string; bunCache?: string; freshShards?: boolean;
224
+ /** Remeasure one named shard, or all incomplete shards with auto. Complete siblings keep their receipts. */
225
+ rerouteShard?: string | 'auto';
226
+ /** Pool context for the rerouted shard; the planner otherwise distributes missing shards over pool contexts. */
227
+ shardHost?: string;
228
+ /** Shard capability requirements are independent of host names; placement resolves them at run time. */
229
+ shardCapabilities?: Readonly<Record<string, { os?: 'darwin' | 'linux' }>>;
154
230
  /** Opt-in gate shadow: provider fills the fenced lease identity. The Job remains authoritative. */
155
231
  warmReplay?: (input: Omit<WarmWorkerCommand, 'leaseId' | 'generation' | 'workerId' | 'key'>) => Promise<{ command: WarmWorkerCommand | null; transport: WarmPodTransportResult }>;
156
232
  warmQuarantine?: (command: WarmWorkerCommand, reason: string) => Promise<void>;
@@ -159,6 +235,8 @@ export interface GateOptions {
159
235
  warmFinalize?: (command: WarmWorkerCommand) => Promise<void>;
160
236
  /** Set only on the cut recheck; baseline Jobs never consume the cut worker. */
161
237
  warmCutSha?: string;
238
+ /** A measured planner snapshot may accompany the cut; absent fields remain visibly unmeasured. */
239
+ warmPrewarmInput?: GatePrewarmInput;
162
240
  warmBaselineSha?: string;
163
241
  warmReuseConfig?: WarmReuseSwitchConfig;
164
242
  /** At most three cut isolation files may request a shadow replay per verdict. */
@@ -184,8 +262,8 @@ export interface GateOptions {
184
262
  hostMirrorReaders?: GateHostMirrorReaders;
185
263
  /** GATE-INSTALL-CACHE — concurrent `bun install`s per node (`release.loop.gatePodInstallSlots`); omitted = 4 · 0 = off. */
186
264
  installSlots?: number;
187
- /** GATE-MEM-ADMIT — seconds a shard waits for pool admission (memory headroom) before it launches anyway with
188
- * `admission-timeout` (`release.loop.gatePodAdmissionWaitSeconds`); omitted = {@link GATE_ADMISSION_WAIT_SECONDS_DEFAULT} · 0 = off. */
265
+ /** GATE-MEM-ADMIT — seconds an active shard waits for pool admission. Root shards beyond the pool cap queue
266
+ * outside this timer; an exhausted wait stays unmeasured (`release.loop.gatePodAdmissionWaitSeconds`); 0 = off. */
189
267
  admissionWaitSeconds?: number;
190
268
  /** GATE-MEM-ADMIT — an injected admission (tests · embedders); config never sets it. Absent = the gate's own pool's. */
191
269
  admission?: GateAdmission;
@@ -210,7 +288,8 @@ export interface GateOptions {
210
288
  * of {@link gateShardAbortScript} on the gate's own pool; an injected pool without it runs without the mid-run watch. */
211
289
  abortTests?: (job: { name: string; context?: string }, reason: string) => boolean | void;
212
290
  /** GATE-LIVE-OBS — the shard state file (`gateShardsPath(version)`) this sweep keeps current; absent = not written. */
213
- shardsFile?: { path: string; version: string };
291
+ shardsFile?: { path: string; version: string; ledgerPath?: string; envFingerprint?: string;
292
+ receiptRoot?: string; base40?: string; expectedImageDigest?: string | null };
214
293
  /** GATE-RISK-LANE — the ledgers `gateHistory` reads its watch list from; absent = no risk lane (the sweep plans as before). */
215
294
  riskWatch?: { instanceRoots: readonly string[]; ledgerRoots: readonly string[] };
216
295
  /** GATE-MULTI-HOST — is this pool member reachable (kubectl ns ⊕ ssh)? Injected by tests; absent = the real probe on
@@ -258,7 +337,7 @@ export interface GateResult {
258
337
  /** GATE-WARM-BOOT-SCAN — observation only: the cut sweep's test selection time (pod sweep · absent otherwise). */
259
338
  selectionMs?: number;
260
339
  error?: string;
261
- stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'exit-code' | 'unattributed' | 'timeout'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string;
340
+ stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'exit-code' | 'unattributed' | 'timeout' | 'admission-timeout' | 'admission-error'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string;
262
341
  /** Pool member that ran the final unmeasured attempt; enables a bounded cross-host tail remeasure. */
263
342
  lastHost?: string;
264
343
  /** Failure of the optional cross-host tail attempt; original stall fields remain authoritative. */
@@ -454,6 +533,31 @@ const lastMatch = (output: string, pattern: RegExp): RegExpExecArray | null => {
454
533
  return last;
455
534
  };
456
535
  const summaryCount = (output: string, label: string) => Number(lastMatch(output, new RegExp(`(?:^|\\n)\\s*(\\d+) ${label}s?\\s*(?=\\n|$)`))?.[1] ?? NaN);
536
+ /** `test:deterministic` runs several Bun commands. Its final clean command is not the verdict for earlier commands. */
537
+ function deterministicSubrunCounts(output: string): { fail: number; error: number } | undefined {
538
+ if (!/error: script "test:deterministic" exited with code 1/.test(output)) return undefined;
539
+ const lines = output.split(/\r?\n/).map((line) => line.trim());
540
+ const runs: Array<{ fail: number; error: number }> = [];
541
+ for (let i = 0; i < lines.length; i += 1) {
542
+ if (!/^Ran \d+ tests? across [1-9]\d* files?\./.test(lines[i]!)) continue;
543
+ let pass: number | undefined;
544
+ let fail: number | undefined;
545
+ let error = 0;
546
+ for (let j = i - 1; j >= Math.max(0, i - 8); j -= 1) {
547
+ const line = lines[j]!;
548
+ const count = /^(\d+) (pass|fail|skip|errors?|expect\(\) calls)$/.exec(line);
549
+ if (!line) continue;
550
+ if (!count) break;
551
+ if (count[2] === 'pass') { pass = Number(count[1]); break; }
552
+ if (count[2] === 'fail') fail = Number(count[1]);
553
+ if (count[2] === 'error' || count[2] === 'errors') error = Number(count[1]);
554
+ }
555
+ if (pass === undefined || fail === undefined) return undefined;
556
+ runs.push({ fail, error });
557
+ }
558
+ if (runs.length < 2) return undefined;
559
+ return runs.reduce((sum, run) => ({ fail: sum.fail + run.fail, error: sum.error + run.error }), { fail: 0, error: 0 });
560
+ }
457
561
  /** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
458
562
  const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
459
563
  /** A Pod path (`~/repo/src/…` · `../..~/repo/src/…`) as a repository-relative one. */
@@ -976,20 +1080,29 @@ export function gateShardAbortScript(reason: string, graceSeconds = GATE_SHARD_A
976
1080
  return [
977
1081
  `reason=${quote(reason.replace(/[^\w .:-]/g, '_'))}`,
978
1082
  `only=${quote(outbox ?? '')}`,
979
- "pg_of() { sed 's/^.*) //' \"/proc/$1/stat\" 2>/dev/null | cut -d' ' -f3; }",
1083
+ // Read after the final ')' because comm can itself contain spaces and ')'. starttime is field 20 in the suffix
1084
+ // (field 22 in proc stat); pairing it with PID prevents a recycled PID from validating an old group number.
1085
+ "proc_identity() { sed 's/^.*) //' \"/proc/$1/stat\" 2>/dev/null | awk '{print $3, $20}'; }",
1086
+ "pg_of() { proc_identity \"$1\" | awk '{print $1}'; }",
980
1087
  'root_pg=$(pg_of 1); my_pg=$(pg_of $$); found=0',
981
1088
  'for d in /proc/[0-9]*; do',
982
1089
  ' pid=${d#/proc/}',
983
1090
  " out=$(tr '\\0' '\\n' < \"$d/cmdline\" 2>/dev/null | grep -m1 -E '^--reporter-outfile=.*/outbox/part-[0-9]+\\.junit\\.xml$') || continue",
984
1091
  ' if [ -n "$only" ]; then case "$out" in "--reporter-outfile=$only/part-"*) ;; *) continue;; esac; fi',
985
- ' pg=$(pg_of "$pid")',
986
- ' case "$pg" in ""|*[!0-9]*) continue;; esac',
1092
+ ' identity=$(proc_identity "$pid")',
1093
+ ' pg=${identity%% *}; started=${identity#* }',
1094
+ ' case "$pg:$started" in *[!0-9:]*|:|*:|:*) continue;; esac',
987
1095
  ' if [ "$pg" != "$pid" ] || [ "$pg" -le 1 ] || [ "$pg" = "$root_pg" ] || [ "$pg" = "$my_pg" ]; then continue; fi',
1096
+ // Re-read the process identity immediately before signaling. The same check guards delayed KILL so a PID/PGID
1097
+ // that disappeared and was reused during the grace interval is never signaled as if it were the original test.
1098
+ ' [ "$(proc_identity "$pid")" = "$identity" ] || continue',
988
1099
  ' out=${out#--reporter-outfile=}; dir=${out%/part-*}; part=${out##*/part-}; part=${part%.junit.xml}',
989
- ' printf "%s\\n" "$reason" > "$dir/part-$part.abort"',
990
- ' [ -f "$dir/shard.timeout" ] || echo "$part" > "$dir/shard.timeout"',
991
- ' if kill -TERM -- "-$pg" 2>/dev/null; then found=$((found + 1)); echo "ELANOUS_GATE_ABORT part=$part pgid=$pg"; fi',
992
- ` ( sleep ${Math.max(1, Math.floor(graceSeconds))}; kill -KILL -- "-$pg" 2>/dev/null ) >/dev/null 2>&1 </dev/null &`,
1100
+ ' if [ "$(proc_identity "$pid")" = "$identity" ] && kill -TERM -- "-$pg" 2>/dev/null; then',
1101
+ ' printf "%s\\n" "$reason" > "$dir/part-$part.abort"',
1102
+ ' [ -f "$dir/shard.timeout" ] || echo "$part" > "$dir/shard.timeout"',
1103
+ ' found=$((found + 1)); echo "ELANOUS_GATE_ABORT part=$part pgid=$pg"',
1104
+ ` ( sleep ${Math.max(1, Math.floor(graceSeconds))}; [ "$(proc_identity "$pid")" = "$identity" ] && kill -KILL -- "-$pg" 2>/dev/null ) >/dev/null 2>&1 </dev/null &`,
1105
+ ' fi',
993
1106
  'done',
994
1107
  'echo "ELANOUS_GATE_ABORT signalled=$found"',
995
1108
  '[ "$found" -gt 0 ]',
@@ -1121,8 +1234,8 @@ const lastStartedTestFile = (output: string, paths: string[]): string | undefine
1121
1234
  * split shape follows from depth and size alone, so the same plan reaches the same child names (TC 09:30 review ②).
1122
1235
  * `runKey` hashes where and how the Pod ran the shard (pool · command · ignores · memory · deadline · Bun cache): a fix on the Pod side
1123
1236
  * makes every old result a miss instead of re-reading an old failure as introduced (TC 09:48 review). */
1124
- export function previousShardDescent(logDir: string, name: string, commit: string, files: readonly string[], runKey: string, parentAttempt?: string, splitChild?: string): { way: 'retry' | 'split'; attemptId: string } | undefined {
1125
- const meta = (file: string): { commit?: unknown; files?: unknown; rc?: unknown; runKey?: unknown; attemptId?: unknown; parentAttempt?: unknown } | undefined => {
1237
+ export function previousShardDescent(logDir: string, name: string, commit: string, files: readonly string[], runKey: string, parentAttempt?: string, splitChild?: string): { way: 'retry' | 'split' | 'file-fanout'; attemptId: string } | undefined {
1238
+ const meta = (file: string): { commit?: unknown; files?: unknown; rc?: unknown; reason?: unknown; runKey?: unknown; attemptId?: unknown; parentAttempt?: unknown } | undefined => {
1126
1239
  try { return JSON.parse(readFileSync(join(logDir, `${file}.json`), 'utf8')); } catch { return undefined; }
1127
1240
  };
1128
1241
  const own = meta(name);
@@ -1139,6 +1252,8 @@ export function previousShardDescent(logDir: string, name: string, commit: strin
1139
1252
  && (!whole || row.files.length === files.length);
1140
1253
  };
1141
1254
  if (child(`${name}-retry`, true)) return { way: 'retry', attemptId };
1255
+ // A stalled multi-file parent may have descended directly to one Pod per file. Keep that shape on resume.
1256
+ if (own.reason === 'stall' && files.length > 1 && child(`${name}-file-0`, false)) return { way: 'file-fanout', attemptId };
1142
1257
  // Only the first child of the split the caller takes now (`-file-0` · `-c0` · `-0` · review r3): a record of another
1143
1258
  // split shape is not followed.
1144
1259
  return splitChild !== undefined && child(`${name}${splitChild}`, false) ? { way: 'split', attemptId } : undefined;
@@ -1170,9 +1285,11 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
1170
1285
  const output = stripPodRepoRoot(run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, ''));
1171
1286
  const ran = lastMatch(output, /Ran (\d+) tests? across ([1-9]\d*) files?/);
1172
1287
  const reportedErrors = summaryCount(output, 'error');
1173
- const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
1174
- const incomplete = (reason: string) => new Error(`${label} incomplete (rc=${run.rc}; Ran=${ran ? ran[0] : 'missing'}; errors=${Number.isNaN(reportedErrors) ? 'missing' : errorCount}; ${reason})\n${tail40(output)}`);
1175
- const count = summaryCount(output, 'fail');
1288
+ // Validated Pod shards were checked separately before their logs were joined; only the host sweep needs subrun totals.
1289
+ const subruns = !run.validatedShards && run.rc === 1 ? deterministicSubrunCounts(output) : undefined;
1290
+ const errorCount = subruns?.error ?? (Number.isNaN(reportedErrors) ? 0 : reportedErrors);
1291
+ const incomplete = (reason: string) => new Error(`${label} incomplete (rc=${run.rc}; Ran=${ran ? ran[0] : 'missing'}; errors=${subruns || !Number.isNaN(reportedErrors) ? errorCount : 'missing'}; ${reason})\n${tail40(output)}`);
1292
+ const count = subruns?.fail ?? summaryCount(output, 'fail');
1176
1293
  const failures = parseFailures(output);
1177
1294
  // Bun repeats failed test names under "N tests failed:". A validated Pod sweep joins
1178
1295
  // separately checked shards, so a summary in one shard can precede real failures
@@ -1202,7 +1319,7 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
1202
1319
  }
1203
1320
  }
1204
1321
  if (errors.length !== errorCount) throw incomplete(`error attribution identified=${errors.length}`);
1205
- if (!Number.isFinite(count) || (count !== occurrences && !(run.validatedShards && occurrences > count && failures.length === count)) || (count > 0 && failures.length === 0)
1322
+ if (!Number.isFinite(count) || (count !== occurrences && !(run.validatedShards && occurrences > count && failures.length === count)) || (subruns && failures.length !== count) || (count > 0 && failures.length === 0)
1206
1323
  || (rc === 1 && count === 0 && errorCount === 0) || (rc === 0 && count + errorCount > 0)) {
1207
1324
  throw incomplete(`failure attribution incomplete: summary=${count}, identified=${failures.length}`);
1208
1325
  }
@@ -1229,7 +1346,7 @@ export function readIsolatedRun(run: CommandResult, label: string, file: string)
1229
1346
  if (!cleanNonzero) {
1230
1347
  const read = failuresOf(run, label);
1231
1348
  // Two (fail) lines folding into one id would undercount the file's failures: unreadable, not fewer.
1232
- const failCount = summaryCount(output, 'fail');
1349
+ const failCount = deterministicSubrunCounts(output)?.fail ?? summaryCount(output, 'fail');
1233
1350
  if (Number.isFinite(failCount) && read.failures.length !== failCount)
1234
1351
  throw new Error(`${label} incomplete (rc=${run.rc}; failure attribution summary=${failCount}, identified=${read.failures.length})`);
1235
1352
  return { kind: 'read', ...read };
@@ -1342,22 +1459,54 @@ export function limitedLocalCommand(cmd: string, args: string[], cwd: string, li
1342
1459
  * GATE-MEM-ADMIT (0.2.20) — the gate's shard Jobs ask the same pool admission harness goal Pods ask
1343
1460
  * (`PodPoolScheduler.acquireAdmission` · memory headroom by observed usage · `limitedBy`). 0.2.19: 8 shards sat Pending
1344
1461
  * for CPU (requests 31.2/32) because harness Pods held the node and the gate counted only its own Jobs (#24698).
1345
- * The gate never deadlocks on it: after {@link GATE_ADMISSION_WAIT_SECONDS_DEFAULT} it launches anyway and records
1346
- * `admission-timeout`; an admission error launches at once (`admission-error`). Harness launches wait without a bound,
1347
- * so the gate keeps priority.
1462
+ * A timed out or failed admission means the lease cannot protect capacity, so the shard remains unmeasured.
1463
+ * A later release resume can retry it when capacity is available.
1348
1464
  */
1349
1465
  export interface GateAdmission {
1350
1466
  acquire(signal: AbortSignal): Promise<() => void>;
1351
1467
  /** The pool-wide reading admission waits on (recommended · limitedBy · memorySlots); null before any reading. */
1352
1468
  waitReason(): string | null;
1353
1469
  }
1354
- export const GATE_ADMISSION_WAIT_SECONDS_DEFAULT = 300;
1470
+ export const GATE_ADMISSION_WAIT_SECONDS_DEFAULT = 1800;
1471
+ /** Queue pressure scales the default wait but never creates an unbounded admission timer. */
1472
+ export function gateAdmissionWaitSeconds(shards: number, slots: number): number {
1473
+ return Math.max(GATE_ADMISSION_WAIT_SECONDS_DEFAULT, Math.min(3600,
1474
+ Math.ceil(shards / Math.max(1, slots)) * 300));
1475
+ }
1476
+ export function createGateRootLimiter(slots: number): <T>(run: () => Promise<T>) => Promise<T> {
1477
+ if (!Number.isSafeInteger(slots) || slots < 1) throw new Error('invalid gate root slots');
1478
+ let active = 0;
1479
+ const waiters: Array<() => void> = [];
1480
+ return async <T>(run: () => Promise<T>): Promise<T> => {
1481
+ if (active >= slots) await new Promise<void>((resolve) => waiters.push(resolve));
1482
+ else active++;
1483
+ try { return await run(); }
1484
+ finally {
1485
+ const next = waiters.shift();
1486
+ if (next) next(); // transfer the occupied slot before another caller can enter
1487
+ else active--;
1488
+ }
1489
+ };
1490
+ }
1491
+ /** A direct gate has no preflight image pin. Its measured receipt still carries its own image digest. */
1492
+ export function gateReceiptReuseKey(receipt: GateShardReceipt | null, fingerprint: string, expectedImageDigest: string | null | undefined,
1493
+ hosts: ReadonlySet<string>): { envKey?: string; reason: string } {
1494
+ if (!receipt) return { reason: 'receipt-missing' };
1495
+ if (receipt.status !== 'complete') return { reason: 'receipt-unmeasured' };
1496
+ if (!hosts.has(receipt.host)) return { reason: 'receipt-host-outside-pool' };
1497
+ if (expectedImageDigest) return receipt.envKey === `${fingerprint}:${expectedImageDigest}`
1498
+ ? { envKey: receipt.envKey, reason: 'image-pinned' } : { reason: 'receipt-image-or-env-drift' };
1499
+ const prefix = `${fingerprint}:`;
1500
+ if (!receipt.envKey.startsWith(prefix)) return { reason: 'receipt-env-drift' };
1501
+ return /^sha256:[a-f0-9]{64}$/i.test(receipt.envKey.slice(prefix.length))
1502
+ ? { envKey: receipt.envKey, reason: 'measured-image' } : { reason: 'receipt-image-unmeasured' };
1503
+ }
1355
1504
  export type GateAdmissionOutcome = 'granted' | 'admission-timeout' | 'admission-error' | 'off';
1356
1505
  export interface GateAdmissionResult { outcome: GateAdmissionOutcome; waitedMs: number; reason?: string; release: () => void }
1357
1506
  export const gateAdmissionWaitText = (reading: string | null) => `메모리 여유 대기(admission) · ${reading ?? '측정 전'}`.slice(0, 160);
1358
1507
  /**
1359
1508
  * GATE-MULTI-HOST (0.2.25 · 대표) — the gate pool is a host roster (`release.loop.gatePodPool` =
1360
- * `pool-node-b@node-b:N,pool-node-c@node-c:M,…`); shards spread over it by free share (selfCapped). A member that fails a
1509
+ * `pool-node-a@node-a:N,pool-node-b@node-b:M,…`); shards spread over it by free share (selfCapped). A member that fails a
1361
1510
  * shard's launch or goes silent mid-Job is probed; if it is unreachable it leaves the roster for the rest of this gate and
1362
1511
  * the shard runs again on the others — one dead host no longer stops the gate. One-member pools keep the old behaviour.
1363
1512
  */
@@ -1412,6 +1561,7 @@ export function defaultSshCommand(host: string, script: string, limitMs?: number
1412
1561
  }
1413
1562
 
1414
1563
  export function createGateRunner(repo: string, remote?: string, commandOverride?: GateRunner['command'], podCommand: (options: RunPodCommandOptions) => Promise<PodCommandResult> = runPodCommand, poolOverride?: PodPoolScheduler, podLogTail?: (job: PodCommandResult) => string, tools: GateTools = {}): GateRunner {
1564
+ const observedPrewarmCuts = new Set<string>();
1415
1565
  let remoteMirror: string | undefined;
1416
1566
  // GATE-FLOW-SIM: the one place the real outside world is named — an injected seam replaces it, absent = today's tools.
1417
1567
  const clock = tools.now ?? Date.now;
@@ -1500,6 +1650,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1500
1650
  const watchList = pod.riskWatch ? readGateRiskWatchList(pod.riskWatch) : [];
1501
1651
  const riskPlan = planGateRiskLaneShards({ assignable, durations, shardCount, heavy, watchList });
1502
1652
  const shards = riskPlan.shards;
1653
+ const receiptRoot = pod.shardsFile?.receiptRoot;
1654
+ const base40 = pod.shardsFile?.base40;
1655
+ const envFingerprint = pod.shardsFile?.envFingerprint;
1656
+ const expectedImageDigest = pod.shardsFile?.expectedImageDigest;
1503
1657
  if (riskPlan.spawnFiles.length) debug.log('release-loop.gate', 'pod-spawn-lane', { files: riskPlan.spawnFiles, shards: riskPlan.spawnShardCount });
1504
1658
  if (riskPlan.riskFiles.length) debug.log('release-loop.gate', 'pod-risk-lane-plan', { files: riskPlan.riskFiles, shard: 0,
1505
1659
  plannedSeconds: riskPlan.riskPlannedSeconds, shards: shards.length, watch: watchList.length });
@@ -1521,6 +1675,56 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1521
1675
  const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), kubectl);
1522
1676
  if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
1523
1677
  const poolScheduler = poolOverride ?? new PodPoolScheduler(ready!.ready);
1678
+ const receiptChecks = shards.map((item, shard) => {
1679
+ if (pod.freshShards) return { reason: 'fresh-shards' };
1680
+ if (!receiptRoot || !base40 || !envFingerprint) return { reason: 'receipt-context-missing' };
1681
+ const prior = readGateShardManifest(receiptRoot, `pod-${shard}`);
1682
+ const key = gateReceiptReuseKey(prior, envFingerprint, expectedImageDigest,
1683
+ new Set(poolScheduler.members.map((member) => member.context)));
1684
+ if (!key.envKey) return { reason: key.reason };
1685
+ const result = readGateShardResult(receiptRoot, { shard: { id: `pod-${shard}`, files: item.files },
1686
+ head40: commit, base40, envKey: key.envKey });
1687
+ return result ? { reason: key.reason, result } : { reason: 'receipt-identity-or-evidence-invalid' };
1688
+ });
1689
+ const receipts = receiptChecks.flatMap((check) => check.result ? [check.result.receipt] : []);
1690
+ const requiredOs = pod.shardCapabilities ?? {};
1691
+ const envKnownIds = loadEnvKnownFailures(tree);
1692
+ const envKnownFiles = new Set([...envKnownIds].map(fileOf));
1693
+ const macPassShardIds = shards.flatMap((shard, index) => {
1694
+ const prior = receipts.find((receipt) => receipt.id === `pod-${index}`);
1695
+ if (!prior) return [];
1696
+ const knownFailure = [...prior.failureIds, ...prior.errorIds].some((id) => envKnownIds.has(id));
1697
+ const knownUnmeasuredFile = prior.status === 'unmeasured' && shard.files.some((file) => envKnownFiles.has(file));
1698
+ return knownFailure || knownUnmeasuredFile ? [`pod-${index}`] : [];
1699
+ });
1700
+ for (const id of Object.keys(requiredOs)) if (!/^pod-\d+$/.test(id) || Number(id.slice(4)) >= shards.length)
1701
+ throw new Error(`unknown shard capability target: ${id}`);
1702
+ const needsPlatform = Object.values(requiredOs).some((value) => value.os)
1703
+ || (pod.rerouteShard === 'auto' && macPassShardIds.length > 0);
1704
+ const hosts = poolScheduler.members.map((member) => {
1705
+ let platform: 'darwin' | 'linux' | 'other' = 'other';
1706
+ if (needsPlatform) {
1707
+ const system = member.sshHost ? remoteRun(member.sshHost, 'uname -s') :
1708
+ { status: 0, stdout: process.platform, stderr: '' };
1709
+ if (system.status === 0) platform = /^(?:darwin)$/i.test(system.stdout.trim()) ? 'darwin'
1710
+ : /^(?:linux)$/i.test(system.stdout.trim()) ? 'linux' : 'other';
1711
+ }
1712
+ return { id: member.context, platform, available: true, reserved: false };
1713
+ });
1714
+ const hostPlan = planGateShardDispatch({ head40: commit, base40: base40 ?? commit,
1715
+ shards: shards.map((item, shard) => ({ id: `pod-${shard}`, files: item.files,
1716
+ ...(requiredOs[`pod-${shard}`]?.os ? { requiredOs: requiredOs[`pod-${shard}`]!.os } : {}) })), receipts,
1717
+ hosts,
1718
+ macPassShardIds,
1719
+ ...(pod.rerouteShard ? { rerouteShard: pod.rerouteShard } : {}), ...(pod.shardHost ? { shardHost: pod.shardHost } : {}) });
1720
+ // Leave ordinary shards to the live free-slot scheduler. A static plan can pick a busy member
1721
+ // while another is idle; pin only a capability or a deliberate cross-host replacement.
1722
+ const hostPins = new Map(hostPlan.flatMap((item) => item.kind === 'run'
1723
+ && (requiredOs[item.shardId]?.os || (pod.rerouteShard === 'auto' && macPassShardIds.includes(item.shardId))
1724
+ || (item.replacing && item.host !== item.replacing.host)
1725
+ || (pod.shardHost && item.host === pod.shardHost)) ? [[item.shardId, item.host] as const] : []));
1726
+ const unavailable = hostPlan.find((item) => item.kind === 'unmeasured');
1727
+ if (unavailable) throw new Error(`gate shard ${unavailable.shardId} unmeasured: ${unavailable.reason}`);
1524
1728
  const configCache = typeof pod.bunCache === 'string' ? pod.bunCache.trim() : '';
1525
1729
  const envCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim();
1526
1730
  const bunCache = configCache || envCache || undefined;
@@ -1531,11 +1735,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1531
1735
  if (!Number.isSafeInteger(installSlots) || installSlots < 0) throw new Error('invalid pod sweep options');
1532
1736
  debug.log('release-loop.gate', 'pod-install-slots', { slots: installSlots, source: pod.installSlots !== undefined ? 'config' : 'default' });
1533
1737
  // GATE-MEM-ADMIT: every shard Job (retries and splits too) asks pool admission before it is placed.
1534
- const admissionWaitSeconds = pod.admissionWaitSeconds ?? GATE_ADMISSION_WAIT_SECONDS_DEFAULT;
1738
+ const rootSlots = Math.max(1, poolScheduler.members.reduce((total, member) => total + member.capacity, 0));
1739
+ const admissionWaitSeconds = pod.admissionWaitSeconds ?? gateAdmissionWaitSeconds(shards.length, rootSlots);
1535
1740
  if (!Number.isSafeInteger(admissionWaitSeconds) || admissionWaitSeconds < 0) throw new Error('invalid pod sweep options');
1536
1741
  // An injected pool (tests) runs without admission unless `pod.admission` is passed; the gate's own pool asks it.
1537
1742
  const gateAdmission = pod.admission ?? tools.admission ?? (poolOverride ? undefined : poolGateAdmission(poolScheduler));
1538
- debug.log('release-loop.gate', 'pod-admission', { waitSeconds: admissionWaitSeconds, source: pod.admissionWaitSeconds !== undefined ? 'config' : 'default', enabled: !!gateAdmission && admissionWaitSeconds > 0 });
1743
+ debug.log('release-loop.gate', 'pod-admission', { waitSeconds: admissionWaitSeconds, source: pod.admissionWaitSeconds !== undefined ? 'config' : 'default', enabled: !!gateAdmission && admissionWaitSeconds > 0,
1744
+ rootSlots, shards: shards.length });
1539
1745
  // GATE-POD-STALL-FAILFAST: a shard with no test output within the stall window is stopped and recorded unmeasured.
1540
1746
  const stallSeconds = pod.stallSeconds ?? GATE_POD_STALL_SECONDS_DEFAULT;
1541
1747
  const stallPollMs = pod.stallPollMs ?? GATE_POD_STALL_POLL_MS;
@@ -1653,42 +1859,131 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1653
1859
  }, 0);
1654
1860
  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);
1655
1861
  const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
1656
- // GATE-LIVE-OBS — one row per root shard in the release ledger's shards.json (UX contract · src/release-loop/gate-shards.ts).
1657
- // Retries and splits of a shard update its row. A write failure is logged and never stops the gate.
1862
+ // GATE-LIVE-OBS — root rows stay stable; each recursive split/retry and tail gets its own row.
1863
+ // Otherwise a finished root Job can hide a still-running child (or tail) behind 80/80 done.
1658
1864
  const board: GateShard[] = shards.map((item, index) => ({ id: `pod-${index}`, state: 'pending', plannedMin: Math.round(item.plannedSeconds / 6) / 10 }));
1865
+ const filesById = new Map(shards.map((item, index) => [`pod-${index}`, [...item.files]]));
1866
+ const detailsById = new Map<string, Partial<GateShardLedgerRow>>();
1867
+ const observeShard = (row: GateShard) => {
1868
+ if (!pod.shardsFile?.ledgerPath || !pod.shardsFile.envFingerprint) return;
1869
+ const detail = detailsById.get(row.id) ?? {};
1870
+ const logName = row.resultFile?.replace(/\.json$/, '') ?? null;
1871
+ const logPath = logName && logDir ? join(logDir, `${logName}.log`) : null;
1872
+ const junitPath = logName && logDir ? join(logDir, `${logName}.junit.xml`) : null;
1873
+ try {
1874
+ appendGateShardLedger(pod.shardsFile.ledgerPath, {
1875
+ v: 1, version: pod.shardsFile.version, phase: 'cut', shardId: row.id,
1876
+ files: filesById.get(row.id) ?? [], host: row.host ?? null, executor: 'pod',
1877
+ imageDigest: detail.imageDigest ?? null, commit, envFingerprint: pod.shardsFile.envFingerprint,
1878
+ startedAt: row.startedAt ?? null, endedAt: row.endedAt ?? null, observedAt: new Date().toISOString(),
1879
+ state: row.state, rc: row.rc ?? null, pass: detail.pass ?? null, fail: detail.fail ?? null,
1880
+ errors: detail.errors ?? null, ran: detail.ran ?? null,
1881
+ failureIds: detail.failureIds ?? null, errorIds: detail.errorIds ?? null,
1882
+ attribution: detail.attribution ?? 'unmeasured', logPath, junitPath,
1883
+ stopReason: detail.stopReason ?? row.waitReason ?? null, attemptId: row.attemptId ?? null,
1884
+ });
1885
+ } catch (error) { debug.log('release-loop.gate', 'shard-ledger-write-failed', { shard: row.id, error: String(error) }, { level: 'warn' }); }
1886
+ };
1659
1887
  const flushBoard = () => {
1660
1888
  if (!pod.shardsFile) return;
1661
1889
  try { writeGateShards(pod.shardsFile.path, { v: 1, version: pod.shardsFile.version, updatedAt: new Date().toISOString(), shards: board }); }
1662
1890
  catch (error) { debug.log('release-loop.gate', 'shards-write-failed', { path: pod.shardsFile.path, error: String(error) }); }
1663
1891
  };
1664
- const markShard = (shard: number, patch: Partial<GateShard>, clear: Array<keyof GateShard> = []) => {
1665
- const row = board[shard];
1892
+ const rowIndex = (shard: number, branch = '') => branch ? board.findIndex((row) => row.id === `pod-${shard}${branch}`) : shard;
1893
+ const markShard = (shard: number, patch: Partial<GateShard>, clear: Array<keyof GateShard> = [], branch = '') => {
1894
+ const index = rowIndex(shard, branch);
1895
+ const row = board[index];
1666
1896
  if (!row) return;
1667
1897
  const next: GateShard = { ...row, ...patch };
1668
1898
  for (const key of clear) delete next[key];
1669
1899
  if (JSON.stringify(next) === JSON.stringify(row)) return;
1670
- board[shard] = next;
1900
+ board[index] = next;
1901
+ observeShard(next);
1671
1902
  flushBoard();
1672
1903
  };
1904
+ const resetShardAttempt = (shard: number, branch = '', plannedMin?: number) => {
1905
+ if (branch && rowIndex(shard, branch) < 0) board.push({ id: `pod-${shard}${branch}`, state: 'pending', ...(plannedMin !== undefined ? { plannedMin } : {}) });
1906
+ const index = rowIndex(shard, branch);
1907
+ const row = board[index];
1908
+ if (!row) return;
1909
+ board[index] = beginGateShardAttempt(row);
1910
+ delete board[index]!.rc;
1911
+ delete board[index]!.installSec;
1912
+ // The root is a logical aggregate while its descendants run, not the terminal state of its first Job.
1913
+ if (branch) {
1914
+ board[shard] = beginGateShardAttempt(board[shard]!);
1915
+ delete board[shard]!.rc;
1916
+ delete board[shard]!.installSec;
1917
+ observeShard(board[shard]!);
1918
+ }
1919
+ detailsById.delete(row.id);
1920
+ observeShard(board[index]!);
1921
+ flushBoard();
1922
+ };
1923
+ const recordShardResult = (id: string, done: { runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }) => {
1924
+ const index = board.findIndex((row) => row.id === id);
1925
+ if (index < 0) return;
1926
+ const counts = done.runs.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
1927
+ errors: sum.errors + run.errors, ran: sum.ran + run.ran }), { pass: 0, fail: 0, errors: 0, ran: 0 });
1928
+ const failureIds: string[] = [];
1929
+ const errorIds: string[] = [];
1930
+ let attributed = done.stalled.length === 0 && done.runs.length > 0;
1931
+ for (const run of done.runs) {
1932
+ try {
1933
+ const parsed = failuresOf({ rc: run.rc, output: run.output, validatedShards: true }, `ledger ${id}`);
1934
+ failureIds.push(...parsed.failures); errorIds.push(...parsed.errors);
1935
+ } catch { attributed = false; }
1936
+ }
1937
+ const detail: Partial<GateShardLedgerRow> = { ...(detailsById.get(id) ?? {}), ...counts, failureIds: attributed ? [...new Set(failureIds)].sort() : null,
1938
+ errorIds: attributed ? [...new Set(errorIds)].sort() : null, attribution: attributed ? 'complete' : 'unmeasured',
1939
+ stopReason: done.stalled.map((item) => item.reason).join(', ') || null };
1940
+ detailsById.set(id, detail);
1941
+ observeShard(board[index]!);
1942
+ };
1943
+ for (const row of board) observeShard(row);
1673
1944
  flushBoard();
1945
+ // A result collector can lose its callback after the Kubernetes Job has already completed. Reconcile
1946
+ // the public shard board independently so a finished Job cannot remain «running» indefinitely.
1947
+ const shardsFile = pod.shardsFile;
1948
+ let reconcileTimer: ReturnType<typeof setInterval> | undefined;
1949
+ const reconcileTick = () => {
1950
+ if (!shardsFile) return;
1951
+ try {
1952
+ const reconciled = reconcileStaleGateShardBoard({ version: shardsFile.version, board, ...(logDir ? { logDir } : {}), kubectl });
1953
+ for (const item of reconciled.finalized) {
1954
+ const index = board.findIndex((shard) => shard.id === item.shard);
1955
+ if (index < 0) continue;
1956
+ board[index] = reconciled.board.find((shard) => shard.id === item.shard)!;
1957
+ observeShard(board[index]!);
1958
+ debug.log('release.gate', 'shard-stale-finalized', item);
1959
+ }
1960
+ flushBoard();
1961
+ } catch (error) {
1962
+ debug.log('release.gate', 'shard-watchdog-tick-failed', { error: String(error) }, { level: 'warn' });
1963
+ }
1964
+ };
1674
1965
  /** The gate's scheduler, seen through one shard: a slot wait and the Pod start become row states. */
1675
1966
  // GATE-MEM-ADMIT: `placed` releases the shard's admission once a member is chosen — from then on the Job is
1676
1967
  // Pending/Running in the cluster and the next admission reading counts it, so holding the lease would count it twice.
1677
- const trackedPool = (shard: number, retry: boolean, placed: (member: NonNullable<Awaited<ReturnType<PodPoolScheduler['tryAcquire']>>>) => void = () => {}, skipMembers: ReadonlySet<string> = new Set()): PodPoolScheduler => ({
1968
+ const trackedPool = (shard: number, branch: string, jobName: string, resultFile: string, attemptId: string, placed: (member: NonNullable<Awaited<ReturnType<PodPoolScheduler['tryAcquire']>>>) => void = () => {}, skipMembers: ReadonlySet<string> = new Set(), allowPin = false): PodPoolScheduler => ({
1678
1969
  members: poolScheduler.members,
1679
1970
  tryAcquire: async (...[skip, onEmpty, mode]: Parameters<PodPoolScheduler['tryAcquire']>) => {
1680
1971
  // GATE-MULTI-HOST: a lost member is skipped while another member is left (all lost = the old behaviour).
1681
1972
  const anyLeft = poolScheduler.members.some((m) => !downMembers.has(m.context));
1682
- const excluded = new Set([...(skip ?? []), ...skipMembers, ...(downMembers.size && anyLeft ? downMembers : [])]);
1973
+ // Only the first placement follows the plan; retries and unavailable hosts can move.
1974
+ const planned = allowPin && !skip?.size ? hostPins.get(`pod-${shard}`) : undefined;
1975
+ const pin = planned && !downMembers.has(planned) ? planned : undefined;
1976
+ const excluded = new Set([...(skip ?? []), ...skipMembers, ...(downMembers.size && anyLeft ? downMembers : []),
1977
+ ...(pin ? poolScheduler.members.filter((m) => m.context !== pin).map((m) => m.context) : [])]);
1683
1978
  const member = await poolScheduler.tryAcquire(excluded.size ? excluded : undefined, onEmpty, mode);
1684
1979
  if (member) placed(member);
1685
- if (member) markShard(shard, { state: retry ? 'retry' : 'running', startedAt: board[shard]?.startedAt ?? new Date().toISOString(), host: member.context }, ['waitReason']);
1686
- else markShard(shard, { waitReason: 'Pod 자리 대기' });
1980
+ if (member) markShard(shard, { state: 'running', startedAt: board[rowIndex(shard, branch)]?.startedAt ?? new Date().toISOString(), host: member.context, job: jobName, resultFile, attemptId }, ['waitReason'], branch);
1981
+ else markShard(shard, { waitReason: 'Pod 자리 대기' }, [], branch);
1687
1982
  return member;
1688
1983
  },
1689
1984
  release: (member: Parameters<PodPoolScheduler['release']>[0]) => poolScheduler.release(member),
1690
1985
  }) as unknown as PodPoolScheduler;
1691
- const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false, fresh = pod.freshShards === true, parentAttempt?: string, hostRetries = 0, batchIsolation = false, skipMembers: ReadonlySet<string> = new Set(), singleShotTail = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
1986
+ const runShardImpl = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false, fresh = pod.freshShards === true, parentAttempt?: string, hostRetries = 0, batchIsolation = false, skipMembers: ReadonlySet<string> = new Set(), singleShotTail = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
1692
1987
  // 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
1693
1988
  // GATE-TIMEOUT-HEAL: parts (each with its own junit) under a soft deadline inside this shard's own Job deadline.
1694
1989
  const shardDeadline = shardDeadlineSeconds(batchIsolation ? Math.max(...paths.map((file) => estimated([file]))) * Math.ceil(paths.length / batchWidth) : estimated(paths), deadlineSeconds);
@@ -1752,11 +2047,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1752
2047
  if (descent === 'retry') {
1753
2048
  // «descend» is only the way down; «resumed» comes from each leaf whose log reads cleanly (review r4).
1754
2049
  debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: 'retry' });
2050
+ resetShardAttempt(shard, branch);
1755
2051
  return runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId, hostRetries, batchIsolation, skipMembers);
1756
2052
  }
1757
- if (descent === 'split') {
2053
+ if (descent === 'split' || descent === 'file-fanout') {
2054
+ resetShardAttempt(shard, branch);
1758
2055
  output = undefined;
1759
- debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: 'split' });
2056
+ debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: descent });
1760
2057
  } else if (reused) {
1761
2058
  output = reused.output;
1762
2059
  junit = reused.junit;
@@ -1765,13 +2062,18 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1765
2062
  lastFile = lastStartedTestFile(output, paths);
1766
2063
  } else try {
1767
2064
  stamp('admissionStart');
1768
- admitted = await admitGateShard(gateAdmission, { waitMs: admissionWaitSeconds * 1000, onWait: (reason) => markShard(shard, { waitReason: reason }) });
2065
+ admitted = await admitGateShard(gateAdmission, { waitMs: admissionWaitSeconds * 1000, onWait: (reason) => markShard(shard, { waitReason: reason }, [], branch) });
1769
2066
  stamp('admissionEnd');
1770
2067
  if (admitted.outcome !== 'off') debug.log('release-loop.gate', 'shard-admission', { shard, branch, outcome: admitted.outcome, waitedMs: admitted.waitedMs, ...(admitted.reason ? { reason: admitted.reason } : {}) },
1771
2068
  admitted.outcome === 'granted' ? undefined : { level: 'warn' });
2069
+ if (admitted.outcome === 'admission-error' || admitted.outcome === 'admission-timeout') {
2070
+ admitted.release();
2071
+ return { runs: [], stalled: [{ shard, files: paths, reason: admitted.outcome, ...(admitted.reason ? { detail: admitted.reason } : {}) }] };
2072
+ }
1772
2073
  // The admission wait is not the shard's run time: deadline/timeout judgment and the measured duration start here.
1773
2074
  start = clock();
1774
2075
  const jobName = `gate-${randomUUID()}`;
2076
+ const resultFile = `pod-${shard}${branch}.json`;
1775
2077
  jobNameForCapture = jobName;
1776
2078
  // The member's context is known once the Job is placed; the live log is read (and the Job stopped) there.
1777
2079
  let jobContext: string | undefined;
@@ -1801,10 +2103,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1801
2103
  graphNode: 'gate', version: pod.shardsFile?.version, commit, shard: `${shard + 1}-of-${shards.length}`,
1802
2104
  reason: `release gate shard ${shard + 1}/${shards.length}`, pool: pod.pool, cpuReq: cpu.request,
1803
2105
  memReq: memoryLimit ?? POD_ISOLATION_MEMORY_LIMIT, warmReused: false },
1804
- pool: pod.pool, poolScheduler: trackedPool(shard, depth > 0 || branch !== '', (member) => {
2106
+ pool: pod.pool, poolScheduler: trackedPool(shard, branch, jobName, resultFile, attemptId, (member) => {
1805
2107
  stamp('poolPlaced'); jobContext = member.context; jobContextForCapture = member.context; placedOn = member.context; placed();
1806
2108
  if (branch === '-tail') tailPlacedHost = member.context;
1807
- }, skipMembers), clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds: shardDeadline,
2109
+ }, skipMembers, depth === 0 && branch === '' && hostRetries === 0 && !retriedNoOutput && !batchIsolation && skipMembers.size === 0), clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds: shardDeadline,
1808
2110
  ...gateHostMirror(pod),
1809
2111
  ...(multiHost ? { statusUnreadableMs: memberUnreadableSeconds * 1000 } : {}),
1810
2112
  ...(bunCache ? { bunCache } : {}),
@@ -1898,7 +2200,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1898
2200
  const unfinishedDestination = join(logDir, `pod-${shard}${branch}.unfinished.log`);
1899
2201
  if (unfinishedLog !== undefined) writeFileSync(unfinishedDestination, unfinishedLog, { mode: 0o600 });
1900
2202
  else rmSync(unfinishedDestination, { force: true });
1901
- writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null,
2203
+ writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ completed: true, jobName: jobNameForCapture, durationMs, rc: rc ?? null,
1902
2204
  ...(stall ? { reason: 'stall', finishedFiles: 0, stallSeconds: stall.seconds, sinceCheckoutMs: stall.sinceCheckoutMs } : timedOut ? { reason: 'timeout', finishedFiles: finished.length } : {}), files: paths, plannedSeconds: estimated(paths), deadlineSeconds: shardDeadline, shardCount: shards.length, sweepId, commit, runKey, attemptId, ...(parentAttempt ? { parentAttempt } : {}), jobExitCode: jobExitCode ?? null, jobFailed, ...(placedOn ? { host: placedOn } : {}),
1903
2205
  ...(Object.keys(phaseTimes).length ? { phaseTimes } : {}), ...(podStages ? { podStages } : {}),
1904
2206
  ...(podLifecycle && Object.keys(podLifecycle).length ? { podLifecycle } : {}),
@@ -1927,6 +2229,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1927
2229
  }
1928
2230
  }
1929
2231
  const installSeconds = parseInstallSeconds(output ?? '');
2232
+ if (imageDigest) detailsById.set(`pod-${shard}${branch}`, { ...(detailsById.get(`pod-${shard}${branch}`) ?? {}), imageDigest });
1930
2233
  // GATE-TIMEOUT-HEAL ③: a cut shard is «reason: timeout» (with the files that still finished), not a bare rc=null.
1931
2234
  if (!reused && !descent) debug.log('release-loop.gate', 'pod-shard', { shard, files: paths, durationMs, rc: rc ?? null, attempt: depth + 1, installSeconds, gateDeps: parseGateDeps(output ?? ''), deadlineSeconds: shardDeadline, parts: parts.length, ...(placedOn ? { host: placedOn } : {}),
1932
2235
  ...(testSeconds !== undefined ? { testSeconds } : {}),
@@ -1935,7 +2238,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1935
2238
  // only for a Job that came back (a launch that failed before the Job returned has no Pod to describe).
1936
2239
  if (!reused && !descent && podJobName !== undefined) emitPodLifecycle(gatePodLifecycleEvent({ runId: process.env.ELANOUS_RUN_ID ?? null, podJob: podJobName, host: placedOn ?? null,
1937
2240
  shard: `pod-${shard}${branch}`, attemptId, phaseTimes, podStages, podLifecycle, outcome: gatePodOutcome({ jobFailed, stalled: !!stall, timedOut, rc }) }));
1938
- if (!reused && !descent) markShard(shard, { ...(installSeconds !== null ? { installSec: installSeconds } : {}), ...(rc !== undefined ? { rc } : {}), ...(timedOut ? { state: 'timeout' as const } : {}) });
2241
+ if (!reused && !descent) markShard(shard, { ...(installSeconds !== null ? { installSec: installSeconds } : {}), ...(rc !== undefined ? { rc } : {}), ...(timedOut ? { state: 'timeout' as const } : {}) }, [], branch);
1939
2242
  let clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
1940
2243
  const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
1941
2244
  const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
@@ -2001,6 +2304,22 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2001
2304
  // 137 (killed · OOM) keeps the isolation path: re-running an OOM file only re-measures the same kill.
2002
2305
  const diedBeforeTests = reason === 'job-failed' && output === undefined && !jobFailed && jobExitCode !== undefined && jobExitCode !== 0
2003
2306
  && jobExitCode !== 137 && (paths.length === 1 || jobExitCode === 5);
2307
+ // A watched stall in a multi-file Job has already spent its first deadline. Measure each file on its own
2308
+ // under the sweep-wide isolation permit pool instead of paying for another whole-bundle Job and split tree.
2309
+ // The same descent is replayed after a cut resume when the parent and first child records are attributed.
2310
+ if (reason === 'no-output' && paths.length > 1 && !singleShotTail && (stall || descent === 'file-fanout')) {
2311
+ const count = Math.min(paths.length, isolationRemaining[shard]!);
2312
+ isolationRemaining[shard]! -= count;
2313
+ debug.log('release-loop.gate', 'pod-stall-file-fanout', { shard, branch, files: paths.length, launched: count,
2314
+ resumed: descent === 'file-fanout', limit: isolationConcurrency });
2315
+ const children = await Promise.all(paths.slice(0, count).map((file, index) => isolatedFile(() =>
2316
+ runShard([file], shard, depth + 1, `${branch}-file-${index}`, false, undefined, attemptId))));
2317
+ return { runs: children.flatMap((child) => child.runs), stalled: [
2318
+ ...children.flatMap((child) => child.stalled),
2319
+ ...(paths.length > count ? [{ shard, files: paths.slice(count), reason: 'no-output' as const,
2320
+ ...(placedOn ? { lastHost: placedOn } : {}) }] : []),
2321
+ ] };
2322
+ }
2004
2323
  if ((reason === 'no-output' || diedBeforeTests) && !retriedNoOutput) {
2005
2324
  const retry = await runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId, hostRetries, batchIsolation, skipMembers);
2006
2325
  debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
@@ -2131,22 +2450,98 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2131
2450
  errors: Number.isNaN(errors) ? 0 : errors,
2132
2451
  ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
2133
2452
  };
2134
- // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
2135
- // GATE-RISK-LANE — the risk shard's own wall time (planned vs measured in its log line); it never changes a verdict.
2136
- const shardDurations: number[] = [];
2137
- const settled = await Promise.allSettled(shards.map(async (item, shard) => {
2138
- const shardStartedAt = clock();
2453
+ const runShard = async (...args: Parameters<typeof runShardImpl>): ReturnType<typeof runShardImpl> => {
2454
+ const [paths, shard, depth = 0, branch = '', , , parentAttempt, hostRetries = 0] = args;
2455
+ filesById.set(`pod-${shard}${branch}`, [...paths]);
2456
+ if (branch || depth > 0 || parentAttempt !== undefined || hostRetries > 0) {
2457
+ resetShardAttempt(shard, branch, Math.round(estimated(paths) / 6) / 10);
2458
+ }
2139
2459
  try {
2140
- const done = await runShard(item.files, shard);
2141
- shardDurations[shard] = clock() - shardStartedAt;
2142
- const state = !done.stalled.length ? 'done' as const : done.stalled.some((stall) => stall.reason === 'timeout') ? 'timeout' as const : 'failed' as const;
2143
- markShard(shard, { state, endedAt: new Date().toISOString() }, ['waitReason']);
2460
+ const done = await runShardImpl(...args);
2461
+ if (branch) markShard(shard, { state: !done.stalled.length ? 'done' : done.stalled.some((stall) => stall.reason === 'timeout') ? 'timeout' : 'failed', endedAt: new Date().toISOString() }, ['waitReason'], branch);
2462
+ if (branch) recordShardResult(`pod-${shard}${branch}`, done);
2144
2463
  return done;
2145
2464
  } catch (error) {
2146
- markShard(shard, { state: 'failed', endedAt: new Date().toISOString() }, ['waitReason']);
2465
+ if (branch) markShard(shard, { state: 'failed', endedAt: new Date().toISOString() }, ['waitReason'], branch);
2147
2466
  throw error;
2148
2467
  }
2149
- }));
2468
+ };
2469
+ // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
2470
+ // GATE-RISK-LANE — the risk shard's own wall time (planned vs measured in its log line); it never changes a verdict.
2471
+ const shardDurations: number[] = [];
2472
+ // Keep root shards outside the timed Pod admission until a gate slot is available.
2473
+ // Otherwise a wide sweep starts every admission clock together and later waves expire before placement.
2474
+ const withRootSlot = createGateRootLimiter(rootSlots);
2475
+ debug.log('release-loop.gate', 'pod-root-concurrency', { limit: rootSlots, shards: shards.length });
2476
+ let settled: Array<PromiseSettledResult<Awaited<ReturnType<typeof runShard>>>>;
2477
+ if (shardsFile) {
2478
+ reconcileTimer = setInterval(reconcileTick, 30_000);
2479
+ reconcileTimer.unref();
2480
+ }
2481
+ try {
2482
+ settled = await Promise.allSettled(shards.map(async (item, shard) => {
2483
+ const shardStartedAt = clock();
2484
+ try {
2485
+ const shardId = `pod-${shard}`;
2486
+ const expected = hostPlan[shard]?.kind === 'reuse' ? receiptChecks[shard]?.result : null;
2487
+ if (expected) {
2488
+ const done = { runs: expected.runs, stalled: [] };
2489
+ shardDurations[shard] = 0;
2490
+ detailsById.set(shardId, { ...(detailsById.get(shardId) ?? {}), imageDigest: expected.receipt.envKey.slice(envFingerprint!.length + 1) });
2491
+ markShard(shard, { state: 'done', rc: done.runs.some((run) => run.fail || run.errors) ? 1 : 0,
2492
+ startedAt: expected.receipt.measuredAt, endedAt: new Date().toISOString(), host: expected.receipt.host }, ['waitReason']);
2493
+ recordShardResult(shardId, done);
2494
+ debug.log('release-loop.gate', 'shard-receipt-reused', { shard: shardId, host: expected.receipt.host, files: item.files.length });
2495
+ return done;
2496
+ }
2497
+ debug.log('release-loop.gate', 'shard-receipt-not-reused', { shard: shardId,
2498
+ reason: pod.rerouteShard === shardId ? 'reroute-requested'
2499
+ : receiptChecks[shard]?.result && pod.rerouteShard === 'auto' ? 'auto-reroute'
2500
+ : receiptChecks[shard]?.result ? 'dispatch-remeasure-required'
2501
+ : receiptChecks[shard]?.reason ?? 'receipt-unavailable',
2502
+ plan: hostPlan[shard]?.kind ?? 'none' });
2503
+ const receiptAttempt = receiptRoot ? nextGateShardAttempt(receiptRoot, shardId) : 1;
2504
+ if (receiptRoot && base40 && envFingerprint) {
2505
+ try {
2506
+ markGateShardUnmeasured(receiptRoot, { id: shardId, files: [...item.files], head40: commit, base40,
2507
+ host: 'pending', envKey: `${envFingerprint}:${expectedImageDigest ?? 'unmeasured'}`, status: 'unmeasured',
2508
+ failureIds: [], errorIds: [], evidencePath: 'pending', evidenceSha256: '0'.repeat(64),
2509
+ measuredAt: new Date().toISOString(), attempt: receiptAttempt });
2510
+ } catch (error) {
2511
+ debug.log('release-loop.gate', 'shard-receipt-invalidate-failed', { shard: shardId, error: String(error) }, { level: 'warn' });
2512
+ throw error;
2513
+ }
2514
+ }
2515
+ // A missing or deliberately replaced receipt must execute a new Job. The older log-only resume
2516
+ // has no base/environment/image binding and cannot authorize this cross-host measurement.
2517
+ // The sixth runShardImpl argument is `fresh`: a missing receipt must launch a new Job.
2518
+ const freshForReceipt = !!receiptRoot || pod.freshShards === true;
2519
+ const done = await withRootSlot(() => runShard(item.files, shard, 0, '', false, freshForReceipt));
2520
+ shardDurations[shard] = clock() - shardStartedAt;
2521
+ const state = !done.stalled.length ? 'done' as const : done.stalled.some((stall) => stall.reason === 'timeout') ? 'timeout' as const : 'failed' as const;
2522
+ markShard(shard, { state, rc: done.stalled.length || done.runs.some((run) => run.fail || run.errors) ? 1 : 0, endedAt: new Date().toISOString() }, ['waitReason']);
2523
+ recordShardResult(`pod-${shard}`, done);
2524
+ if (receiptRoot && base40 && envFingerprint && !done.stalled.length) {
2525
+ const imageDigest = detailsById.get(shardId)?.imageDigest;
2526
+ const attribution = detailsById.get(shardId)?.attribution;
2527
+ const row = board[shard];
2528
+ if (imageDigest && row?.host && attribution === 'complete') {
2529
+ try {
2530
+ saveGateShardResult(receiptRoot, { receipt: { id: shardId, files: [...item.files], head40: commit, base40,
2531
+ host: row.host, envKey: `${envFingerprint}:${imageDigest}`, status: 'complete',
2532
+ failureIds: detailsById.get(shardId)?.failureIds ?? [], errorIds: detailsById.get(shardId)?.errorIds ?? [],
2533
+ evidencePath: 'pending', evidenceSha256: '0'.repeat(64), measuredAt: new Date().toISOString(),
2534
+ attempt: receiptAttempt }, runs: done.runs });
2535
+ } catch (error) { debug.log('release-loop.gate', 'shard-receipt-write-failed', { shard: shardId, error: String(error) }, { level: 'warn' }); }
2536
+ }
2537
+ }
2538
+ return done;
2539
+ } catch (error) {
2540
+ markShard(shard, { state: 'failed', endedAt: new Date().toISOString() }, ['waitReason']);
2541
+ throw error;
2542
+ }
2543
+ }));
2544
+ } finally { if (reconcileTimer) clearInterval(reconcileTimer); }
2150
2545
  const rejected = settled.find((result): result is PromiseRejectedResult => result.status === 'rejected');
2151
2546
  if (rejected) throw rejected.reason;
2152
2547
  const results = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
@@ -2165,6 +2560,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2165
2560
  // GATE-STALLED-TAIL-REMEASURE: after the sweep, give only a one-file no-output leaf one fresh attempt on a different
2166
2561
  // pool member. Keep the original unmeasured evidence if no alternate is available or the remeasure is incomplete.
2167
2562
  const remainingStalls: typeof stalledShards = [];
2563
+ const tailTouched = new Set<number>();
2564
+ const tailRc = new Map<number, number>();
2168
2565
  for (const stalled of stalledShards) {
2169
2566
  const alternatives = stalled.lastHost
2170
2567
  ? poolScheduler.members.filter((member) => member.context !== stalled.lastHost && !downMembers.has(member.context))
@@ -2175,6 +2572,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2175
2572
  }
2176
2573
  debug.log('release-loop.gate', 'pod-stalled-tail-remeasure-start', { shard: stalled.shard, file: stalled.files[0], fromHost: stalled.lastHost,
2177
2574
  availableHosts: alternatives.map((member) => member.context) });
2575
+ tailTouched.add(stalled.shard);
2178
2576
  try {
2179
2577
  tailPlacedHost = undefined;
2180
2578
  const tail = await runShard(stalled.files, stalled.shard, 0, '-tail', true, true, undefined, 0, false, new Set([stalled.lastHost]), true);
@@ -2187,6 +2585,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2187
2585
  fromHost: stalled.lastHost, toHost: unresolved.lastHost ?? null, reason: unresolved.reason }, { level: 'warn' });
2188
2586
  } else {
2189
2587
  measured.push(...tail.runs);
2588
+ tailRc.set(stalled.shard, tail.runs.some((run) => run.fail || run.errors) ? 1 : 0);
2190
2589
  debug.log('release-loop.gate', 'pod-stalled-tail-remeasure-complete', { shard: stalled.shard, file: stalled.files[0],
2191
2590
  fromHost: stalled.lastHost, toHost: tailPlacedHost ?? null });
2192
2591
  }
@@ -2197,6 +2596,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2197
2596
  }
2198
2597
  }
2199
2598
  stalledShards = remainingStalls;
2599
+ for (const shard of tailTouched) {
2600
+ const unresolved = stalledShards.some((item) => item.shard === shard);
2601
+ markShard(shard, { state: unresolved ? 'failed' : 'done', rc: unresolved ? 1 : tailRc.get(shard) ?? 0, endedAt: new Date().toISOString() }, ['waitReason']);
2602
+ }
2200
2603
  const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
2201
2604
  errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
2202
2605
  { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
@@ -2240,6 +2643,19 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2240
2643
  || file.split('/').some((part) => part === '.' || part === '..')))
2241
2644
  throw new Error('unsafe pod recheck files');
2242
2645
  if (!sha.test(commit)) throw new Error('invalid pod recheck commit');
2646
+ if (pod.warmCutSha === commit && !observedPrewarmCuts.has(commit)) {
2647
+ observedPrewarmCuts.add(commit);
2648
+ try {
2649
+ const pool = poolOverride ?? new PodPoolScheduler(parsePodPool(pod.pool), { kubectl });
2650
+ const snapshot = observeGatePrewarmCutShadow(
2651
+ pod.warmPrewarmInput?.cutSha === commit ? pod.warmPrewarmInput : { cutSha: commit },
2652
+ () => poolOverride && !tools.kubectl ? null : snapshotWarmPoolMembers(pool.members, kubectl), () => pool.snapshot());
2653
+ debug.log('pod.pool', snapshot.status === 'observed' ? 'gate-prewarm-shadow' : 'gate-prewarm-shadow-unmeasured',
2654
+ { cutSha: commit, ...snapshot });
2655
+ } catch (error) {
2656
+ debug.log('pod.pool', 'gate-prewarm-shadow-unmeasured', { cutSha: commit, reason: String(error) });
2657
+ }
2658
+ }
2243
2659
  const rulesBaseSha = await rulesGateBaseFor(commit, files);
2244
2660
  const installSlots = pod.installSlots ?? POD_INSTALL_SLOTS_DEFAULT;
2245
2661
  if (!Number.isSafeInteger(installSlots) || installSlots < 0) throw new Error('invalid pod recheck install slots');
@@ -2697,6 +3113,11 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2697
3113
  existsSync(join(root, 'release', opts.version, 'rehearsal', `${step}.json`)));
2698
3114
  const currentImage = !explicitPartial && opts.graphRunId && opts.pod && rehearsalStampPresent
2699
3115
  ? imageProbe(opts.pod.pool) : null;
3116
+ const receiptImage = currentImage ?? (opts.graphRunId && opts.pod && process.env.ELANOUS_GRAPH_CONTEXT
3117
+ ? imageProbe(opts.pod.pool) : null);
3118
+ const receiptBase = opts.baselineCommit ?? (opts.baselineVersion && roots.some((location) =>
3119
+ existsSync(join(location, 'release', opts.baselineVersion!, 'release.json')))
3120
+ ? baselineReleaseCommit(opts.baselineVersion) : undefined);
2700
3121
  const radius = !explicitPartial && opts.graphRunId && opts.pod && rehearsalStampPresent
2701
3122
  ? planHardRehearsalRadius({ root, repo, version: opts.version, forCommit: opts.commit,
2702
3123
  graphPins: gateGraphPins(root, opts.graphRunId), envFingerprint: gateEnvFingerprint(process.env), imageDigest: currentImage })
@@ -2704,6 +3125,14 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2704
3125
  if (radius) debug.log('release-loop.gate', 'radius-live-plan', { version: opts.version, reason: radius.reason,
2705
3126
  files: radius.plan?.files.length ?? null, rehearsalRunId: radius.rehearsalRunId ?? null });
2706
3127
  let partial = explicitPartial ?? radius?.plan ?? null;
3128
+ const explicitPodPlan = !!explicitPartial && explicitPartial.kind !== 'recheck' && !!opts.pod;
3129
+ const explicitPodDigest = explicitPodPlan ? explicitPartial!.shardImageDigests?.[0] : undefined;
3130
+ const probedPodDigest = explicitPodDigest ? imageProbe(opts.pod!.pool) : null;
3131
+ if (explicitPodPlan && (!explicitPodDigest || probedPodDigest !== explicitPodDigest)) {
3132
+ debug.log('release-loop.gate', 'partial-image-preflight-fallback', { version: opts.version,
3133
+ reason: !explicitPodDigest || !probedPodDigest ? 'executed-pod-image-digest-missing' : 'executed-pod-image-digest-mismatch' }, { level: 'warn' });
3134
+ partial = null;
3135
+ }
2707
3136
  debug.log('release-loop.gate', partial ? 'partial-regate' : 'partial-fallback', {
2708
3137
  version: opts.version, priorCommit: partial?.priorCommit ?? null, commit: opts.commit,
2709
3138
  planKind: partial?.kind ?? 'full', remeasureFileCount: partial?.files.length ?? 0,
@@ -2749,8 +3178,12 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2749
3178
  // the cut sweep puts those files first, the baseline sweep keeps its plan.
2750
3179
  riskWatch: { instanceRoots: roots, ledgerRoots: roots },
2751
3180
  // GATE-LIVE-OBS: the release ledger's shards.json (UX #24782) — never the run tree's universe; an explicit
2752
- // --ledger-root (tests) keeps it in that ledger.
2753
- shardsFile: { path: opts.ledgerRoot ? gateShardsPath(opts.version, opts.ledgerRoot) : gateShardsPath(opts.version), version: opts.version } } : undefined,
3181
+ // --ledger-root or an explicit test instance keeps it in that ledger.
3182
+ shardsFile: { path: gateShardsPath(opts.version, ledger), version: opts.version,
3183
+ ledgerPath: gateShardLedgerPath(opts.version, ledger), envFingerprint: gateEnvFingerprint(process.env),
3184
+ receiptRoot: join(ledger, 'release', opts.version, 'gate-logs', 'cut', 'receipts'),
3185
+ base40: receiptBase,
3186
+ expectedImageDigest: receiptImage } } : undefined,
2754
3187
  selected?.files);
2755
3188
  let cutRun = await runCut(partial);
2756
3189
  const radiusSelectionMatched = partial?.files && cutRun?.podSweep?.files
@@ -2762,6 +3195,12 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2762
3195
  partial = null;
2763
3196
  cutRun = await runCut(null);
2764
3197
  }
3198
+ if (explicitPodDigest && partial === explicitPartial && (!radiusSelectionMatched
3199
+ || !partialGateImagesMatch(root, opts.version, explicitPodDigest, opts.commit, cutRun?.podSweep))) {
3200
+ debug.log('release-loop.gate', 'partial-image-drift', { version: opts.version, action: 'full-sweep' }, { level: 'warn' });
3201
+ partial = null;
3202
+ cutRun = await runCut(null);
3203
+ }
2765
3204
  if (radius?.plan && partial === radius.plan) debug.log('release-loop.gate', 'baseline-consumed', {
2766
3205
  version: opts.version, baselineCommit: radius.plan.priorCommit, commit: opts.commit,
2767
3206
  selectedFiles: radius.plan.files.length, rehearsalRunId: radius.rehearsalRunId,
@@ -3282,7 +3721,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
3282
3721
  // GATE-LIVE-OBS-RC-WORDING ⓒ — the shards board says «판정 전» until this lands; then «새 실패 N» only (UX · gate-shards.ts).
3283
3722
  // Same ledger as the board's own writes (sweep's shardsFile). A write failure is logged and never changes the verdict.
3284
3723
  if (result.outcome !== 'error' && opts.pod) {
3285
- const path = opts.ledgerRoot ? gateShardsPath(opts.version, opts.ledgerRoot) : gateShardsPath(opts.version);
3724
+ const path = gateShardsPath(opts.version, ledger);
3286
3725
  try {
3287
3726
  const written = recordGateShardsVerdict(path, { introduced: result.introduced.length, preexisting: result.preexisting });
3288
3727
  debug.log('release-loop.gate', 'shards-verdict', { version: opts.version, path, written, introduced: result.introduced.length, preexisting: result.preexisting });
@@ -3347,6 +3786,12 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
3347
3786
  if (fromGraph.gateExecutor !== undefined && !isGateExecutor(fromGraph.gateExecutor)) throw new Error(`gateExecutor must be pod or host: ${String(fromGraph.gateExecutor)}`);
3348
3787
  // GATE-HOST-EXECUTOR: a host gate never reads the pod settings (a bad pod value must not block the fallback executor).
3349
3788
  const hostExecutor = fromGraph.gateExecutor === 'host';
3789
+ const graphCapabilities = fromGraph.gateShardCapabilities;
3790
+ if (!hostExecutor && graphCapabilities !== undefined && (!graphCapabilities || typeof graphCapabilities !== 'object'
3791
+ || Array.isArray(graphCapabilities) || Object.entries(graphCapabilities).some(([id, value]) =>
3792
+ !/^pod-\d+$/.test(id) || !value || typeof value !== 'object' || Array.isArray(value)
3793
+ || !['darwin', 'linux'].includes((value as { os?: string }).os ?? ''))))
3794
+ throw new Error('gateShardCapabilities must map pod-N to {os: darwin|linux}');
3350
3795
  const opts: GateOptions = {
3351
3796
  rehearsalTui: fromGraph.rehearsalTui === true,
3352
3797
  recheckFileLimit: fromGraph.gateRecheckFileLimit !== undefined ? Number(fromGraph.gateRecheckFileLimit) : undefined,
@@ -3368,11 +3813,16 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
3368
3813
  ...(fromGraph.gatePodStallSeconds !== undefined ? { stallSeconds: Number(fromGraph.gatePodStallSeconds) } : {}),
3369
3814
  ...(fromGraph.gatePodMidRunStallSeconds !== undefined ? { midRunStallSeconds: Number(fromGraph.gatePodMidRunStallSeconds) } : {}),
3370
3815
  ...(fromGraph.gatePodFreshShards === true ? { freshShards: true } : {}),
3816
+ ...(typeof fromGraph.gateRerouteShard === 'string' ? { rerouteShard: fromGraph.gateRerouteShard } : {}),
3817
+ ...(typeof fromGraph.gateShardHost === 'string' ? { shardHost: fromGraph.gateShardHost } : {}),
3818
+ ...(graphCapabilities ? { shardCapabilities: graphCapabilities as Record<string, { os: 'darwin' | 'linux' }> } : {}),
3371
3819
  ...((cpu) => cpu ? { cpu } : {})(parsePodCpu(fromGraph.gatePodCpu, 'release.loop.gatePodCpu')),
3372
3820
  } : undefined,
3373
3821
  };
3374
3822
  if (isGateExecutor(fromGraph.gateExecutor)) opts.executor = fromGraph.gateExecutor;
3375
3823
  let freshShards = false;
3824
+ let rerouteShard: string | undefined;
3825
+ let shardHost: string | undefined;
3376
3826
  for (let i = 0; i < args.length; i++) {
3377
3827
  const arg = args[i];
3378
3828
  if (arg === '--help') return 'help';
@@ -3380,6 +3830,18 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
3380
3830
  if (arg === '--rehearsal-baseline') { opts.rehearsalBaseline = true; continue; }
3381
3831
  // 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).
3382
3832
  if (arg === '--fresh-shards') { freshShards = true; continue; }
3833
+ if (arg === '--reroute-shard') {
3834
+ const value = args[++i];
3835
+ if (!value || !/^(?:auto|pod-\d+)$/.test(value)) throw new Error('--reroute-shard requires auto or pod-N');
3836
+ rerouteShard = value;
3837
+ continue;
3838
+ }
3839
+ if (arg === '--shard-host') {
3840
+ const value = args[++i];
3841
+ if (!value || value.startsWith('--')) throw new Error('--shard-host requires a pool context');
3842
+ shardHost = value;
3843
+ continue;
3844
+ }
3383
3845
  if (arg === '--rehearsal-tui') { opts.rehearsalTui = true; continue; }
3384
3846
  if (arg === '--recheck-file-limit') {
3385
3847
  const value = args[++i];
@@ -3408,6 +3870,11 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
3408
3870
  else if (key === 'remoteMirror') opts.remoteMirror = value;
3409
3871
  }
3410
3872
  if (freshShards && opts.pod) opts.pod = { ...opts.pod, freshShards: true };
3873
+ if ((rerouteShard || shardHost) && !opts.pod?.pool) throw new Error('shard reroute requires a Pod pool');
3874
+ if (shardHost && !rerouteShard) throw new Error('--shard-host requires --reroute-shard');
3875
+ if (opts.pod && rerouteShard) opts.pod = { ...opts.pod, rerouteShard, ...(shardHost ? { shardHost } : {}) };
3876
+ if (opts.pod?.rerouteShard && !/^(?:auto|pod-\d+)$/.test(opts.pod.rerouteShard)) throw new Error('gateRerouteShard must be auto or pod-N');
3877
+ if (opts.pod?.shardHost && !opts.pod.rerouteShard) throw new Error('gateShardHost requires gateRerouteShard');
3411
3878
  // GATE-HOST-EXECUTOR: the host executor is the existing non-pod sweep on gateRemote — a configured pool must not pull it
3412
3879
  // back into Pods, and without a host it would silently sweep on the runner's own machine.
3413
3880
  if (opts.executor === 'host') {
@@ -3440,7 +3907,7 @@ export async function runGateNode(argv: string[], env: NodeJS.ProcessEnv, runner
3440
3907
  debug.log('release-loop.gate', 'executor', { version: opts.version, ...placement, requested: opts.executor ?? null });
3441
3908
  }
3442
3909
  if (opts === 'help') {
3443
- 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>] [--recheck-file-limit <n>] [--fresh-shards] [--executor <pod|host>] [--rehearsal-baseline] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.\n--executor host (input.gateExecutor) ignores every pod option and sweeps on --remote (input.gateRemote, required).\n--rehearsal-baseline pins source graph bytes only for a direct Pod gate on an isolated rc version.');
3910
+ 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>] [--recheck-file-limit <n>] [--fresh-shards] [--reroute-shard <pod-N|auto>] [--shard-host <pool-context>] [--executor <pod|host>] [--rehearsal-baseline] [--json]\nWithout flags, input.commit, input.version and input.previousVersion come from the JSON file at ELANOUS_GRAPH_CONTEXT.\n--executor host (input.gateExecutor) ignores every pod option and sweeps on --remote (input.gateRemote, required).\n--rehearsal-baseline pins source graph bytes only for a direct Pod gate on an isolated rc version.');
3444
3911
  return 0;
3445
3912
  }
3446
3913
  version = opts.version;