elanous 0.2.38 → 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 (615) 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/{RrjXTwz-urRAbChP1bX02 → HjeK7zONHfR7op1hKXp4k}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{7954.4f7a8832663ee979.js → 1060.014f1f01a17aec26.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/{8911.bf5e712acb25fb6c.js → 1281.d81877e35b9068f4.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/{2104.70e086d5174a4736.js → 1350.59f40e39e02d91b1.js} +1 -1
  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/2047.8f23e210995d4548.js +2 -0
  16. package/apps/pwa/out/_next/static/chunks/2061-7f2bbc53ae96c88b.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/{1632.7d87f998e714bede.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/{8019.21ca554a821dad01.js → 2177.df8547f9d7215c4e.js} +3 -3
  20. package/apps/pwa/out/_next/static/chunks/2200-9a453b8f99687d49.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/{3528.e4b8ab70d339f965.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/{6493.27ad77de7aac7966.js → 2499.02fbfd18126c10c0.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/{7628.6b16863ac3972333.js → 2522.f2766b76d22c8bdf.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/{3086.fb14b60ce98b53d3.js → 2566.beaffc3a5e1fcd2e.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/{9299.24e2a7d9b6d1f91b.js → 261.0d471e8aded799f3.js} +3 -3
  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/2774-719820e3f5b2a3eb.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/2812-f016aa0dda149287.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/2829-734fe8bf2eee7347.js +1 -0
  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/{7524.ce973525d30b3d52.js → 3330.7443183b7e6fdbdf.js} +1 -1
  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/{3124-561f7f037943cde2.js → 3746-f80a78f220bf37b5.js} +1 -1
  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/{bb6987f9.3647e1a23b0e3217.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/{8994d93e.33ba491f95345fc2.js → 409c787b.e48c5bec8ec10456.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/{5041-5aada92ca215ffdd.js → 413-24c8284bda08241d.js} +1 -1
  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/{5638.107314dc479e2f37.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/{8487.291666583bebe640.js → 4769.9fd582071301f382.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/{8509.3a54d4e61893d7cb.js → 483.725e4a61be922788.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/{9fe1b809.738da85c8b17e3fb.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/{8879.ac06755bcf3e44a1.js → 5383.bdfa5d886e39a826.js} +1 -1
  64. package/apps/pwa/out/_next/static/chunks/{6142.913b44e260a3f925.js → 5436.fdf6fa8ae3e9588d.js} +1 -1
  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/{7161-8f22db3ec5ed005e.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/{3291.f6c1005387d514bd.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/{5203.51e5d8b5f1dcfe53.js → 6749.0744ce6eb08d40e9.js} +3 -3
  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/{1938.0e6c60ee91c4d7f5.js → 6908.06008c67f6380ea9.js} +2 -2
  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/{de8b930d.d94c0f42acf807fc.js → 6cc1d1d6.9a98dd4622f1afc6.js} +1 -1
  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/{1026-44010f405704050c.js → 8272-025653884f9645d2.js} +1 -1
  109. package/apps/pwa/out/_next/static/chunks/{5307.d7328020227880c2.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/{4199-f7c6eb40cd0322f8.js → 8639-92c11d069792429e.js} +1 -1
  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/9175.2de5209a5ce6f68a.js +1 -0
  117. package/apps/pwa/out/_next/static/chunks/9184.45afbf44fec87574.js +1 -0
  118. package/apps/pwa/out/_next/static/chunks/{eaad0cba.771200940c529df7.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-866aac5ff226a7f3.js → page-ba957ff875a026a0.js} +1 -1
  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-4edb8df90f4537ce.js +1 -0
  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-fca2e59a81744d9e.js +1 -0
  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-139d722de1dcd6c1.js +1 -0
  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-ccde99f07aaf71f7.js +1 -0
  173. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-80464aced9614390.js +1 -0
  174. package/apps/pwa/out/_next/static/chunks/{b7478fef.5917716286213fcc.js → b84cad00.594027ebe0531fe2.js} +1 -1
  175. package/apps/pwa/out/_next/static/chunks/c696986b.5dc8bb1143af16d1.js +1 -0
  176. package/apps/pwa/out/_next/static/chunks/{b7459770.6539b11860eb8a9c.js → ca3702f8.2913182e73889fa2.js} +7 -7
  177. package/apps/pwa/out/_next/static/chunks/{3f0de3de-2340c532c00395db.js → d5d3cefc-373c4eb722ed848b.js} +1 -1
  178. package/apps/pwa/out/_next/static/chunks/{89d6c04f-6b8ab341748e468a.js → d8de0de2-dd0f4ac8e5ab1832.js} +1 -1
  179. package/apps/pwa/out/_next/static/chunks/{framework-4b12a946f352cee2.js → framework-002b472a66935d31.js} +1 -1
  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/video/explainer-line.yaml +4 -1
  282. package/package.json +1 -1
  283. package/scripts/dead-code/report.ts +169 -0
  284. package/scripts/lib/morning-report-options.ts +3 -4
  285. package/scripts/lib/tui-regress-evidence.ts +56 -0
  286. package/scripts/morning-report.ts +5 -1
  287. package/scripts/public-export.ts +15 -7
  288. package/scripts/release-loop/export-check-node.ts +21 -3
  289. package/scripts/release-loop/gate-node.ts +379 -48
  290. package/scripts/release-loop/gate-shard-dispatch.ts +128 -0
  291. package/scripts/release-loop/gate-shard-receipts.ts +89 -0
  292. package/scripts/release-loop/gate-wall.ts +5 -5
  293. package/scripts/release-loop/preflight.ts +6 -2
  294. package/scripts/release-loop/resume-release.ts +20 -5
  295. package/scripts/release-loop/resume.ts +48 -0
  296. package/scripts/release-loop/version-node.ts +7 -2
  297. package/scripts/release-story/draft.ts +1 -1
  298. package/scripts/research-bench/README.md +53 -0
  299. package/scripts/research-bench/questions.json +66 -0
  300. package/scripts/research-bench/rubric.md +33 -0
  301. package/scripts/research-bench/run.ts +523 -0
  302. package/scripts/tui-regress.ts +85 -10
  303. package/src/acp/core-turn-bridge.ts +8 -0
  304. package/src/agent/autonomous-tools.ts +10 -3
  305. package/src/agent/monad-agent-turn.ts +4 -1
  306. package/src/agent/registry.ts +26 -2
  307. package/src/agent/shared-app-tools.ts +1 -1
  308. package/src/agent/surface-ux/build.ts +12 -3
  309. package/src/agent/surface-ux/types.ts +1 -1
  310. package/src/agent/task-notification.ts +75 -6
  311. package/src/agent/types.ts +3 -0
  312. package/src/agent-mission/driver.ts +29 -3
  313. package/src/agent-mission/mission-cli.ts +2 -1
  314. package/src/agent-substrate/review-context-pack.ts +365 -0
  315. package/src/agent-substrate/self-review-cli.ts +226 -17
  316. package/src/ask-user-question/index.ts +6 -0
  317. package/src/ask-user-question/inline-resolver.ts +49 -0
  318. package/src/auto-research/question-ladder-contract.ts +22 -3
  319. package/src/auto-research/research-turn.ts +350 -17
  320. package/src/autopilot/agent-loop.ts +1 -1
  321. package/src/autopilot/pty-control-loop.ts +8 -1
  322. package/src/boot/daemon-tools/index.ts +4 -3
  323. package/src/boot/daemon-tools/self-implement.ts +270 -2
  324. package/src/boot/daemon-tools/types.ts +2 -0
  325. package/src/budget/llm-cost.ts +44 -5
  326. package/src/budget/unified-usage.ts +17 -1
  327. package/src/capture/tui-self-report.ts +14 -0
  328. package/src/chat/index.ts +33 -7
  329. package/src/cli/agent-cli.ts +43 -8
  330. package/src/cli/intake-cli.ts +22 -1
  331. package/src/cli/lesson-cli.ts +19 -3
  332. package/src/cli/log-event-names.ts +1 -0
  333. package/src/cli/pod-cli.ts +17 -2
  334. package/src/cli/pty-takeover-cli.ts +67 -7
  335. package/src/cli/release-cli.ts +52 -6
  336. package/src/cli/repo-cli.ts +2 -1
  337. package/src/cli/tasks-cli.ts +36 -9
  338. package/src/control-plane/ledger.ts +21 -4
  339. package/src/core-turn/run-core-turn.ts +44 -3
  340. package/src/core-turn/run-goal-loop.ts +152 -8
  341. package/src/core-turn/types.ts +9 -0
  342. package/src/dashboard/chat/acp-chat.ts +2 -2
  343. package/src/dashboard/classic-sigquit.ts +16 -0
  344. package/src/dashboard/index.ts +321 -27
  345. package/src/dashboard/input/chat-main-interaction-opts.ts +3 -0
  346. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +7 -2
  347. package/src/dashboard/input/chat-main-slash-command.ts +7 -0
  348. package/src/dashboard/input/dashboard-plain-turn-stream.ts +4 -0
  349. package/src/dashboard/input/dashboard-priority-key-route.ts +21 -5
  350. package/src/dashboard/input/external-input-host.ts +2 -0
  351. package/src/dashboard/sidetab-runtime.ts +228 -0
  352. package/src/dashboard/slash-runtime/ask-clarification.ts +7 -1
  353. package/src/dashboard/slash-runtime/dashboard-handlers.ts +20 -8
  354. package/src/dashboard/slash-runtime/help-from-registry.ts +2 -2
  355. package/src/dashboard/term-slash-runtime.ts +16 -18
  356. package/src/dashboard/turn-preamble.ts +2 -0
  357. package/src/domains/knowledge.ts +1 -1
  358. package/src/esc-abort-gate.ts +33 -1
  359. package/src/execution-loop/launch-gate.ts +206 -1
  360. package/src/graph-runner/graph-cli.ts +6 -4
  361. package/src/graph-runner/runner.ts +31 -9
  362. package/src/graph-wizard/generate.ts +16 -7
  363. package/src/graph-wizard/mission-compose.ts +201 -0
  364. package/src/grok/agent-search.ts +8 -2
  365. package/src/grok/credential.ts +5 -0
  366. package/src/grounding/ref-index.ts +11 -0
  367. package/src/grounding/ref-vector.ts +61 -0
  368. package/src/grounding/sources-cli.ts +2 -2
  369. package/src/grounding/sources.ts +15 -1
  370. package/src/harness/harness-cli-command.ts +38 -9
  371. package/src/harness/harness-pod-dispatch.ts +7 -1
  372. package/src/index.ts +18 -4
  373. package/src/input/external-input-queue.ts +10 -0
  374. package/src/intake-plane/collect-telegram-saved.ts +36 -1
  375. package/src/intake-plane/intake-to-tasks.ts +246 -3
  376. package/src/lessons/friction-ledger.ts +154 -0
  377. package/src/live-supply/collector.ts +149 -0
  378. package/src/live-supply/probes.ts +52 -0
  379. package/src/live-supply/snapshot.ts +40 -0
  380. package/src/llm.ts +431 -76
  381. package/src/loops/orchestrator/tick.ts +8 -0
  382. package/src/nexus/api/harness-api.ts +2 -1
  383. package/src/pty/snapshot.py +132 -0
  384. package/src/pty-shell/pty-control-ipc.ts +29 -2
  385. package/src/pty-shell/pty-manifest.ts +59 -3
  386. package/src/pty-shell/registry.ts +2 -0
  387. package/src/pty-shell/tui-run-state.ts +80 -3
  388. package/src/release-loop/gate-host-reservation.ts +13 -4
  389. package/src/release-loop/gate-shard-ledger.ts +68 -0
  390. package/src/release-loop/merged-pr-checklist.ts +25 -2
  391. package/src/release-loop/rehearsal-shard-plan.ts +6 -1
  392. package/src/release-loop/rehearsal.ts +27 -0
  393. package/src/research/edge-decision.ts +124 -15
  394. package/src/research/gap-followup.ts +249 -0
  395. package/src/research/lenses/counter-evidence.ts +100 -0
  396. package/src/research/lenses/working-backwards.ts +89 -0
  397. package/src/research/plugin-discovery.ts +242 -0
  398. package/src/research-bridge/invoke.ts +4 -2
  399. package/src/research-bridge/types.ts +2 -0
  400. package/src/self-dev/ask-launch-flow.ts +34 -106
  401. package/src/self-dev/dev-cli.ts +2 -0
  402. package/src/self-dev/dev-pipeline.ts +8 -4
  403. package/src/self-dev/graph-journey-route.ts +10 -2
  404. package/src/self-dev/launch-preflight-text.ts +2 -2
  405. package/src/self-dev/launch-preflight.ts +38 -7
  406. package/src/self-dev/orchestrate-cli.ts +1 -0
  407. package/src/self-dev/run-supervisor.ts +19 -0
  408. package/src/self-implement/device-first.ts +8 -2
  409. package/src/self-implement/doctor-visit.ts +76 -58
  410. package/src/self-implement/gate-artifacts.ts +3 -0
  411. package/src/self-implement/gate-baseline.ts +75 -4
  412. package/src/self-implement/gate-cli.ts +317 -9
  413. package/src/self-implement/gate-receipt-registry.ts +180 -0
  414. package/src/self-implement/gate-remote.ts +145 -22
  415. package/src/self-implement/goal-author-clarification.ts +178 -24
  416. package/src/self-implement/goal-author-decompose.ts +24 -15
  417. package/src/self-implement/goal-author.ts +9 -5
  418. package/src/self-implement/host-regate.ts +120 -45
  419. package/src/self-implement/impl-findings-feedback.ts +188 -0
  420. package/src/self-implement/knowhow-pack.ts +189 -10
  421. package/src/self-implement/off-diff-evidence.ts +124 -0
  422. package/src/self-implement/orchestrator.ts +85 -7
  423. package/src/self-implement/run-ledger.ts +46 -8
  424. package/src/self-implement/run-stop.ts +12 -0
  425. package/src/self-implement/seams.ts +6 -3
  426. package/src/self-implement/self-implement-cli.ts +4 -2
  427. package/src/session/chat.ts +89 -15
  428. package/src/session-runtime/provider-circuit.ts +140 -0
  429. package/src/shell-runner/file-engine.ts +1 -0
  430. package/src/shell-runner/types.ts +4 -0
  431. package/src/sidetab-core/model.ts +176 -0
  432. package/src/skills/tools/schedule-wakeup.ts +23 -1
  433. package/src/skills/tools/shell.ts +51 -5
  434. package/src/steward/directive-audit.ts +354 -0
  435. package/src/surface/child-process.ts +31 -0
  436. package/src/surface/tui-stderr-owner.ts +48 -0
  437. package/src/task-agent/cover.ts +30 -12
  438. package/src/task-agent/judge.ts +56 -0
  439. package/src/task-agent/postland-card.ts +35 -2
  440. package/src/task-agent/rejudge-on-head.ts +46 -2
  441. package/src/task-agent/repair-cycle.ts +1 -1
  442. package/src/task-agent/task-hand.ts +129 -24
  443. package/src/task-agent/tier-split.ts +105 -21
  444. package/src/task-orchestrator/surfaces/pod-command-job.ts +330 -2
  445. package/src/task-orchestrator/surfaces/pod-config-snapshot.ts +123 -0
  446. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +27 -2
  447. package/src/task-orchestrator/surfaces/pod-mirror-seed.ts +11 -8
  448. package/src/task-orchestrator/surfaces/pod-si-warm-protocol.ts +1 -1
  449. package/src/task-orchestrator/surfaces/pod-source-receive.ts +74 -6
  450. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +2 -1
  451. package/src/task-orchestrator/surfaces/pod-warm-worker-provider.ts +56 -15
  452. package/src/task-orchestrator/surfaces/self-implement-pod.ts +186 -33
  453. package/src/tool-runtime/skill-exec-runtime.ts +95 -5
  454. package/src/tui/entry.ts +15 -2
  455. package/src/tui/inline-choice.ts +188 -0
  456. package/src/tui/sidetab/list.ts +2 -176
  457. package/src/tui/slash/term.ts +12 -1
  458. package/src/tui-client/dashboard-session.ts +7 -1
  459. package/src/user-config.ts +34 -4
  460. package/src/ux-sim/classic-tui-seat-scenarios.ts +1 -7
  461. package/src/ux-sim/fake-land-queue.ts +88 -0
  462. package/src/ux-sim/gate-shard-host-reroute.ts +82 -0
  463. package/src/ux-sim/goal-interim-trace.ts +24 -0
  464. package/src/ux-sim/next-tui-seat-scenarios.ts +8 -13
  465. package/src/ux-sim/task-orchestrator-surfaces-self-implement-pod-device.ts +4 -1
  466. package/src/version/packed-revision.json +1 -1
  467. package/src/video-pipeline/recipes/explainer.ts +54 -2
  468. package/src/web-search/grok.ts +32 -6
  469. package/src/web-search/index.ts +8 -7
  470. package/src/workflow-runtime/node-catalog.ts +53 -4
  471. package/apps/pwa/out/_next/static/chunks/1118.a8a74ea29aac3e46.js +0 -27
  472. package/apps/pwa/out/_next/static/chunks/1204-2aa830479adf80f4.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/1341-43b7d2dc77d45c9d.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/1440-c3ea8882badfd9bf.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/1698-9c11cbf97f13717e.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/1770-3b1fc92253ef9f2f.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/1923-5c1695332240246a.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/213-20675f20673ba22a.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/214.186a430da28338f1.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/2277.3bd4c0ac7f1b5996.js +0 -93
  481. package/apps/pwa/out/_next/static/chunks/240.e09f433f71c78ff4.js +0 -104
  482. package/apps/pwa/out/_next/static/chunks/2415.738b777642a02233.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/2417.02151393605a7cd0.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/2463.cd2641a76daaa9ff.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/2576.b9e9c629c4b84c52.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/265-c021de53f0865bd9.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/2998-b1cc13956cd6a6e7.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/2e4782d3.ba2f0ab2b63a53e7.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/3130.626b54382b4dc379.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/3221-1bb3f99a55088613.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/3280.24edb1ffd03a7f3f.js +0 -24
  492. package/apps/pwa/out/_next/static/chunks/3288-eb3c22a1cbf2ae68.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/33.ef99e8374f96c6c4.js +0 -36
  494. package/apps/pwa/out/_next/static/chunks/3341-f09b9efbd7a82f70.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/3389.012c70767a872f9b.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/3410-d7c0f14b329d6fda.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/3421.c87d6313a7d670f7.js +0 -287
  498. package/apps/pwa/out/_next/static/chunks/3422-83a3953f9bd67e26.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/3587-002e24e368c515a9.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/3644.f12a7d821ab940af.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/3660.d7516588e2d875c3.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/3695.b82e2610f4cd4ea9.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/3858.ca1f5bc0f577eac9.js +0 -94
  504. package/apps/pwa/out/_next/static/chunks/3959-e6ac4a139d9d4bcc.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/4032.53f62176514ec0ea.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/4204-ef8484cbecd5d62f.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/4259-163129ca79913c4b.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/428-eaa77df3d0c85969.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/4432-97e400ffa1e161f5.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/449-aa427cf2cbb75f50.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/4491-39a0fd32bbaf3840.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/4588.b7cf06d382953979.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/4690.70ec8060825366ab.js +0 -32
  514. package/apps/pwa/out/_next/static/chunks/4804.6012b5cd33c22792.js +0 -90
  515. package/apps/pwa/out/_next/static/chunks/4881.e4d22d730e3b9df5.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/4937.ce2e340dc29cad3e.js +0 -79
  517. package/apps/pwa/out/_next/static/chunks/5-bb1c08d84bd71df3.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/5005.de98df23b37cdf3c.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/5035.dc64a89cad5573cc.js +0 -74
  520. package/apps/pwa/out/_next/static/chunks/5207.1a224035455790fd.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/5255-1b60d50eabe9f057.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/5269.d0436ee6d773382a.js +0 -286
  523. package/apps/pwa/out/_next/static/chunks/5300.677c97e0393742e6.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/5309.3367c4d50f52b489.js +0 -1
  525. package/apps/pwa/out/_next/static/chunks/552-689827d49780fa00.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/5539.34cc9d60622cb69b.js +0 -8
  527. package/apps/pwa/out/_next/static/chunks/5568.218dea0c00ca2ec7.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/5645.b7e3b055455a04ea.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/5662.18cc0f30353cbd96.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/5671.da24fc9dc5e98325.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/5b141171.324c40fd49cb6821.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/6175-d4621f6a10b0cfab.js +0 -1
  533. package/apps/pwa/out/_next/static/chunks/6290.3eb53a20746b72ce.js +0 -1
  534. package/apps/pwa/out/_next/static/chunks/6415-9307f3eafa90837f.js +0 -1
  535. package/apps/pwa/out/_next/static/chunks/6579-3de35d00aacd71b5.js +0 -1
  536. package/apps/pwa/out/_next/static/chunks/680-2f324c4e039a5b67.js +0 -1
  537. package/apps/pwa/out/_next/static/chunks/6845-331b3c7481da3ba3.js +0 -1
  538. package/apps/pwa/out/_next/static/chunks/7000.28157393caaf5343.js +0 -62
  539. package/apps/pwa/out/_next/static/chunks/7311-60a7ea80d23c7521.js +0 -1
  540. package/apps/pwa/out/_next/static/chunks/7401.525d0a12f7d02c06.js +0 -1
  541. package/apps/pwa/out/_next/static/chunks/7438.c2cf72b66dca6aa4.js +0 -1
  542. package/apps/pwa/out/_next/static/chunks/7488.966b116789f3d41a.js +0 -1
  543. package/apps/pwa/out/_next/static/chunks/7573.2376e13c9c21f1f8.js +0 -77
  544. package/apps/pwa/out/_next/static/chunks/7577.ea6e9ac72e035520.js +0 -1
  545. package/apps/pwa/out/_next/static/chunks/7657.8d1fb3d2f7ebe273.js +0 -1
  546. package/apps/pwa/out/_next/static/chunks/7903-1435d5d2f0dd7807.js +0 -1
  547. package/apps/pwa/out/_next/static/chunks/7978-30a91192c5dbe3c2.js +0 -1
  548. package/apps/pwa/out/_next/static/chunks/8025-8795821525cf1a07.js +0 -1
  549. package/apps/pwa/out/_next/static/chunks/8191.3674a00f0b565c23.js +0 -1
  550. package/apps/pwa/out/_next/static/chunks/8217-604955d8885c5ef2.js +0 -1
  551. package/apps/pwa/out/_next/static/chunks/8298-eed6f5f2585a63f4.js +0 -1
  552. package/apps/pwa/out/_next/static/chunks/8758.ac421f726a189125.js +0 -1
  553. package/apps/pwa/out/_next/static/chunks/891.d6c723a51a067b36.js +0 -149
  554. package/apps/pwa/out/_next/static/chunks/9223.cbcc88e0e9950426.js +0 -41
  555. package/apps/pwa/out/_next/static/chunks/9317.6a9869039c231bb3.js +0 -24
  556. package/apps/pwa/out/_next/static/chunks/9383.e4755f275b3c11ff.js +0 -1
  557. package/apps/pwa/out/_next/static/chunks/9581.da63ddf41acfad46.js +0 -1
  558. package/apps/pwa/out/_next/static/chunks/9715.8c5529a0a5c41a36.js +0 -1
  559. package/apps/pwa/out/_next/static/chunks/9817.3009d65f0889ef1f.js +0 -2
  560. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-76873dd80da4f715.js +0 -1
  561. package/apps/pwa/out/_next/static/chunks/app/approvals/page-ed32b6ec5dea8f23.js +0 -1
  562. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-fdd40e15babf3d25.js +0 -1
  563. package/apps/pwa/out/_next/static/chunks/app/board/page-384efc78e85e400b.js +0 -1
  564. package/apps/pwa/out/_next/static/chunks/app/bots/page-d91809c5aeed319a.js +0 -1
  565. package/apps/pwa/out/_next/static/chunks/app/ceo/page-128f3522f89a81ab.js +0 -1
  566. package/apps/pwa/out/_next/static/chunks/app/chat/page-595961abf3b43ad1.js +0 -1
  567. package/apps/pwa/out/_next/static/chunks/app/consult/page-f41dccb76d8c0263.js +0 -1
  568. package/apps/pwa/out/_next/static/chunks/app/control/page-a3205c2e28b7d0d9.js +0 -1
  569. package/apps/pwa/out/_next/static/chunks/app/decisions/page-c39a80277814a3f4.js +0 -1
  570. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1ab3f98194513195.js +0 -1
  571. package/apps/pwa/out/_next/static/chunks/app/editor/page-a5246f7e254d77d0.js +0 -1
  572. package/apps/pwa/out/_next/static/chunks/app/exec/page-0f92287bdb81217d.js +0 -1
  573. package/apps/pwa/out/_next/static/chunks/app/field/page-5a37131ff63793ef.js +0 -1
  574. package/apps/pwa/out/_next/static/chunks/app/healthz/route-76873dd80da4f715.js +0 -1
  575. package/apps/pwa/out/_next/static/chunks/app/inside/page-ac4bd14bb2348bfd.js +0 -1
  576. package/apps/pwa/out/_next/static/chunks/app/intake/page-c348a3959c5b5fc6.js +0 -1
  577. package/apps/pwa/out/_next/static/chunks/app/layout-2addd81c0d3e2906.js +0 -1
  578. package/apps/pwa/out/_next/static/chunks/app/live/page-da6b765f58a3eefb.js +0 -1
  579. package/apps/pwa/out/_next/static/chunks/app/live-run/page-0661e40f8780c32c.js +0 -1
  580. package/apps/pwa/out/_next/static/chunks/app/loops/page-b89af1b1c8e4875b.js +0 -1
  581. package/apps/pwa/out/_next/static/chunks/app/market/page-da8aeb82ba751cdb.js +0 -1
  582. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-cc71317b239439db.js +0 -1
  583. package/apps/pwa/out/_next/static/chunks/app/morning/page-bde7fc1d64ffe369.js +0 -1
  584. package/apps/pwa/out/_next/static/chunks/app/not-found-e2797214f713fa58.js +0 -1
  585. package/apps/pwa/out/_next/static/chunks/app/observatory/page-2a35bcda70defaf6.js +0 -1
  586. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-3af03deedde45032.js +0 -1
  587. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d1d1210f99f620ae.js +0 -1
  588. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-46d1b5500885861d.js +0 -1
  589. package/apps/pwa/out/_next/static/chunks/app/outputs/page-291ab19fe2acb173.js +0 -1
  590. package/apps/pwa/out/_next/static/chunks/app/page-7f569e533ddeecba.js +0 -1
  591. package/apps/pwa/out/_next/static/chunks/app/reflection/page-990129c9fee46d8f.js +0 -1
  592. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-7737f785ebb1cdaf.js +0 -1
  593. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f3c73a4f7ffd2f8.js +0 -1
  594. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-03be7fb347f01058.js +0 -1
  595. package/apps/pwa/out/_next/static/chunks/app/settings/page-96b0280625e4df2b.js +0 -1
  596. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-56855545049eb56d.js +0 -1
  597. package/apps/pwa/out/_next/static/chunks/app/setup/layout-76873dd80da4f715.js +0 -1
  598. package/apps/pwa/out/_next/static/chunks/app/setup/page-79a909c9981f0338.js +0 -1
  599. package/apps/pwa/out/_next/static/chunks/app/share/page-a9d9a3e66459c1e0.js +0 -1
  600. package/apps/pwa/out/_next/static/chunks/app/showroom/page-c383cbf2013f7069.js +0 -1
  601. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f57c6a2e54dbdca4.js +0 -1
  602. package/apps/pwa/out/_next/static/chunks/app/term/page-6b140f77593576eb.js +0 -1
  603. package/apps/pwa/out/_next/static/chunks/app/today/page-6b767ae2d9d0adfc.js +0 -1
  604. package/apps/pwa/out/_next/static/chunks/app/trace/page-116d6198d5da0e3d.js +0 -1
  605. package/apps/pwa/out/_next/static/chunks/app/vault/page-b9c9b778c7501370.js +0 -1
  606. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-a6c78392ab8f6335.js +0 -1
  607. package/apps/pwa/out/_next/static/chunks/app/workflows/page-455183deb4ed0cfe.js +0 -1
  608. package/apps/pwa/out/_next/static/chunks/app/workspace/page-6a63e99526bd63d0.js +0 -1
  609. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31c22caafe788c08.js +0 -1
  610. package/apps/pwa/out/_next/static/chunks/main-941155250ed2644b.js +0 -1
  611. package/apps/pwa/out/_next/static/chunks/main-app-f8b6765abed6d149.js +0 -1
  612. package/apps/pwa/out/_next/static/chunks/pages/_app-78432194c85388f9.js +0 -1
  613. package/apps/pwa/out/_next/static/chunks/pages/_error-cdf7390829512b1b.js +0 -1
  614. package/apps/pwa/out/_next/static/chunks/webpack-d5f0964a9c2e92d2.js +0 -1
  615. /package/apps/pwa/out/_next/static/{RrjXTwz-urRAbChP1bX02 → HjeK7zONHfR7op1hKXp4k}/_ssgManifest.js +0 -0
@@ -43,6 +43,9 @@ import { resolveWarmPoolProfile } from '../../src/self-implement/pod-warm-pool-p
43
43
  import { decideWarmReuse, type WarmReuseSwitchConfig, type WarmReuseEvidence } from '../../src/task-orchestrator/surfaces/pod-warm-switch.js';
44
44
  import { getUserConfig } from '../../src/user-config.js';
45
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';
46
49
  import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
47
50
  import { executeTuiNodeCheck, type ExecutedTuiCheck } from './tui-node-checks.js';
48
51
  import { runWithReleaseGateTraffic } from './gate-traffic-mode.js';
@@ -218,6 +221,12 @@ export interface GateOptions {
218
221
  recheckFileLimit?: number;
219
222
  /** freshShards — GATE-SHARD-RESUME off: every shard starts a Pod even when this logDir already holds its result. */
220
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' }>>;
221
230
  /** Opt-in gate shadow: provider fills the fenced lease identity. The Job remains authoritative. */
222
231
  warmReplay?: (input: Omit<WarmWorkerCommand, 'leaseId' | 'generation' | 'workerId' | 'key'>) => Promise<{ command: WarmWorkerCommand | null; transport: WarmPodTransportResult }>;
223
232
  warmQuarantine?: (command: WarmWorkerCommand, reason: string) => Promise<void>;
@@ -253,8 +262,8 @@ export interface GateOptions {
253
262
  hostMirrorReaders?: GateHostMirrorReaders;
254
263
  /** GATE-INSTALL-CACHE — concurrent `bun install`s per node (`release.loop.gatePodInstallSlots`); omitted = 4 · 0 = off. */
255
264
  installSlots?: number;
256
- /** GATE-MEM-ADMIT — seconds a shard waits for pool admission (memory headroom) before it launches anyway with
257
- * `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. */
258
267
  admissionWaitSeconds?: number;
259
268
  /** GATE-MEM-ADMIT — an injected admission (tests · embedders); config never sets it. Absent = the gate's own pool's. */
260
269
  admission?: GateAdmission;
@@ -279,7 +288,8 @@ export interface GateOptions {
279
288
  * of {@link gateShardAbortScript} on the gate's own pool; an injected pool without it runs without the mid-run watch. */
280
289
  abortTests?: (job: { name: string; context?: string }, reason: string) => boolean | void;
281
290
  /** GATE-LIVE-OBS — the shard state file (`gateShardsPath(version)`) this sweep keeps current; absent = not written. */
282
- shardsFile?: { path: string; version: string };
291
+ shardsFile?: { path: string; version: string; ledgerPath?: string; envFingerprint?: string;
292
+ receiptRoot?: string; base40?: string; expectedImageDigest?: string | null };
283
293
  /** GATE-RISK-LANE — the ledgers `gateHistory` reads its watch list from; absent = no risk lane (the sweep plans as before). */
284
294
  riskWatch?: { instanceRoots: readonly string[]; ledgerRoots: readonly string[] };
285
295
  /** GATE-MULTI-HOST — is this pool member reachable (kubectl ns ⊕ ssh)? Injected by tests; absent = the real probe on
@@ -523,6 +533,31 @@ const lastMatch = (output: string, pattern: RegExp): RegExpExecArray | null => {
523
533
  return last;
524
534
  };
525
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
+ }
526
561
  /** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
527
562
  const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
528
563
  /** A Pod path (`~/repo/src/…` · `../..~/repo/src/…`) as a repository-relative one. */
@@ -1045,20 +1080,29 @@ export function gateShardAbortScript(reason: string, graceSeconds = GATE_SHARD_A
1045
1080
  return [
1046
1081
  `reason=${quote(reason.replace(/[^\w .:-]/g, '_'))}`,
1047
1082
  `only=${quote(outbox ?? '')}`,
1048
- "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}'; }",
1049
1087
  'root_pg=$(pg_of 1); my_pg=$(pg_of $$); found=0',
1050
1088
  'for d in /proc/[0-9]*; do',
1051
1089
  ' pid=${d#/proc/}',
1052
1090
  " out=$(tr '\\0' '\\n' < \"$d/cmdline\" 2>/dev/null | grep -m1 -E '^--reporter-outfile=.*/outbox/part-[0-9]+\\.junit\\.xml$') || continue",
1053
1091
  ' if [ -n "$only" ]; then case "$out" in "--reporter-outfile=$only/part-"*) ;; *) continue;; esac; fi',
1054
- ' pg=$(pg_of "$pid")',
1055
- ' 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',
1056
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',
1057
1099
  ' out=${out#--reporter-outfile=}; dir=${out%/part-*}; part=${out##*/part-}; part=${part%.junit.xml}',
1058
- ' printf "%s\\n" "$reason" > "$dir/part-$part.abort"',
1059
- ' [ -f "$dir/shard.timeout" ] || echo "$part" > "$dir/shard.timeout"',
1060
- ' if kill -TERM -- "-$pg" 2>/dev/null; then found=$((found + 1)); echo "ELANOUS_GATE_ABORT part=$part pgid=$pg"; fi',
1061
- ` ( 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',
1062
1106
  'done',
1063
1107
  'echo "ELANOUS_GATE_ABORT signalled=$found"',
1064
1108
  '[ "$found" -gt 0 ]',
@@ -1190,8 +1234,8 @@ const lastStartedTestFile = (output: string, paths: string[]): string | undefine
1190
1234
  * split shape follows from depth and size alone, so the same plan reaches the same child names (TC 09:30 review ②).
1191
1235
  * `runKey` hashes where and how the Pod ran the shard (pool · command · ignores · memory · deadline · Bun cache): a fix on the Pod side
1192
1236
  * makes every old result a miss instead of re-reading an old failure as introduced (TC 09:48 review). */
1193
- export function previousShardDescent(logDir: string, name: string, commit: string, files: readonly string[], runKey: string, parentAttempt?: string, splitChild?: string): { way: 'retry' | 'split'; attemptId: string } | undefined {
1194
- 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 => {
1195
1239
  try { return JSON.parse(readFileSync(join(logDir, `${file}.json`), 'utf8')); } catch { return undefined; }
1196
1240
  };
1197
1241
  const own = meta(name);
@@ -1208,6 +1252,8 @@ export function previousShardDescent(logDir: string, name: string, commit: strin
1208
1252
  && (!whole || row.files.length === files.length);
1209
1253
  };
1210
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 };
1211
1257
  // Only the first child of the split the caller takes now (`-file-0` · `-c0` · `-0` · review r3): a record of another
1212
1258
  // split shape is not followed.
1213
1259
  return splitChild !== undefined && child(`${name}${splitChild}`, false) ? { way: 'split', attemptId } : undefined;
@@ -1239,9 +1285,11 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
1239
1285
  const output = stripPodRepoRoot(run.output.replace(/\x1b\[[0-9;]*[A-Za-z]/g, ''));
1240
1286
  const ran = lastMatch(output, /Ran (\d+) tests? across ([1-9]\d*) files?/);
1241
1287
  const reportedErrors = summaryCount(output, 'error');
1242
- const errorCount = Number.isNaN(reportedErrors) ? 0 : reportedErrors;
1243
- 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)}`);
1244
- 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');
1245
1293
  const failures = parseFailures(output);
1246
1294
  // Bun repeats failed test names under "N tests failed:". A validated Pod sweep joins
1247
1295
  // separately checked shards, so a summary in one shard can precede real failures
@@ -1271,7 +1319,7 @@ function failuresOf(run: CommandResult, label: string): SweepFailures {
1271
1319
  }
1272
1320
  }
1273
1321
  if (errors.length !== errorCount) throw incomplete(`error attribution identified=${errors.length}`);
1274
- 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)
1275
1323
  || (rc === 1 && count === 0 && errorCount === 0) || (rc === 0 && count + errorCount > 0)) {
1276
1324
  throw incomplete(`failure attribution incomplete: summary=${count}, identified=${failures.length}`);
1277
1325
  }
@@ -1298,7 +1346,7 @@ export function readIsolatedRun(run: CommandResult, label: string, file: string)
1298
1346
  if (!cleanNonzero) {
1299
1347
  const read = failuresOf(run, label);
1300
1348
  // Two (fail) lines folding into one id would undercount the file's failures: unreadable, not fewer.
1301
- const failCount = summaryCount(output, 'fail');
1349
+ const failCount = deterministicSubrunCounts(output)?.fail ?? summaryCount(output, 'fail');
1302
1350
  if (Number.isFinite(failCount) && read.failures.length !== failCount)
1303
1351
  throw new Error(`${label} incomplete (rc=${run.rc}; failure attribution summary=${failCount}, identified=${read.failures.length})`);
1304
1352
  return { kind: 'read', ...read };
@@ -1419,7 +1467,40 @@ export interface GateAdmission {
1419
1467
  /** The pool-wide reading admission waits on (recommended · limitedBy · memorySlots); null before any reading. */
1420
1468
  waitReason(): string | null;
1421
1469
  }
1422
- 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
+ }
1423
1504
  export type GateAdmissionOutcome = 'granted' | 'admission-timeout' | 'admission-error' | 'off';
1424
1505
  export interface GateAdmissionResult { outcome: GateAdmissionOutcome; waitedMs: number; reason?: string; release: () => void }
1425
1506
  export const gateAdmissionWaitText = (reading: string | null) => `메모리 여유 대기(admission) · ${reading ?? '측정 전'}`.slice(0, 160);
@@ -1569,6 +1650,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1569
1650
  const watchList = pod.riskWatch ? readGateRiskWatchList(pod.riskWatch) : [];
1570
1651
  const riskPlan = planGateRiskLaneShards({ assignable, durations, shardCount, heavy, watchList });
1571
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;
1572
1657
  if (riskPlan.spawnFiles.length) debug.log('release-loop.gate', 'pod-spawn-lane', { files: riskPlan.spawnFiles, shards: riskPlan.spawnShardCount });
1573
1658
  if (riskPlan.riskFiles.length) debug.log('release-loop.gate', 'pod-risk-lane-plan', { files: riskPlan.riskFiles, shard: 0,
1574
1659
  plannedSeconds: riskPlan.riskPlannedSeconds, shards: shards.length, watch: watchList.length });
@@ -1590,6 +1675,56 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1590
1675
  const ready = poolOverride ? undefined : checkPodPool(parsePodPool(pod.pool), kubectl);
1591
1676
  if (ready && !ready.ok) throw new Error('pod command: 풀의 노드가 하나도 준비되지 않았다');
1592
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}`);
1593
1728
  const configCache = typeof pod.bunCache === 'string' ? pod.bunCache.trim() : '';
1594
1729
  const envCache = process.env[POD_BUN_CACHE_HOST_PATH]?.trim();
1595
1730
  const bunCache = configCache || envCache || undefined;
@@ -1600,11 +1735,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1600
1735
  if (!Number.isSafeInteger(installSlots) || installSlots < 0) throw new Error('invalid pod sweep options');
1601
1736
  debug.log('release-loop.gate', 'pod-install-slots', { slots: installSlots, source: pod.installSlots !== undefined ? 'config' : 'default' });
1602
1737
  // GATE-MEM-ADMIT: every shard Job (retries and splits too) asks pool admission before it is placed.
1603
- 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);
1604
1740
  if (!Number.isSafeInteger(admissionWaitSeconds) || admissionWaitSeconds < 0) throw new Error('invalid pod sweep options');
1605
1741
  // An injected pool (tests) runs without admission unless `pod.admission` is passed; the gate's own pool asks it.
1606
1742
  const gateAdmission = pod.admission ?? tools.admission ?? (poolOverride ? undefined : poolGateAdmission(poolScheduler));
1607
- 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 });
1608
1745
  // GATE-POD-STALL-FAILFAST: a shard with no test output within the stall window is stopped and recorded unmeasured.
1609
1746
  const stallSeconds = pod.stallSeconds ?? GATE_POD_STALL_SECONDS_DEFAULT;
1610
1747
  const stallPollMs = pod.stallPollMs ?? GATE_POD_STALL_POLL_MS;
@@ -1722,29 +1859,88 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1722
1859
  }, 0);
1723
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);
1724
1861
  const unknownSeconds = knownTimes.length ? (knownTimes[Math.floor((knownTimes.length - 1) / 2)]! + knownTimes[Math.floor(knownTimes.length / 2)]!) / 2 : 1;
1725
- // GATE-LIVE-OBS — one row per root shard in the release ledger's shards.json (UX contract · src/release-loop/gate-shards.ts).
1726
- // 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.
1727
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
+ };
1728
1887
  const flushBoard = () => {
1729
1888
  if (!pod.shardsFile) return;
1730
1889
  try { writeGateShards(pod.shardsFile.path, { v: 1, version: pod.shardsFile.version, updatedAt: new Date().toISOString(), shards: board }); }
1731
1890
  catch (error) { debug.log('release-loop.gate', 'shards-write-failed', { path: pod.shardsFile.path, error: String(error) }); }
1732
1891
  };
1733
- const markShard = (shard: number, patch: Partial<GateShard>, clear: Array<keyof GateShard> = []) => {
1734
- 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];
1735
1896
  if (!row) return;
1736
1897
  const next: GateShard = { ...row, ...patch };
1737
1898
  for (const key of clear) delete next[key];
1738
1899
  if (JSON.stringify(next) === JSON.stringify(row)) return;
1739
- board[shard] = next;
1900
+ board[index] = next;
1901
+ observeShard(next);
1740
1902
  flushBoard();
1741
1903
  };
1742
- const resetShardAttempt = (shard: number) => {
1743
- const row = board[shard];
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];
1744
1908
  if (!row) return;
1745
- board[shard] = beginGateShardAttempt(row);
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]!);
1746
1921
  flushBoard();
1747
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);
1748
1944
  flushBoard();
1749
1945
  // A result collector can lose its callback after the Kubernetes Job has already completed. Reconcile
1750
1946
  // the public shard board independently so a finished Job cannot remain «running» indefinitely.
@@ -1758,6 +1954,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1758
1954
  const index = board.findIndex((shard) => shard.id === item.shard);
1759
1955
  if (index < 0) continue;
1760
1956
  board[index] = reconciled.board.find((shard) => shard.id === item.shard)!;
1957
+ observeShard(board[index]!);
1761
1958
  debug.log('release.gate', 'shard-stale-finalized', item);
1762
1959
  }
1763
1960
  flushBoard();
@@ -1768,22 +1965,25 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1768
1965
  /** The gate's scheduler, seen through one shard: a slot wait and the Pod start become row states. */
1769
1966
  // GATE-MEM-ADMIT: `placed` releases the shard's admission once a member is chosen — from then on the Job is
1770
1967
  // Pending/Running in the cluster and the next admission reading counts it, so holding the lease would count it twice.
1771
- const trackedPool = (shard: number, retry: boolean, jobName: string, resultFile: string, attemptId: string, 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 => ({
1772
1969
  members: poolScheduler.members,
1773
1970
  tryAcquire: async (...[skip, onEmpty, mode]: Parameters<PodPoolScheduler['tryAcquire']>) => {
1774
1971
  // GATE-MULTI-HOST: a lost member is skipped while another member is left (all lost = the old behaviour).
1775
1972
  const anyLeft = poolScheduler.members.some((m) => !downMembers.has(m.context));
1776
- 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) : [])]);
1777
1978
  const member = await poolScheduler.tryAcquire(excluded.size ? excluded : undefined, onEmpty, mode);
1778
1979
  if (member) placed(member);
1779
- if (member) markShard(shard, { state: retry ? 'retry' : 'running', startedAt: board[shard]?.startedAt ?? new Date().toISOString(), host: member.context, job: jobName, resultFile, attemptId }, ['waitReason']);
1780
- 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);
1781
1982
  return member;
1782
1983
  },
1783
1984
  release: (member: Parameters<PodPoolScheduler['release']>[0]) => poolScheduler.release(member),
1784
1985
  }) as unknown as PodPoolScheduler;
1785
- 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']> }> => {
1786
- if (branch || depth > 0 || parentAttempt !== undefined || hostRetries > 0) resetShardAttempt(shard);
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']> }> => {
1787
1987
  // 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
1788
1988
  // GATE-TIMEOUT-HEAL: parts (each with its own junit) under a soft deadline inside this shard's own Job deadline.
1789
1989
  const shardDeadline = shardDeadlineSeconds(batchIsolation ? Math.max(...paths.map((file) => estimated([file]))) * Math.ceil(paths.length / batchWidth) : estimated(paths), deadlineSeconds);
@@ -1847,13 +2047,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1847
2047
  if (descent === 'retry') {
1848
2048
  // «descend» is only the way down; «resumed» comes from each leaf whose log reads cleanly (review r4).
1849
2049
  debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: 'retry' });
1850
- resetShardAttempt(shard);
2050
+ resetShardAttempt(shard, branch);
1851
2051
  return runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId, hostRetries, batchIsolation, skipMembers);
1852
2052
  }
1853
- if (descent === 'split') {
1854
- resetShardAttempt(shard);
2053
+ if (descent === 'split' || descent === 'file-fanout') {
2054
+ resetShardAttempt(shard, branch);
1855
2055
  output = undefined;
1856
- 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 });
1857
2057
  } else if (reused) {
1858
2058
  output = reused.output;
1859
2059
  junit = reused.junit;
@@ -1862,7 +2062,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1862
2062
  lastFile = lastStartedTestFile(output, paths);
1863
2063
  } else try {
1864
2064
  stamp('admissionStart');
1865
- 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) });
1866
2066
  stamp('admissionEnd');
1867
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 } : {}) },
1868
2068
  admitted.outcome === 'granted' ? undefined : { level: 'warn' });
@@ -1903,10 +2103,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1903
2103
  graphNode: 'gate', version: pod.shardsFile?.version, commit, shard: `${shard + 1}-of-${shards.length}`,
1904
2104
  reason: `release gate shard ${shard + 1}/${shards.length}`, pool: pod.pool, cpuReq: cpu.request,
1905
2105
  memReq: memoryLimit ?? POD_ISOLATION_MEMORY_LIMIT, warmReused: false },
1906
- pool: pod.pool, poolScheduler: trackedPool(shard, depth > 0 || branch !== '', jobName, resultFile, attemptId, (member) => {
2106
+ pool: pod.pool, poolScheduler: trackedPool(shard, branch, jobName, resultFile, attemptId, (member) => {
1907
2107
  stamp('poolPlaced'); jobContext = member.context; jobContextForCapture = member.context; placedOn = member.context; placed();
1908
2108
  if (branch === '-tail') tailPlacedHost = member.context;
1909
- }, 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,
1910
2110
  ...gateHostMirror(pod),
1911
2111
  ...(multiHost ? { statusUnreadableMs: memberUnreadableSeconds * 1000 } : {}),
1912
2112
  ...(bunCache ? { bunCache } : {}),
@@ -2029,6 +2229,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2029
2229
  }
2030
2230
  }
2031
2231
  const installSeconds = parseInstallSeconds(output ?? '');
2232
+ if (imageDigest) detailsById.set(`pod-${shard}${branch}`, { ...(detailsById.get(`pod-${shard}${branch}`) ?? {}), imageDigest });
2032
2233
  // GATE-TIMEOUT-HEAL ③: a cut shard is «reason: timeout» (with the files that still finished), not a bare rc=null.
2033
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 } : {}),
2034
2235
  ...(testSeconds !== undefined ? { testSeconds } : {}),
@@ -2037,7 +2238,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2037
2238
  // only for a Job that came back (a launch that failed before the Job returned has no Pod to describe).
2038
2239
  if (!reused && !descent && podJobName !== undefined) emitPodLifecycle(gatePodLifecycleEvent({ runId: process.env.ELANOUS_RUN_ID ?? null, podJob: podJobName, host: placedOn ?? null,
2039
2240
  shard: `pod-${shard}${branch}`, attemptId, phaseTimes, podStages, podLifecycle, outcome: gatePodOutcome({ jobFailed, stalled: !!stall, timedOut, rc }) }));
2040
- 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);
2041
2242
  let clean = output?.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
2042
2243
  const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
2043
2244
  const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
@@ -2103,6 +2304,22 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2103
2304
  // 137 (killed · OOM) keeps the isolation path: re-running an OOM file only re-measures the same kill.
2104
2305
  const diedBeforeTests = reason === 'job-failed' && output === undefined && !jobFailed && jobExitCode !== undefined && jobExitCode !== 0
2105
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
+ }
2106
2323
  if ((reason === 'no-output' || diedBeforeTests) && !retriedNoOutput) {
2107
2324
  const retry = await runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId, hostRetries, batchIsolation, skipMembers);
2108
2325
  debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
@@ -2233,9 +2450,29 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2233
2450
  errors: Number.isNaN(errors) ? 0 : errors,
2234
2451
  ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
2235
2452
  };
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
+ }
2459
+ try {
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);
2463
+ return done;
2464
+ } catch (error) {
2465
+ if (branch) markShard(shard, { state: 'failed', endedAt: new Date().toISOString() }, ['waitReason'], branch);
2466
+ throw error;
2467
+ }
2468
+ };
2236
2469
  // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
2237
2470
  // GATE-RISK-LANE — the risk shard's own wall time (planned vs measured in its log line); it never changes a verdict.
2238
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 });
2239
2476
  let settled: Array<PromiseSettledResult<Awaited<ReturnType<typeof runShard>>>>;
2240
2477
  if (shardsFile) {
2241
2478
  reconcileTimer = setInterval(reconcileTick, 30_000);
@@ -2245,10 +2482,59 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2245
2482
  settled = await Promise.allSettled(shards.map(async (item, shard) => {
2246
2483
  const shardStartedAt = clock();
2247
2484
  try {
2248
- const done = await runShard(item.files, shard);
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));
2249
2520
  shardDurations[shard] = clock() - shardStartedAt;
2250
2521
  const state = !done.stalled.length ? 'done' as const : done.stalled.some((stall) => stall.reason === 'timeout') ? 'timeout' as const : 'failed' as const;
2251
- markShard(shard, { state, endedAt: new Date().toISOString() }, ['waitReason']);
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
+ }
2252
2538
  return done;
2253
2539
  } catch (error) {
2254
2540
  markShard(shard, { state: 'failed', endedAt: new Date().toISOString() }, ['waitReason']);
@@ -2274,6 +2560,8 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2274
2560
  // GATE-STALLED-TAIL-REMEASURE: after the sweep, give only a one-file no-output leaf one fresh attempt on a different
2275
2561
  // pool member. Keep the original unmeasured evidence if no alternate is available or the remeasure is incomplete.
2276
2562
  const remainingStalls: typeof stalledShards = [];
2563
+ const tailTouched = new Set<number>();
2564
+ const tailRc = new Map<number, number>();
2277
2565
  for (const stalled of stalledShards) {
2278
2566
  const alternatives = stalled.lastHost
2279
2567
  ? poolScheduler.members.filter((member) => member.context !== stalled.lastHost && !downMembers.has(member.context))
@@ -2284,6 +2572,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2284
2572
  }
2285
2573
  debug.log('release-loop.gate', 'pod-stalled-tail-remeasure-start', { shard: stalled.shard, file: stalled.files[0], fromHost: stalled.lastHost,
2286
2574
  availableHosts: alternatives.map((member) => member.context) });
2575
+ tailTouched.add(stalled.shard);
2287
2576
  try {
2288
2577
  tailPlacedHost = undefined;
2289
2578
  const tail = await runShard(stalled.files, stalled.shard, 0, '-tail', true, true, undefined, 0, false, new Set([stalled.lastHost]), true);
@@ -2296,6 +2585,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2296
2585
  fromHost: stalled.lastHost, toHost: unresolved.lastHost ?? null, reason: unresolved.reason }, { level: 'warn' });
2297
2586
  } else {
2298
2587
  measured.push(...tail.runs);
2588
+ tailRc.set(stalled.shard, tail.runs.some((run) => run.fail || run.errors) ? 1 : 0);
2299
2589
  debug.log('release-loop.gate', 'pod-stalled-tail-remeasure-complete', { shard: stalled.shard, file: stalled.files[0],
2300
2590
  fromHost: stalled.lastHost, toHost: tailPlacedHost ?? null });
2301
2591
  }
@@ -2306,6 +2596,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
2306
2596
  }
2307
2597
  }
2308
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
+ }
2309
2603
  const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
2310
2604
  errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
2311
2605
  { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
@@ -2819,6 +3113,11 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2819
3113
  existsSync(join(root, 'release', opts.version, 'rehearsal', `${step}.json`)));
2820
3114
  const currentImage = !explicitPartial && opts.graphRunId && opts.pod && rehearsalStampPresent
2821
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);
2822
3121
  const radius = !explicitPartial && opts.graphRunId && opts.pod && rehearsalStampPresent
2823
3122
  ? planHardRehearsalRadius({ root, repo, version: opts.version, forCommit: opts.commit,
2824
3123
  graphPins: gateGraphPins(root, opts.graphRunId), envFingerprint: gateEnvFingerprint(process.env), imageDigest: currentImage })
@@ -2879,8 +3178,12 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2879
3178
  // the cut sweep puts those files first, the baseline sweep keeps its plan.
2880
3179
  riskWatch: { instanceRoots: roots, ledgerRoots: roots },
2881
3180
  // GATE-LIVE-OBS: the release ledger's shards.json (UX #24782) — never the run tree's universe; an explicit
2882
- // --ledger-root (tests) keeps it in that ledger.
2883
- 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,
2884
3187
  selected?.files);
2885
3188
  let cutRun = await runCut(partial);
2886
3189
  const radiusSelectionMatched = partial?.files && cutRun?.podSweep?.files
@@ -3418,7 +3721,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
3418
3721
  // GATE-LIVE-OBS-RC-WORDING ⓒ — the shards board says «판정 전» until this lands; then «새 실패 N» only (UX · gate-shards.ts).
3419
3722
  // Same ledger as the board's own writes (sweep's shardsFile). A write failure is logged and never changes the verdict.
3420
3723
  if (result.outcome !== 'error' && opts.pod) {
3421
- const path = opts.ledgerRoot ? gateShardsPath(opts.version, opts.ledgerRoot) : gateShardsPath(opts.version);
3724
+ const path = gateShardsPath(opts.version, ledger);
3422
3725
  try {
3423
3726
  const written = recordGateShardsVerdict(path, { introduced: result.introduced.length, preexisting: result.preexisting });
3424
3727
  debug.log('release-loop.gate', 'shards-verdict', { version: opts.version, path, written, introduced: result.introduced.length, preexisting: result.preexisting });
@@ -3483,6 +3786,12 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
3483
3786
  if (fromGraph.gateExecutor !== undefined && !isGateExecutor(fromGraph.gateExecutor)) throw new Error(`gateExecutor must be pod or host: ${String(fromGraph.gateExecutor)}`);
3484
3787
  // GATE-HOST-EXECUTOR: a host gate never reads the pod settings (a bad pod value must not block the fallback executor).
3485
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}');
3486
3795
  const opts: GateOptions = {
3487
3796
  rehearsalTui: fromGraph.rehearsalTui === true,
3488
3797
  recheckFileLimit: fromGraph.gateRecheckFileLimit !== undefined ? Number(fromGraph.gateRecheckFileLimit) : undefined,
@@ -3504,11 +3813,16 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
3504
3813
  ...(fromGraph.gatePodStallSeconds !== undefined ? { stallSeconds: Number(fromGraph.gatePodStallSeconds) } : {}),
3505
3814
  ...(fromGraph.gatePodMidRunStallSeconds !== undefined ? { midRunStallSeconds: Number(fromGraph.gatePodMidRunStallSeconds) } : {}),
3506
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' }> } : {}),
3507
3819
  ...((cpu) => cpu ? { cpu } : {})(parsePodCpu(fromGraph.gatePodCpu, 'release.loop.gatePodCpu')),
3508
3820
  } : undefined,
3509
3821
  };
3510
3822
  if (isGateExecutor(fromGraph.gateExecutor)) opts.executor = fromGraph.gateExecutor;
3511
3823
  let freshShards = false;
3824
+ let rerouteShard: string | undefined;
3825
+ let shardHost: string | undefined;
3512
3826
  for (let i = 0; i < args.length; i++) {
3513
3827
  const arg = args[i];
3514
3828
  if (arg === '--help') return 'help';
@@ -3516,6 +3830,18 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
3516
3830
  if (arg === '--rehearsal-baseline') { opts.rehearsalBaseline = true; continue; }
3517
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).
3518
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
+ }
3519
3845
  if (arg === '--rehearsal-tui') { opts.rehearsalTui = true; continue; }
3520
3846
  if (arg === '--recheck-file-limit') {
3521
3847
  const value = args[++i];
@@ -3544,6 +3870,11 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
3544
3870
  else if (key === 'remoteMirror') opts.remoteMirror = value;
3545
3871
  }
3546
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');
3547
3878
  // GATE-HOST-EXECUTOR: the host executor is the existing non-pod sweep on gateRemote — a configured pool must not pull it
3548
3879
  // back into Pods, and without a host it would silently sweep on the runner's own machine.
3549
3880
  if (opts.executor === 'host') {
@@ -3576,7 +3907,7 @@ export async function runGateNode(argv: string[], env: NodeJS.ProcessEnv, runner
3576
3907
  debug.log('release-loop.gate', 'executor', { version: opts.version, ...placement, requested: opts.executor ?? null });
3577
3908
  }
3578
3909
  if (opts === 'help') {
3579
- 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.');
3580
3911
  return 0;
3581
3912
  }
3582
3913
  version = opts.version;