elanous 0.2.32 → 0.2.35

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 (753) 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/{IVKr3AlRUVLKsw2fh7eXF → T1mBQqmkwKDLGtyisDCOd}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{997ce8a5-2ad501927003b41b.js → 0368f25d-ad84af92fefee68f.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1165.bad13fc9151a7288.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/13.02a07436dec15f89.js +32 -0
  7. package/apps/pwa/out/_next/static/chunks/{5525.86debf231e181813.js → 1530.0e612369b6fd7ea0.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/1540.af38731fa7c4a3e0.js +41 -0
  9. package/apps/pwa/out/_next/static/chunks/{7391.ebae168025c91b6a.js → 1674.2a65fd5668e1cc5f.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/1850.f2b470ba5a471da0.js +24 -0
  11. package/apps/pwa/out/_next/static/chunks/193.62da94cd0b88323b.js +74 -0
  12. package/apps/pwa/out/_next/static/chunks/1975.260f6d249fac5f7c.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/1982-67cd8dfe8682325f.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/{7947-d0759430f8042240.js → 1993-c0007b7ad0c0ef41.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/1999-bb415351c8a2846c.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/{60fb96c9.360b01d8d4927b7b.js → 1c735de4.f993a30bbd3c57c4.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/1dc5a567.9b092bfae3d7099b.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/{5307.60f2209e809bd627.js → 2018.a741297bce759490.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/2085.8f950b4bda1cbbc7.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/2159-6179a961376956b9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/216.b5c2a421aa1d28a5.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/2251.49fbe369f74c372c.js +148 -0
  23. package/apps/pwa/out/_next/static/chunks/2286-2939577360d3dcc0.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/{3902.e1e71aaac850a045.js → 2625.5d870aa999f33db1.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/2668.34d21539e62bf111.js +77 -0
  26. package/apps/pwa/out/_next/static/chunks/2833-0fe47af99ed13c29.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/2930.d0edb4f108787ef7.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/2973.77318e7d85f59477.js +62 -0
  29. package/apps/pwa/out/_next/static/chunks/3103.0df8da390948bb84.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/3123.2bee9ba15d320bd2.js +90 -0
  31. package/apps/pwa/out/_next/static/chunks/3162-6cf652bf7124d8cf.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/{4174.92ed04922709ac9e.js → 3408.21d1570602f4b6c0.js} +2 -2
  33. package/apps/pwa/out/_next/static/chunks/3577-7340009f7007a795.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/3589.5444a06bcf6e775c.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/359-2250789a2d3499e7.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/3618.f514792d6c59d7fd.js +36 -0
  37. package/apps/pwa/out/_next/static/chunks/3669.343f9119aa276001.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/{9398-6aed28f0a96d0d6f.js → 3677-4215f826f6aa68e5.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/3736.c2efad489604cc3b.js +79 -0
  40. package/apps/pwa/out/_next/static/chunks/3817.f9aeab2bb6675d73.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/3f7c4ee5.3dde54f01a816613.js +122 -0
  42. package/apps/pwa/out/_next/static/chunks/4182-0b9b1251c29cc0cf.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/{7371.80c05c653b08e907.js → 4346.8601952e73bf5803.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/4441-875b97f5821ed58a.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/4582.bd68683c076649e3.js +286 -0
  46. package/apps/pwa/out/_next/static/chunks/4605-6e623f55aec77422.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/4607.5cc27468628eb4bf.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/4627.f07508b3936fd6c0.js +24 -0
  49. package/apps/pwa/out/_next/static/chunks/{2940-619bfd8ef337c611.js → 4705-194e64e47ca0fbdb.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/{1335-6731f392c35216eb.js → 4718-17d2af636796a330.js} +3 -3
  51. package/apps/pwa/out/_next/static/chunks/4747-c22262c093e46807.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/4775-178215aed0daba61.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/4815-6ffb8c3c991cc45d.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/4834-beca8b47ad29a898.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/5023-fb4802b6d2492b02.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/{7502.ccd5b8065545edd0.js → 5101.e32282718f99ed6f.js} +2 -2
  57. package/apps/pwa/out/_next/static/chunks/{2001.6c7e0fd02190886c.js → 5154.a118f3957ed14e36.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/5210-a2e0015e3d4d5d9c.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/5356.84ac712cc15928bf.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/5401.22352141b6fd336e.js +23 -0
  61. package/apps/pwa/out/_next/static/chunks/{5173-233188df2f42c626.js → 5483-dd5d7697f6bdb106.js} +1 -1
  62. package/apps/pwa/out/_next/static/chunks/5637.281e5dec4ea6ecf1.js +94 -0
  63. package/apps/pwa/out/_next/static/chunks/5662.02eaa029af05ad46.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{3267c0e6.d34854e7b3984a6e.js → 56d372f2.681ab4ec4c998fae.js} +1 -1
  65. package/apps/pwa/out/_next/static/chunks/5760.22456f433c6f42c0.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/5852.db1f7449f4f74612.js +287 -0
  67. package/apps/pwa/out/_next/static/chunks/5891-0f11a7d7362a9072.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/5943.264d14a44ca32513.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/604-1b3c39df083c02aa.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/6121-5f97072330738caf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/6193.07d8ca5af4b09086.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/6203-b3fea3a60d69ff14.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/{6433.fc573af22bbc9ab7.js → 6236.e18cced2d5ba6afb.js} +17 -17
  74. package/apps/pwa/out/_next/static/chunks/6286.d638fb5953e2e757.js +8 -0
  75. package/apps/pwa/out/_next/static/chunks/630.fe5a46428d63b013.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/6301.ae0245f507e4395e.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/6673.fbbe5699f6ea5991.js +27 -0
  78. package/apps/pwa/out/_next/static/chunks/6777-f6d301efb5de828d.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/6786.423f24c88de034c7.js +93 -0
  80. package/apps/pwa/out/_next/static/chunks/6854-cef64a948c5e122d.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/6866-79aa5b4e47cac6aa.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/{6544cb48.a87fe20ddf137be2.js → 6b89c8bd.24df687a820500b3.js} +1 -1
  83. package/apps/pwa/out/_next/static/chunks/7037-fe27f001db30d323.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/7080.2d826860534fd88e.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/7118.8731be28c3ebb14b.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/7196.44241bc1df3e26fe.js +149 -0
  87. package/apps/pwa/out/_next/static/chunks/7235.c7c4e6cb5c693c2d.js +104 -0
  88. package/apps/pwa/out/_next/static/chunks/{2871.0bca6137343b3f76.js → 7385.50a308a477bdfc21.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/{2985.39ccdecd36848b12.js → 7644.17e37ba6f1d4226e.js} +4 -4
  90. package/apps/pwa/out/_next/static/chunks/7706-58bfb1546637f217.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/7839.5b91455338c9fd3f.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/7857-31f13ee96f4011aa.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/81.f74cb60312fe4001.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/{8018.a32663f04a6fc5f9.js → 8219.baa715d1deee80e2.js} +1 -1
  95. package/apps/pwa/out/_next/static/chunks/{4111.2bfedbb233ad206b.js → 8256.e3ac8421a71f82f4.js} +1 -1
  96. package/apps/pwa/out/_next/static/chunks/8310-444a6a895c402f12.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/8423.f7dda8aca4fcaae5.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/8437-8d5492094b166cfa.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/{2213.2ccbada92c33ea0f.js → 8444.4c26b379ca2b9596.js} +17 -17
  100. package/apps/pwa/out/_next/static/chunks/8496.75649af8652c74f4.js +226 -0
  101. package/apps/pwa/out/_next/static/chunks/8557-e7898769499cd56f.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/8576.65bb654b524f3614.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/8609.558ce2f8f9aea2be.js +64 -0
  104. package/apps/pwa/out/_next/static/chunks/8614.caec96cfb91f4ae7.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/{6092.a7f99e68b188ca66.js → 8637.47f5b35afcae5e6b.js} +1 -1
  106. package/apps/pwa/out/_next/static/chunks/8714.ff0b7ce3ec3e3ae2.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/8732.367f44a916202353.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/9141-7fd1f73a5400bdc2.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/921.c4267d0d69a65404.js +1 -0
  110. package/apps/pwa/out/_next/static/chunks/{3278.2e5d15b53a4091c8.js → 9214.63a510d39178e997.js} +2 -2
  111. package/apps/pwa/out/_next/static/chunks/9261.3962c3321b047691.js +115 -0
  112. package/apps/pwa/out/_next/static/chunks/936-26efd1a0efdd44d3.js +1 -0
  113. package/apps/pwa/out/_next/static/chunks/{9221.7d22eaf353a2d4e7.js → 938.66ad1ef0e5abae1c.js} +1 -1
  114. package/apps/pwa/out/_next/static/chunks/{6157.0c9af021de4fb31b.js → 9512.5a76207a730e0491.js} +2 -2
  115. package/apps/pwa/out/_next/static/chunks/9627-4b9efe616b27e2e2.js +1 -0
  116. package/apps/pwa/out/_next/static/chunks/{69-0a2c8209c6d4a158.js → 967-2aa39b2d04b4c690.js} +1 -1
  117. package/apps/pwa/out/_next/static/chunks/9734.40eb1e558c29ccea.js +1 -0
  118. package/apps/pwa/out/_next/static/chunks/9908.3aa54f7a586a73dc.js +1 -0
  119. package/apps/pwa/out/_next/static/chunks/{7862.30a8d6482bb238eb.js → 9952.5daf5f94063a2b3e.js} +1 -1
  120. package/apps/pwa/out/_next/static/chunks/9954-ca8d29752bdbb45a.js +1 -0
  121. package/apps/pwa/out/_next/static/chunks/9974-507fa67cb94066dd.js +1 -0
  122. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-ce9e00f7fb45506a.js +1 -0
  123. package/apps/pwa/out/_next/static/chunks/app/approvals/page-fa59d6d17daa3389.js +1 -0
  124. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-53f68bbe7bd4b774.js +1 -0
  125. package/apps/pwa/out/_next/static/chunks/app/board/page-1c500fbb9f4a7a04.js +1 -0
  126. package/apps/pwa/out/_next/static/chunks/app/botlab/page-99d72673aa5c307c.js +1 -0
  127. package/apps/pwa/out/_next/static/chunks/app/bots/page-d2d5adc1e9484c88.js +1 -0
  128. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c607c5aae637f1cc.js +1 -0
  129. package/apps/pwa/out/_next/static/chunks/app/chat/page-fa8c23596d555c0f.js +1 -0
  130. package/apps/pwa/out/_next/static/chunks/app/consult/page-bdab8c5e855c8416.js +1 -0
  131. package/apps/pwa/out/_next/static/chunks/app/control/{page-a27b1e5a0d1b308f.js → page-406b47745b53f269.js} +1 -1
  132. package/apps/pwa/out/_next/static/chunks/app/decisions/page-a4276386b7051f50.js +1 -0
  133. package/apps/pwa/out/_next/static/chunks/app/design-check/page-8a26bab9ad375f25.js +1 -0
  134. package/apps/pwa/out/_next/static/chunks/app/editor/page-775f48aa1dc7a2d5.js +1 -0
  135. package/apps/pwa/out/_next/static/chunks/app/exec/page-1c4dd5f959d0c558.js +1 -0
  136. package/apps/pwa/out/_next/static/chunks/app/field/page-c48938a8377c9f70.js +1 -0
  137. package/apps/pwa/out/_next/static/chunks/app/healthz/route-ce9e00f7fb45506a.js +1 -0
  138. package/apps/pwa/out/_next/static/chunks/app/inside/page-c6e7905859425aa9.js +1 -0
  139. package/apps/pwa/out/_next/static/chunks/app/intake/page-c280f4252452e830.js +1 -0
  140. package/apps/pwa/out/_next/static/chunks/app/layout-cf8c57498f20970a.js +1 -0
  141. package/apps/pwa/out/_next/static/chunks/app/live/page-ca3a17caf409fc83.js +1 -0
  142. package/apps/pwa/out/_next/static/chunks/app/live-run/page-26e6c18a1a750cf6.js +1 -0
  143. package/apps/pwa/out/_next/static/chunks/app/loops/page-3abc872cc6aeee39.js +1 -0
  144. package/apps/pwa/out/_next/static/chunks/app/market/page-f7481b7b2174fe3d.js +1 -0
  145. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1ae7ef8077680337.js +1 -0
  146. package/apps/pwa/out/_next/static/chunks/app/morning/{page-32866485913d6a04.js → page-47c5d4db587266bd.js} +1 -1
  147. package/apps/pwa/out/_next/static/chunks/app/not-found-415f1e243c7ccb13.js +1 -0
  148. package/apps/pwa/out/_next/static/chunks/app/observatory/page-73f380f17b76f95c.js +1 -0
  149. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-2991073ad66ee2d9.js +1 -0
  150. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-1f5a60f70cb4dd0c.js +1 -0
  151. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fbad60d1e358fb2c.js +1 -0
  152. package/apps/pwa/out/_next/static/chunks/app/outputs/page-bac7dee94e5eae4a.js +1 -0
  153. package/apps/pwa/out/_next/static/chunks/app/page-398047d6949e8ca5.js +1 -0
  154. package/apps/pwa/out/_next/static/chunks/app/reflection/page-2d33a2231540ca89.js +1 -0
  155. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-5bf1e6bf70bcb96c.js +1 -0
  156. package/apps/pwa/out/_next/static/chunks/app/sessions/page-ba59c19376a0917d.js +1 -0
  157. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-111f187139add30f.js +1 -0
  158. package/apps/pwa/out/_next/static/chunks/app/settings/page-acaae197d5be5779.js +1 -0
  159. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-0df20b0f546a0d7d.js +1 -0
  160. package/apps/pwa/out/_next/static/chunks/app/setup/layout-ce9e00f7fb45506a.js +1 -0
  161. package/apps/pwa/out/_next/static/chunks/app/setup/page-27e1202d24b8fe35.js +1 -0
  162. package/apps/pwa/out/_next/static/chunks/app/share/page-bda78c50a78363e5.js +1 -0
  163. package/apps/pwa/out/_next/static/chunks/app/showroom/page-da592691293d9c9f.js +1 -0
  164. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f832994317a2e9f0.js +1 -0
  165. package/apps/pwa/out/_next/static/chunks/app/term/{page-04d00ae9b0890349.js → page-dc0a532f5b9196ec.js} +1 -1
  166. package/apps/pwa/out/_next/static/chunks/app/today/page-cfccbbeae0473404.js +1 -0
  167. package/apps/pwa/out/_next/static/chunks/app/trace/page-527e2073af84c81b.js +1 -0
  168. package/apps/pwa/out/_next/static/chunks/app/vault/page-bac86e9112671357.js +1 -0
  169. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6d59dff410e9d76a.js +1 -0
  170. package/apps/pwa/out/_next/static/chunks/app/workflows/page-53a61f8be09f02d2.js +1 -0
  171. package/apps/pwa/out/_next/static/chunks/app/workspace/page-db8f559e763c17f9.js +1 -0
  172. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c02aa4cc66455093.js +1 -0
  173. package/apps/pwa/out/_next/static/chunks/{7a1887c4.f36b88fba31da8fd.js → b2f6ba4d.fb56fdbbf798df33.js} +1 -1
  174. package/apps/pwa/out/_next/static/chunks/b6705a4f-b9a9047de9eecfd0.js +1 -0
  175. package/apps/pwa/out/_next/static/chunks/{b313b242.cb1265eea908db61.js → cbe38a15.5a032598e39970c6.js} +1 -1
  176. package/apps/pwa/out/_next/static/chunks/{409f1e57.d0649fc47691ebfe.js → d06f9160.a3d9062f95de8ad7.js} +1 -1
  177. package/apps/pwa/out/_next/static/chunks/{0bb0ff36.b8d418980951e5ba.js → da5176c6.6773b6c5994e1d99.js} +7 -7
  178. package/apps/pwa/out/_next/static/chunks/framework-50e6acebc630849b.js +1 -0
  179. package/apps/pwa/out/_next/static/chunks/main-7a1ae342d5ed75e6.js +1 -0
  180. package/apps/pwa/out/_next/static/chunks/main-app-7ad7d505ba1f92b4.js +1 -0
  181. package/apps/pwa/out/_next/static/chunks/pages/_app-02edba75232cb235.js +1 -0
  182. package/apps/pwa/out/_next/static/chunks/pages/_error-97d6ebae3e8eee4a.js +1 -0
  183. package/apps/pwa/out/_next/static/chunks/webpack-e986adaa429163e8.js +1 -0
  184. package/apps/pwa/out/approvals/index.html +1 -1
  185. package/apps/pwa/out/approvals/index.txt +22 -22
  186. package/apps/pwa/out/autopilot/index.html +1 -1
  187. package/apps/pwa/out/autopilot/index.txt +22 -22
  188. package/apps/pwa/out/board/index.html +1 -1
  189. package/apps/pwa/out/board/index.txt +22 -22
  190. package/apps/pwa/out/botlab/index.html +1 -1
  191. package/apps/pwa/out/botlab/index.txt +21 -21
  192. package/apps/pwa/out/bots/index.html +1 -1
  193. package/apps/pwa/out/bots/index.txt +21 -21
  194. package/apps/pwa/out/ceo/index.html +1 -1
  195. package/apps/pwa/out/ceo/index.txt +22 -22
  196. package/apps/pwa/out/chat/index.html +1 -1
  197. package/apps/pwa/out/chat/index.txt +21 -21
  198. package/apps/pwa/out/consult/index.html +1 -1
  199. package/apps/pwa/out/consult/index.txt +22 -22
  200. package/apps/pwa/out/control/index.html +1 -1
  201. package/apps/pwa/out/control/index.txt +22 -22
  202. package/apps/pwa/out/decisions/index.html +1 -1
  203. package/apps/pwa/out/decisions/index.txt +21 -21
  204. package/apps/pwa/out/design-check/index.html +1 -1
  205. package/apps/pwa/out/design-check/index.txt +22 -22
  206. package/apps/pwa/out/editor/index.html +1 -1
  207. package/apps/pwa/out/editor/index.txt +22 -22
  208. package/apps/pwa/out/exec/index.html +1 -1
  209. package/apps/pwa/out/exec/index.txt +22 -22
  210. package/apps/pwa/out/field/index.html +1 -1
  211. package/apps/pwa/out/field/index.txt +22 -22
  212. package/apps/pwa/out/healthz +1 -1
  213. package/apps/pwa/out/index.html +1 -1
  214. package/apps/pwa/out/index.txt +21 -21
  215. package/apps/pwa/out/inside/index.html +1 -1
  216. package/apps/pwa/out/inside/index.txt +21 -21
  217. package/apps/pwa/out/intake/index.html +1 -1
  218. package/apps/pwa/out/intake/index.txt +22 -22
  219. package/apps/pwa/out/live/index.html +1 -1
  220. package/apps/pwa/out/live/index.txt +22 -22
  221. package/apps/pwa/out/live-run/index.html +1 -1
  222. package/apps/pwa/out/live-run/index.txt +22 -22
  223. package/apps/pwa/out/loops/index.html +1 -1
  224. package/apps/pwa/out/loops/index.txt +21 -21
  225. package/apps/pwa/out/market/index.html +1 -1
  226. package/apps/pwa/out/market/index.txt +21 -21
  227. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  228. package/apps/pwa/out/missions/placeholder/index.txt +21 -21
  229. package/apps/pwa/out/morning/index.html +1 -1
  230. package/apps/pwa/out/morning/index.txt +22 -22
  231. package/apps/pwa/out/observatory/index.html +1 -1
  232. package/apps/pwa/out/observatory/index.txt +22 -22
  233. package/apps/pwa/out/ops/checklist/index.html +1 -1
  234. package/apps/pwa/out/ops/checklist/index.txt +21 -21
  235. package/apps/pwa/out/ops/release/index.html +1 -1
  236. package/apps/pwa/out/ops/release/index.txt +21 -21
  237. package/apps/pwa/out/ops/seats/index.html +1 -1
  238. package/apps/pwa/out/ops/seats/index.txt +21 -21
  239. package/apps/pwa/out/outputs/index.html +1 -1
  240. package/apps/pwa/out/outputs/index.txt +21 -21
  241. package/apps/pwa/out/reflection/index.html +1 -1
  242. package/apps/pwa/out/reflection/index.txt +22 -22
  243. package/apps/pwa/out/scheduler/index.html +1 -1
  244. package/apps/pwa/out/scheduler/index.txt +22 -22
  245. package/apps/pwa/out/sessions/index.html +1 -1
  246. package/apps/pwa/out/sessions/index.txt +21 -21
  247. package/apps/pwa/out/settings/devices/index.html +1 -1
  248. package/apps/pwa/out/settings/devices/index.txt +22 -22
  249. package/apps/pwa/out/settings/index.html +1 -1
  250. package/apps/pwa/out/settings/index.txt +22 -22
  251. package/apps/pwa/out/setup/done/index.html +1 -1
  252. package/apps/pwa/out/setup/done/index.txt +22 -22
  253. package/apps/pwa/out/setup/index.html +1 -1
  254. package/apps/pwa/out/setup/index.txt +22 -22
  255. package/apps/pwa/out/share/index.html +1 -1
  256. package/apps/pwa/out/share/index.txt +22 -22
  257. package/apps/pwa/out/showroom/index.html +1 -1
  258. package/apps/pwa/out/showroom/index.txt +21 -21
  259. package/apps/pwa/out/tasks/index.html +1 -1
  260. package/apps/pwa/out/tasks/index.txt +22 -22
  261. package/apps/pwa/out/term/index.html +1 -1
  262. package/apps/pwa/out/term/index.txt +22 -22
  263. package/apps/pwa/out/today/index.html +1 -1
  264. package/apps/pwa/out/today/index.txt +22 -22
  265. package/apps/pwa/out/trace/index.html +1 -1
  266. package/apps/pwa/out/trace/index.txt +22 -22
  267. package/apps/pwa/out/vault/index.html +1 -1
  268. package/apps/pwa/out/vault/index.txt +22 -22
  269. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  270. package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
  271. package/apps/pwa/out/workflows/index.html +1 -1
  272. package/apps/pwa/out/workflows/index.txt +22 -22
  273. package/apps/pwa/out/workspace/index.html +1 -1
  274. package/apps/pwa/out/workspace/index.txt +22 -22
  275. package/apps/pwa/out/worktrees/index.html +1 -1
  276. package/apps/pwa/out/worktrees/index.txt +22 -22
  277. package/bin/elanous.mjs +10 -2
  278. package/catalog/providers/opengateway.yaml +25 -0
  279. package/docs/goal-context/01-goal-authoring-checks.md +8 -0
  280. package/docs/goal-context/03-verification-menu.md +24 -0
  281. package/docs/goal-context/04-mock-scenarios.md +100 -0
  282. package/docs/goal-context/README.md +5 -0
  283. package/graphs/catalog/catalog.yaml +1 -1
  284. package/graphs/default-loop.yaml +76 -5
  285. package/graphs/document-loop.yaml +71 -4
  286. package/graphs/implement-loop.yaml +59 -4
  287. package/graphs/journey/SCENARIOS.md +33 -0
  288. package/graphs/journey/recipes.yaml +1 -0
  289. package/graphs/journey/ta-journey.yaml +94 -0
  290. package/graphs/journey/units/author/author.yaml +20 -0
  291. package/graphs/journey/units/author/recipes.yaml +2 -0
  292. package/graphs/journey/units/author-dialogue/author-dialogue.yaml +42 -0
  293. package/graphs/journey/units/author-dialogue/observe.ts +96 -0
  294. package/graphs/journey/units/author-dialogue/recipes.yaml +10 -0
  295. package/graphs/journey/units/green/green.yaml +20 -0
  296. package/graphs/journey/units/green/recipes.yaml +2 -0
  297. package/graphs/journey/units/pr/pr.yaml +20 -0
  298. package/graphs/journey/units/pr/recipes.yaml +2 -0
  299. package/graphs/journey/units/remember/recipes.yaml +2 -0
  300. package/graphs/journey/units/remember/remember.yaml +20 -0
  301. package/graphs/operate-loop.yaml +71 -4
  302. package/graphs/overlays/stop-heal-ladder.yaml +1 -1
  303. package/graphs/release-rehearsal/recipes.yaml +1 -1
  304. package/graphs/release-rehearsal/release-rehearsal.yaml +2 -2
  305. package/graphs/research-loop.yaml +159 -4
  306. package/graphs/test-diet/test-diet.yaml +3 -0
  307. package/graphs/units/dispatch-pod/dispatch-pod.yaml +50 -0
  308. package/graphs/units/dispatch-pod/recipes.yaml +14 -0
  309. package/graphs/units/entry/entry.yaml +50 -0
  310. package/graphs/units/entry/recipes.yaml +14 -0
  311. package/graphs/units/harvest/harvest.yaml +25 -0
  312. package/graphs/units/harvest/recipes.yaml +4 -0
  313. package/graphs/units/land/land.yaml +45 -0
  314. package/graphs/units/land/recipes.yaml +3 -0
  315. package/graphs/units/recipes.yaml +7 -0
  316. package/graphs/units/recover.yaml +39 -0
  317. package/graphs/units/reflect/recipes.yaml +3 -0
  318. package/graphs/units/reflect/reflect.yaml +15 -0
  319. package/graphs/units/task-agent-recover-review.yaml +17 -0
  320. package/graphs/units/task-agent-recover-wait.yaml +17 -0
  321. package/graphs/units/verbs/make-doc.yaml +34 -0
  322. package/graphs/units/verbs/recipes.yaml +33 -0
  323. package/graphs/units/verbs/verify.yaml +55 -0
  324. package/graphs/work/recipes.yaml +2 -0
  325. package/graphs/work/research-report.yaml +28 -0
  326. package/package.json +1 -1
  327. package/scripts/ci-typecheck-changed.ts +69 -16
  328. package/scripts/coord-role-pack.ts +19 -6
  329. package/scripts/docs/generate-graph-anatomy.ts +19 -2
  330. package/scripts/graph-harvest-unit.ts +22 -0
  331. package/scripts/graph-units/journey-stub.ts +6 -0
  332. package/scripts/graph-units/land-enqueue.ts +78 -0
  333. package/scripts/graph-units/recover-plan.ts +15 -0
  334. package/scripts/graph-units/reflect-node.ts +88 -0
  335. package/scripts/graph-units/ta-journey-observe.ts +109 -0
  336. package/scripts/graph-units/verbs/headless-goal-loop.ts +152 -0
  337. package/scripts/graph-units/verbs/verify-stage.ts +75 -0
  338. package/scripts/graph-units/verbs-envelope.ts +139 -0
  339. package/scripts/hq/standby-snapshot.ts +30 -0
  340. package/scripts/public-export.ts +43 -1
  341. package/scripts/ratchets/train-side-invariants.sh +45 -0
  342. package/scripts/release-loop/auto-approve-node.ts +30 -16
  343. package/scripts/release-loop/docs-land-node.ts +21 -5
  344. package/scripts/release-loop/gate-baseline-receipt.ts +155 -0
  345. package/scripts/release-loop/gate-closure.ts +5 -1
  346. package/scripts/release-loop/gate-diff.ts +1 -1
  347. package/scripts/release-loop/gate-node.ts +675 -49
  348. package/scripts/release-loop/gate-partial.ts +68 -14
  349. package/scripts/release-loop/gate-radius-live.ts +177 -0
  350. package/scripts/release-loop/gate-radius-shadow.ts +138 -0
  351. package/scripts/release-loop/gate-traffic-mode.ts +128 -0
  352. package/scripts/release-loop/gate-triage-node.ts +5 -2
  353. package/scripts/release-loop/known-issues-0.2.33-approved.json +194 -0
  354. package/scripts/release-loop/known-issues-node.ts +77 -8
  355. package/scripts/release-loop/node-verdict.ts +12 -1
  356. package/scripts/release-loop/npm-publish-node.ts +66 -12
  357. package/scripts/release-loop/preflight.ts +93 -7
  358. package/scripts/release-loop/probe-pod-host-dirs-live.ts +5 -0
  359. package/scripts/release-loop/publish-node.ts +60 -9
  360. package/scripts/release-loop/pwa-remote.ts +166 -0
  361. package/scripts/release-loop/pwa-scenarios-node.ts +19 -3
  362. package/scripts/release-loop/resume-cap.ts +56 -0
  363. package/scripts/release-loop/resume-release.ts +155 -19
  364. package/scripts/release-loop/resume.ts +24 -1
  365. package/scripts/release-loop/tui-sim-node.ts +8 -2
  366. package/scripts/release-loop/unattended-release.ts +10 -2
  367. package/scripts/test-diet/lib.ts +6 -1
  368. package/scripts/test-diet/node.ts +194 -13
  369. package/scripts/ux-sim.ts +39 -0
  370. package/src/acp/core-turn-bridge.ts +20 -5
  371. package/src/acp/reasoning-thought-buffer.ts +45 -0
  372. package/src/agent/self-ambient-cache.ts +8 -0
  373. package/src/agent-substrate/acp-reviewer.ts +90 -15
  374. package/src/agent-substrate/pr-reviewer.ts +17 -0
  375. package/src/agent-substrate/review-finding-key.ts +53 -0
  376. package/src/agent-substrate/self-review-cli.ts +188 -18
  377. package/src/ask-user-question/pending-questions.ts +1 -1
  378. package/src/autopilot/pr-manager.ts +393 -65
  379. package/src/budget/run-usage-rollup.ts +1 -0
  380. package/src/chat/index.ts +0 -2
  381. package/src/chat/pickers/modal-runtime.ts +10 -27
  382. package/src/chat/tool-render/dispatch.ts +62 -10
  383. package/src/cli/agent-cli.ts +13 -2
  384. package/src/cli/control-cli.ts +24 -7
  385. package/src/cli/doctor-cli.ts +4 -1
  386. package/src/cli/guardian-cli.ts +10 -0
  387. package/src/cli/light-entry-route.ts +75 -0
  388. package/src/cli/llm-detect-cli.ts +23 -1
  389. package/src/cli/pod-cli.ts +77 -3
  390. package/src/cli/pr-cli.ts +81 -14
  391. package/src/cli/pr-land-base-read.ts +2 -0
  392. package/src/cli/pr-land-overlap-evidence.ts +154 -16
  393. package/src/cli/pr-land-pod-shadow-job.ts +3 -0
  394. package/src/cli/pr-land-recovery.ts +64 -0
  395. package/src/cli/pty-takeover-cli.ts +6 -5
  396. package/src/cli/release-checklist-read-cli.ts +79 -0
  397. package/src/cli/release-cli.ts +189 -132
  398. package/src/cli/release-run-if-ready.ts +29 -28
  399. package/src/cli/release-single-flight.ts +178 -0
  400. package/src/cli/remote-resolve.ts +1 -1
  401. package/src/cli/root-tui-entry.ts +48 -0
  402. package/src/cli/seat-cli.ts +37 -0
  403. package/src/cli/setup-cli.ts +40 -1
  404. package/src/cli/sim-cli.ts +22 -0
  405. package/src/cli/tasks-cli.ts +667 -14
  406. package/src/cli/usage-cli.ts +13 -11
  407. package/src/clipboard/history.ts +179 -13
  408. package/src/config.ts +8 -1
  409. package/src/control/control-status.ts +8 -3
  410. package/src/control-plane/ledger.ts +5 -3
  411. package/src/control-plane/server.ts +5 -3
  412. package/src/core-turn/short-question.ts +5 -1
  413. package/src/dashboard/chat/acp-chat.ts +8 -0
  414. package/src/dashboard/index.ts +114 -77
  415. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +3 -0
  416. package/src/dashboard/input/dashboard-plain-turn-stream.ts +4 -0
  417. package/src/dashboard/input/seat-autowake.ts +56 -3
  418. package/src/dashboard/render/theme-resolver.ts +33 -4
  419. package/src/dashboard/seat-boot-preamble.ts +1 -1
  420. package/src/dashboard/slash-runtime/ask-clarification.ts +48 -0
  421. package/src/dashboard/slash-runtime/dashboard-delegated-slashes.ts +68 -0
  422. package/src/dashboard/slash-runtime/dashboard-handlers.ts +62 -322
  423. package/src/dashboard/slash-runtime/dashboard-log-slash.ts +208 -0
  424. package/src/dashboard/turn-preamble.ts +47 -1
  425. package/src/dashboard/turn-stream-formatter.ts +34 -1
  426. package/src/dashboard/turn-stream-runtime.ts +2 -0
  427. package/src/dashboard/turn-typeahead-prompt-zone.ts +67 -0
  428. package/src/domains/guardian-tick.ts +54 -2
  429. package/src/domains/outbound-alert.ts +3 -2
  430. package/src/domains/schedule-registry.ts +16 -2
  431. package/src/extractors.ts +3 -1
  432. package/src/git-fs/idle-refresh.ts +76 -0
  433. package/src/git-fs/index.ts +18 -7
  434. package/src/git-fs/retry.ts +4 -0
  435. package/src/git-fs/runner.ts +2 -0
  436. package/src/git-fs/worktree.ts +42 -4
  437. package/src/graph-kinds/registry.ts +2 -2
  438. package/src/graph-runner/runner.ts +267 -54
  439. package/src/grok/credential.ts +15 -3
  440. package/src/harness/harness-cli-command.ts +151 -38
  441. package/src/harness/harness-incidents.ts +37 -0
  442. package/src/harness/harness-pod-dispatch.ts +38 -1
  443. package/src/harness/harness-salvage-cli.ts +14 -2
  444. package/src/harness/launch-utterance-ledger.ts +208 -0
  445. package/src/impl-discipline/detector.ts +16 -4
  446. package/src/impl-discipline/system-prompt.ts +11 -0
  447. package/src/impl-discipline/types.ts +2 -2
  448. package/src/index.ts +683 -146
  449. package/src/intake-plane/already-built-hint.ts +91 -0
  450. package/src/intake-plane/requirement-funnel.ts +29 -1
  451. package/src/knowhow/pack.ts +199 -0
  452. package/src/light-cli.ts +111 -0
  453. package/src/llm/opengateway-models.ts +11 -0
  454. package/src/llm/provider-credentials.ts +14 -0
  455. package/src/llm/provider-detect.ts +8 -1
  456. package/src/llm/runtime-model-family.ts +2 -1
  457. package/src/llm.ts +268 -65
  458. package/src/loops/budget.ts +95 -0
  459. package/src/loops/msg-wake.ts +8 -2
  460. package/src/loops/orchestrator/traffic.ts +132 -1
  461. package/src/model-tier/llm-tier-map.ts +15 -1
  462. package/src/mss/logging/log-store.ts +4 -5
  463. package/src/native-tool-catalog.ts +15 -1
  464. package/src/nexus/api/graphs-api.ts +22 -3
  465. package/src/nexus/api/http-server.ts +1 -1
  466. package/src/nexus/api/loop-resources.ts +13 -1
  467. package/src/nexus/api/ops-api.ts +12 -0
  468. package/src/oauth/claude-pty-maturity.ts +66 -0
  469. package/src/oauth/codex-access-mirror.ts +55 -0
  470. package/src/oauth/codex.ts +7 -0
  471. package/src/oauth/fallback-chain.ts +1 -1
  472. package/src/onboarding/first-run.ts +5 -3
  473. package/src/onboarding/non-interactive.ts +64 -13
  474. package/src/onboarding.ts +10 -2
  475. package/src/ops/overview-collect.ts +54 -4
  476. package/src/ops/overview.ts +31 -7
  477. package/src/pr-tool/census.ts +282 -30
  478. package/src/pr-tool/inflight-registry.ts +160 -7
  479. package/src/prompt-library/gpt6-family-addendum.ts +14 -1
  480. package/src/pty-shell/herdr-agent-report.ts +442 -0
  481. package/src/pty-shell/pty-control-ipc.ts +5 -5
  482. package/src/pty-shell/pty-manifest.ts +7 -0
  483. package/src/pty-shell/pty-write-outcome.ts +24 -6
  484. package/src/pty-shell/tui-run-state.ts +117 -3
  485. package/src/release-loop/checklist.ts +103 -5
  486. package/src/release-loop/feature-store.ts +19 -2
  487. package/src/release-loop/landed-but-yellow.ts +129 -5
  488. package/src/release-loop/light-rc.ts +1 -0
  489. package/src/release-loop/placement.ts +10 -3
  490. package/src/release-loop/rehearsal.ts +205 -25
  491. package/src/release-loop/rubric.ts +16 -2
  492. package/src/repl/index.ts +174 -17
  493. package/src/repl/ops-msg-bridge.ts +32 -0
  494. package/src/self-dev/ask-launch-flow.ts +44 -5
  495. package/src/self-dev/cli-help-text.ts +17 -0
  496. package/src/self-dev/dev-cli.ts +11 -9
  497. package/src/self-dev/draft-residue.ts +37 -1
  498. package/src/self-dev/draft-sweep.ts +188 -13
  499. package/src/self-dev/draft-triage-rules.ts +107 -0
  500. package/src/self-dev/fleet-reduce.ts +3 -2
  501. package/src/self-dev/graph-front-nodes.ts +112 -0
  502. package/src/self-dev/graph-journey-nodes.ts +11 -6
  503. package/src/self-dev/graph-journey-route.ts +60 -6
  504. package/src/self-dev/harness-run-cli.ts +3 -15
  505. package/src/self-dev/orchestrate-cli-text.ts +10 -0
  506. package/src/self-dev/orchestrate.ts +79 -3
  507. package/src/self-dev/review-provider-fallback.ts +35 -1
  508. package/src/self-dev/run-supervisor.ts +107 -12
  509. package/src/self-dev/run-types.ts +5 -1
  510. package/src/self-dev/self-orchestrate-runtime.ts +1 -11
  511. package/src/self-dev/shard-aggregate.ts +42 -5
  512. package/src/self-dev/shard-run.ts +8 -1
  513. package/src/self-implement/blocked-draft-policy.ts +20 -0
  514. package/src/self-implement/classify-shadow.ts +3 -7
  515. package/src/self-implement/doctor-visit.ts +18 -1
  516. package/src/self-implement/gate-baseline.ts +95 -12
  517. package/src/self-implement/gate-cli.ts +1 -0
  518. package/src/self-implement/gate-remote.ts +40 -25
  519. package/src/self-implement/goal-author-decompose.ts +275 -47
  520. package/src/self-implement/goal-author-identity.ts +66 -0
  521. package/src/self-implement/goal-author-preservation-obligation.ts +35 -0
  522. package/src/self-implement/goal-author.ts +254 -120
  523. package/src/self-implement/goal-elaborate.ts +295 -0
  524. package/src/self-implement/goal-refine.ts +270 -0
  525. package/src/self-implement/graph-overlay-yaml.ts +28 -2
  526. package/src/self-implement/graph-unit-flatten.ts +103 -0
  527. package/src/self-implement/graph-unit-resolver.ts +107 -0
  528. package/src/self-implement/graph-yaml.ts +151 -1
  529. package/src/self-implement/headless-elanous-driver.ts +1 -0
  530. package/src/self-implement/knowhow-pack.ts +103 -0
  531. package/src/self-implement/orchestrator.ts +160 -45
  532. package/src/self-implement/pod-warm-pool-profile.ts +45 -0
  533. package/src/self-implement/review-mustfix-class.ts +203 -0
  534. package/src/self-implement/rework-budget-judgment.ts +5 -8
  535. package/src/self-implement/run-stop.ts +17 -2
  536. package/src/self-implement/running-runs.ts +29 -1
  537. package/src/self-implement/scope-adjacency.ts +113 -0
  538. package/src/self-implement/seams.ts +138 -4
  539. package/src/self-implement/stop-autoheal.ts +112 -2
  540. package/src/session/chat.ts +27 -0
  541. package/src/session-runtime/index.ts +1 -1
  542. package/src/session-runtime/retry-policy.ts +2 -1
  543. package/src/setup/llm-env-detect.ts +5 -0
  544. package/src/skills/tools/agent.ts +140 -14
  545. package/src/skills/tools/msg-ask.ts +100 -0
  546. package/src/skills/tools/ops-ledger.ts +2 -1
  547. package/src/skills/tools/schedule-wakeup.ts +66 -17
  548. package/src/steward/hold-hygiene.ts +24 -6
  549. package/src/steward/release-day.ts +7 -3
  550. package/src/steward/triage-budget.ts +194 -0
  551. package/src/steward/triage.ts +57 -7
  552. package/src/task-agent/actions.ts +69 -1
  553. package/src/task-agent/green-sweep.ts +4 -0
  554. package/src/task-agent/harvest-unit.ts +141 -0
  555. package/src/task-agent/live-moves.ts +257 -15
  556. package/src/task-agent/parent-host.ts +216 -6
  557. package/src/task-agent/postland-card.ts +80 -1
  558. package/src/task-agent/rejudge-on-head.ts +112 -26
  559. package/src/task-agent/repair-branch-cas.ts +4 -1
  560. package/src/task-agent/repair-cycle.ts +16 -3
  561. package/src/task-agent/run-identity.ts +4 -3
  562. package/src/task-agent/shadow.ts +19 -3
  563. package/src/task-agent/task-hand.ts +96 -36
  564. package/src/task-agent/tier-split.ts +127 -0
  565. package/src/task-agent/work-order-build.ts +24 -0
  566. package/src/task-agent/work-order.ts +3 -2
  567. package/src/task-orchestrator/generator-prompt.ts +60 -11
  568. package/src/task-orchestrator/surfaces/pod-account-broker.ts +72 -3
  569. package/src/task-orchestrator/surfaces/pod-child-providers.ts +1 -1
  570. package/src/task-orchestrator/surfaces/pod-command-job.ts +47 -8
  571. package/src/task-orchestrator/surfaces/pod-image-source.ts +89 -1
  572. package/src/task-orchestrator/surfaces/pod-lease.ts +17 -8
  573. package/src/task-orchestrator/surfaces/pod-lifecycle-stages.ts +86 -8
  574. package/src/task-orchestrator/surfaces/pod-manifest-test-normalize.ts +8 -2
  575. package/src/task-orchestrator/surfaces/pod-pool.ts +73 -17
  576. package/src/task-orchestrator/surfaces/pod-provenance.ts +76 -0
  577. package/src/task-orchestrator/surfaces/pod-source-receive.ts +72 -4
  578. package/src/task-orchestrator/surfaces/pod-warm-provenance.ts +81 -0
  579. package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +32 -5
  580. package/src/task-orchestrator/surfaces/pod-warm-worker-protocol.ts +3 -0
  581. package/src/task-orchestrator/surfaces/pod-warm-worker-provider.ts +421 -0
  582. package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +2 -1
  583. package/src/task-orchestrator/surfaces/self-implement-pod.ts +588 -71
  584. package/src/task-orchestrator/surfaces/self-implement.ts +106 -2
  585. package/src/tool-runtime/ops-ledger-runtimes.ts +10 -2
  586. package/src/tool-runtime/truncation-store.ts +4 -0
  587. package/src/tui/README.md +2 -0
  588. package/src/tui/chat/background-job-status.ts +54 -0
  589. package/src/tui/chat/log-view.ts +80 -0
  590. package/src/tui/entry.ts +406 -0
  591. package/src/tui/input/composer.ts +144 -0
  592. package/src/tui/seat-mail-boot.ts +59 -0
  593. package/src/tui/slash/compact.ts +100 -0
  594. package/src/tui/slash/new-session.ts +35 -0
  595. package/src/tui-client/dashboard-session.ts +10 -1
  596. package/src/tui-client/local-chat-turn.ts +239 -0
  597. package/src/tui-client/local-harness-command.ts +63 -0
  598. package/src/tui-client/local-provider-command.ts +184 -0
  599. package/src/user-config/coercion.ts +36 -0
  600. package/src/user-config.ts +22 -43
  601. package/src/ux-sim/event-monkey.ts +378 -0
  602. package/src/ux-sim/fake-bin.ts +42 -7
  603. package/src/ux-sim/fault-nextmove-dispatch.ts +266 -0
  604. package/src/ux-sim/fault-pod-codex-503.ts +211 -0
  605. package/src/ux-sim/fault-scope-escape-lift.ts +189 -0
  606. package/src/ux-sim/scenario.ts +72 -13
  607. package/src/version/packed-revision.json +1 -1
  608. package/apps/pwa/out/_next/static/chunks/1034-8b639cee57b8e4a8.js +0 -1
  609. package/apps/pwa/out/_next/static/chunks/1085-6c2d35526f11f840.js +0 -1
  610. package/apps/pwa/out/_next/static/chunks/1220.1ff7d5cdc34b1ae5.js +0 -27
  611. package/apps/pwa/out/_next/static/chunks/1247.5884ec81b39fe2e7.js +0 -287
  612. package/apps/pwa/out/_next/static/chunks/1302.87681521a7a4661e.js +0 -62
  613. package/apps/pwa/out/_next/static/chunks/1324-59dae3002413623b.js +0 -1
  614. package/apps/pwa/out/_next/static/chunks/1343-f40b5870ac1eda5a.js +0 -1
  615. package/apps/pwa/out/_next/static/chunks/146-b6cd56be11dda017.js +0 -1
  616. package/apps/pwa/out/_next/static/chunks/1654.32de4b324981a188.js +0 -23
  617. package/apps/pwa/out/_next/static/chunks/1834-98b64172bc1efe77.js +0 -1
  618. package/apps/pwa/out/_next/static/chunks/1849.1ce38efa6c527d9a.js +0 -149
  619. package/apps/pwa/out/_next/static/chunks/1913.7cc6fb6055b1ed3f.js +0 -41
  620. package/apps/pwa/out/_next/static/chunks/2030.41fae98c7e907304.js +0 -1
  621. package/apps/pwa/out/_next/static/chunks/2099-08b2f2dfba742c9b.js +0 -1
  622. package/apps/pwa/out/_next/static/chunks/2112-e4efc21a6505d5bc.js +0 -1
  623. package/apps/pwa/out/_next/static/chunks/2134.ecee03a550ee7250.js +0 -1
  624. package/apps/pwa/out/_next/static/chunks/2236.66061832181cb745.js +0 -1
  625. package/apps/pwa/out/_next/static/chunks/2422.b0bd272e7020d7e6.js +0 -1
  626. package/apps/pwa/out/_next/static/chunks/2534.e85ae21ad0dac829.js +0 -1
  627. package/apps/pwa/out/_next/static/chunks/2898.fd781bc7cf01a7b7.js +0 -1
  628. package/apps/pwa/out/_next/static/chunks/3040.92ff9ffc9f971cb9.js +0 -115
  629. package/apps/pwa/out/_next/static/chunks/3144.6861096c7b4edef7.js +0 -64
  630. package/apps/pwa/out/_next/static/chunks/3541-ce3a5424ed5ad7f1.js +0 -1
  631. package/apps/pwa/out/_next/static/chunks/3583.a05e88cc8d594628.js +0 -1
  632. package/apps/pwa/out/_next/static/chunks/3771.e928edf2867d28c0.js +0 -1
  633. package/apps/pwa/out/_next/static/chunks/4063-03284b5de59965f3.js +0 -1
  634. package/apps/pwa/out/_next/static/chunks/4300-c7ec4eeabbb5f3f7.js +0 -1
  635. package/apps/pwa/out/_next/static/chunks/4483.646d5065199e1b55.js +0 -36
  636. package/apps/pwa/out/_next/static/chunks/4519.28199ff26b2fd80d.js +0 -1
  637. package/apps/pwa/out/_next/static/chunks/4581.ef2443578054544f.js +0 -1
  638. package/apps/pwa/out/_next/static/chunks/4608-8d894e9cbe4f08bf.js +0 -1
  639. package/apps/pwa/out/_next/static/chunks/4639.ae48a01d1bb50a93.js +0 -286
  640. package/apps/pwa/out/_next/static/chunks/4739.08742ac668aada67.js +0 -77
  641. package/apps/pwa/out/_next/static/chunks/4745.67d6f99c245af3f7.js +0 -226
  642. package/apps/pwa/out/_next/static/chunks/4882-16cef71d6a1a32be.js +0 -1
  643. package/apps/pwa/out/_next/static/chunks/4999.c064a4000c2a9e94.js +0 -79
  644. package/apps/pwa/out/_next/static/chunks/5260-69f35419bca263e7.js +0 -1
  645. package/apps/pwa/out/_next/static/chunks/5272.b92d1b72a15c25fe.js +0 -1
  646. package/apps/pwa/out/_next/static/chunks/5304.04d24756f242a094.js +0 -148
  647. package/apps/pwa/out/_next/static/chunks/5415-997980f828667736.js +0 -1
  648. package/apps/pwa/out/_next/static/chunks/5458-16cd7a2b51b74c8c.js +0 -1
  649. package/apps/pwa/out/_next/static/chunks/5543.a438b8ab5a13ea47.js +0 -1
  650. package/apps/pwa/out/_next/static/chunks/5572-42c9084f27ca761e.js +0 -1
  651. package/apps/pwa/out/_next/static/chunks/5661-80470aac963d9f36.js +0 -1
  652. package/apps/pwa/out/_next/static/chunks/5723.421338408b5c30c4.js +0 -74
  653. package/apps/pwa/out/_next/static/chunks/5809.64c3664267c50bce.js +0 -1
  654. package/apps/pwa/out/_next/static/chunks/6000.64bc21ac8627f2f5.js +0 -94
  655. package/apps/pwa/out/_next/static/chunks/6013.39459b9c68eabf5f.js +0 -1
  656. package/apps/pwa/out/_next/static/chunks/607-06466dff012b03ef.js +0 -1
  657. package/apps/pwa/out/_next/static/chunks/6192.a190b989b81e9743.js +0 -1
  658. package/apps/pwa/out/_next/static/chunks/6323.8672b4817a306928.js +0 -1
  659. package/apps/pwa/out/_next/static/chunks/6444.7d1fdcb9b09ae71c.js +0 -1
  660. package/apps/pwa/out/_next/static/chunks/6517.4147878ac06d584d.js +0 -90
  661. package/apps/pwa/out/_next/static/chunks/6532.e34302010b2f0b68.js +0 -32
  662. package/apps/pwa/out/_next/static/chunks/6546.74bc4ed82f19016d.js +0 -104
  663. package/apps/pwa/out/_next/static/chunks/6684-e7523ce7c2fad075.js +0 -1
  664. package/apps/pwa/out/_next/static/chunks/6726.ebdc82feb161431e.js +0 -1
  665. package/apps/pwa/out/_next/static/chunks/6739.00fb6f892e5e392b.js +0 -1
  666. package/apps/pwa/out/_next/static/chunks/6745.934c4921bf15513a.js +0 -1
  667. package/apps/pwa/out/_next/static/chunks/6767.ea607cf0c20a25af.js +0 -24
  668. package/apps/pwa/out/_next/static/chunks/6843.67bfd020da0926c5.js +0 -93
  669. package/apps/pwa/out/_next/static/chunks/6948.1062d4594652b5bf.js +0 -1
  670. package/apps/pwa/out/_next/static/chunks/7138-eaf841fc1dc03463.js +0 -1
  671. package/apps/pwa/out/_next/static/chunks/7174-cda29ee13d7790e7.js +0 -1
  672. package/apps/pwa/out/_next/static/chunks/7304-3c07583f4e405f4f.js +0 -1
  673. package/apps/pwa/out/_next/static/chunks/7557.e325503a090a4310.js +0 -8
  674. package/apps/pwa/out/_next/static/chunks/7569-9ce9432c7f10989f.js +0 -1
  675. package/apps/pwa/out/_next/static/chunks/7886-55854585fadeded1.js +0 -1
  676. package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +0 -1
  677. package/apps/pwa/out/_next/static/chunks/8215-1661812b6e9ad71f.js +0 -1
  678. package/apps/pwa/out/_next/static/chunks/8260-19c921a51ca8ec3e.js +0 -1
  679. package/apps/pwa/out/_next/static/chunks/8440.3a5cf5c85aa76db8.js +0 -1
  680. package/apps/pwa/out/_next/static/chunks/8519-4599be2249161e68.js +0 -1
  681. package/apps/pwa/out/_next/static/chunks/8547.b028b90c5c01ace6.js +0 -1
  682. package/apps/pwa/out/_next/static/chunks/8587-cbe02a953a7f4990.js +0 -1
  683. package/apps/pwa/out/_next/static/chunks/877-2b977259c4b15655.js +0 -1
  684. package/apps/pwa/out/_next/static/chunks/9002.e5658224e3dc1b6f.js +0 -24
  685. package/apps/pwa/out/_next/static/chunks/9019-0a4b40c50c7314fb.js +0 -1
  686. package/apps/pwa/out/_next/static/chunks/9138.b6e80031afb1d973.js +0 -1
  687. package/apps/pwa/out/_next/static/chunks/9188-d36e4fc869ddc166.js +0 -1
  688. package/apps/pwa/out/_next/static/chunks/9230.44f5039e796e4ab4.js +0 -1
  689. package/apps/pwa/out/_next/static/chunks/9279-d28c5b7754d010ae.js +0 -1
  690. package/apps/pwa/out/_next/static/chunks/939.336acce73e98482c.js +0 -1
  691. package/apps/pwa/out/_next/static/chunks/9407.e49124dedf0f5e72.js +0 -1
  692. package/apps/pwa/out/_next/static/chunks/9610.c07cce4bfcc14ed7.js +0 -1
  693. package/apps/pwa/out/_next/static/chunks/9667-ce78c2ce91050ff9.js +0 -1
  694. package/apps/pwa/out/_next/static/chunks/9989.3694300e5f000b14.js +0 -1
  695. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-339fabb3499da0e1.js +0 -1
  696. package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +0 -1
  697. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-a9717a67c2eecbaf.js +0 -1
  698. package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +0 -1
  699. package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +0 -1
  700. package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +0 -1
  701. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c0f1ca371f710b06.js +0 -1
  702. package/apps/pwa/out/_next/static/chunks/app/chat/page-46430d45a9e04eac.js +0 -1
  703. package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +0 -1
  704. package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +0 -1
  705. package/apps/pwa/out/_next/static/chunks/app/design-check/page-8ea49a4c6dd2c6e7.js +0 -1
  706. package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +0 -1
  707. package/apps/pwa/out/_next/static/chunks/app/exec/page-0e5a41fac1c75396.js +0 -1
  708. package/apps/pwa/out/_next/static/chunks/app/field/page-31aaf1ed37c6df9a.js +0 -1
  709. package/apps/pwa/out/_next/static/chunks/app/healthz/route-339fabb3499da0e1.js +0 -1
  710. package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +0 -1
  711. package/apps/pwa/out/_next/static/chunks/app/intake/page-c69247e7134d329d.js +0 -1
  712. package/apps/pwa/out/_next/static/chunks/app/layout-66dc5cd3a0999abe.js +0 -1
  713. package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +0 -1
  714. package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +0 -1
  715. package/apps/pwa/out/_next/static/chunks/app/loops/page-d256f5d5d9552823.js +0 -1
  716. package/apps/pwa/out/_next/static/chunks/app/market/page-ce8f1907b8a6101c.js +0 -1
  717. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +0 -1
  718. package/apps/pwa/out/_next/static/chunks/app/not-found-7607cca9b84950c6.js +0 -1
  719. package/apps/pwa/out/_next/static/chunks/app/observatory/page-eeab86176b03dc3b.js +0 -1
  720. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5b93dbae3432b996.js +0 -1
  721. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +0 -1
  722. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-f47e768b5b242b5a.js +0 -1
  723. package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +0 -1
  724. package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +0 -1
  725. package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +0 -1
  726. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-95306f892aa2d0ee.js +0 -1
  727. package/apps/pwa/out/_next/static/chunks/app/sessions/page-940fb6c217f6b470.js +0 -1
  728. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-8c66a3b24a745630.js +0 -1
  729. package/apps/pwa/out/_next/static/chunks/app/settings/page-7b675dfd35c36fc9.js +0 -1
  730. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8bd0d3cf7665bcfc.js +0 -1
  731. package/apps/pwa/out/_next/static/chunks/app/setup/layout-339fabb3499da0e1.js +0 -1
  732. package/apps/pwa/out/_next/static/chunks/app/setup/page-c219f2ed171131be.js +0 -1
  733. package/apps/pwa/out/_next/static/chunks/app/share/page-40f8fa21bdae955b.js +0 -1
  734. package/apps/pwa/out/_next/static/chunks/app/showroom/page-468f8313e4f1fbd9.js +0 -1
  735. package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +0 -1
  736. package/apps/pwa/out/_next/static/chunks/app/today/page-4214b855d9233ea8.js +0 -1
  737. package/apps/pwa/out/_next/static/chunks/app/trace/page-936752c7aa58762e.js +0 -1
  738. package/apps/pwa/out/_next/static/chunks/app/vault/page-ec052ad359df1797.js +0 -1
  739. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +0 -1
  740. package/apps/pwa/out/_next/static/chunks/app/workflows/page-fad650e4793adb65.js +0 -1
  741. package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +0 -1
  742. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +0 -1
  743. package/apps/pwa/out/_next/static/chunks/e5012ffe.f727e23c231f4342.js +0 -122
  744. package/apps/pwa/out/_next/static/chunks/ea695e3c-1f1d70d740cf3031.js +0 -1
  745. package/apps/pwa/out/_next/static/chunks/ebc7d829.264a0bcb6c750b50.js +0 -1
  746. package/apps/pwa/out/_next/static/chunks/framework-906af9bd029e166c.js +0 -1
  747. package/apps/pwa/out/_next/static/chunks/main-a24847f9f684d469.js +0 -1
  748. package/apps/pwa/out/_next/static/chunks/main-app-7bdcd8aa7c8397d3.js +0 -1
  749. package/apps/pwa/out/_next/static/chunks/pages/_app-6ba46a5066fc226d.js +0 -1
  750. package/apps/pwa/out/_next/static/chunks/pages/_error-a8dc6d07591f2a25.js +0 -1
  751. package/apps/pwa/out/_next/static/chunks/webpack-f13df28c15c55eee.js +0 -1
  752. package/src/cli/g6a-registration-probe.ts +0 -16
  753. /package/apps/pwa/out/_next/static/{IVKr3AlRUVLKsw2fh7eXF → T1mBQqmkwKDLGtyisDCOd}/_ssgManifest.js +0 -0
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
- import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { hostname, tmpdir } from 'node:os';
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
6
  import { dirname, join, resolve } from 'node:path';
7
- import { debug } from '../../src/debug/log.js';
7
+ import { debug, redactSecretText } from '../../src/debug/log.js';
8
8
  import { registerStandaloneLogSink } from '../../src/domains/standalone-log-sink.js';
9
9
  import { landingFreezeMessage, readLandingFreeze } from '../../src/release-loop/landing-freeze.js';
10
10
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
@@ -13,7 +13,12 @@ import { diffFailures, junitFailures, parseFailures, parseTimedOutFailures } fro
13
13
  import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
14
14
  import { loadGateBatchSafe } from './gate-batch-safe';
15
15
  import { baselineFromCutLogs } from './baseline-from-logs';
16
- import { mergePartialFailures, readPartialPlan, testFileOfId, verifyDocsOnlyCarry } from './gate-partial';
16
+ import { mergePartialFailures, readPartialPlanResult, testFileOfId, verifyDocsOnlyCarry } from './gate-partial';
17
+ import { planGateRadiusShadow, shadowActual, writeGateRadiusShadow, type GateRadiusShadowPlan } from './gate-radius-shadow';
18
+ import { planHardRehearsalRadius, partialGateImagesMatch, preflightGateImageDigest } from './gate-radius-live';
19
+ import { actualGatePodImageDigest, gateBaselineReceipt, gateEnvFingerprint, gateGraphPins, gateSourcePins,
20
+ POD_SWEEP_INTEGRATION_ONLY } from './gate-baseline-receipt';
21
+ export { POD_SWEEP_INTEGRATION_ONLY } from './gate-baseline-receipt';
17
22
  import { logPlanWalltime, planDurations, readFileMemory } from './shard-plan';
18
23
  import { defaultGateHostMirrorReaders, resolveGateHostMirror, type GateHostMirror, type GateHostMirrorReaders } from './gate-host-mirror';
19
24
  import { isGateSpawnLaneFile, planGateSpawnSeparatedShards } from './spawn-lane';
@@ -28,19 +33,61 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
28
33
  import { PodPoolScheduler, parsePodPool, checkPodPool, defaultRemoteRun, type PodPoolMember } from '../../src/task-orchestrator/surfaces/pod-pool.js';
29
34
  import { POD_INSTALL_SLOTS_DEFAULT, POD_INSTALL_SLOTS_HOST_PATH, installSlotScript } from '../../src/task-orchestrator/surfaces/pod-install-slots.js';
30
35
  import { gateDepsInstallScript, parseGateDeps } from '../../src/task-orchestrator/surfaces/pod-gate-deps.js';
31
- import { writeWarmJobManifest } from './gate-warm-artifacts.js';
36
+ import { warmReferenceFromJobArtifacts, writeWarmJobManifest } from './gate-warm-artifacts.js';
37
+ import { assessWarmWorkerShadow, parseWarmWorkerReply, type WarmWorkerCommand, type WarmWorkerFileResult, type WarmWorkerKey } from '../../src/task-orchestrator/surfaces/pod-warm-worker-protocol.js';
38
+ import type { WarmPodTransportResult } from '../../src/task-orchestrator/surfaces/pod-warm-worker-pod.js';
39
+ import { requestWarmPodReplay, finalizeWarmPodReplay, quarantineWarmPodReplay, type WarmReplayResult } from '../../src/task-orchestrator/surfaces/pod-warm-worker-provider.js';
40
+ import { resolveWarmPoolProfile } from '../../src/self-implement/pod-warm-pool-profile.js';
41
+ import { decideWarmReuse, type WarmReuseSwitchConfig, type WarmReuseEvidence } from '../../src/task-orchestrator/surfaces/pod-warm-switch.js';
42
+ import { getUserConfig } from '../../src/user-config.js';
32
43
  import { gateShardsPath, recordGateShardsVerdict, writeGateShards, type GateShard } from '../../src/release-loop/gate-shards.js';
33
44
  import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
34
45
  import { executeTuiNodeCheck, type ExecutedTuiCheck } from './tui-node-checks.js';
46
+ import { runWithReleaseGateTraffic } from './gate-traffic-mode.js';
35
47
  import { emitPodLifecycle, gatePodLifecycleEvent, gatePodOutcome } from '../../src/task-orchestrator/surfaces/pod-lifecycle-stages.js';
36
48
  import { gateHostReservationRoot, reserveReleaseGateHosts } from '../../src/release-loop/gate-host-reservation.js';
37
49
  import { normRoot } from '../../src/instance/resolve.js';
38
50
 
51
+ export interface WarmShadowObservation {
52
+ files: string[];
53
+ attemptId: string;
54
+ reason: string;
55
+ status: 'observed' | 'fallback' | 'unmeasured';
56
+ quarantined: 'yes' | 'no' | 'failed' | 'unavailable';
57
+ finalized?: boolean;
58
+ /** Exact producer identity; absent on an old verdict means the sample cannot enable reuse. */
59
+ cutSha?: string;
60
+ baselineSha?: string;
61
+ workerKey?: WarmWorkerKey;
62
+ }
63
+
64
+ /** A provider may fill only lease identity; it cannot rewrite the Job attempt being compared. */
65
+ export function warmCommandMatchesRequest(command: WarmWorkerCommand, request: Omit<WarmWorkerCommand, 'leaseId' | 'generation' | 'workerId' | 'key'>): boolean {
66
+ return command.protocol === request.protocol && command.runId === request.runId
67
+ && command.attemptId === request.attemptId && command.cutSha === request.cutSha
68
+ && command.baselineSha === request.baselineSha && command.deadlineAt === request.deadlineAt
69
+ && command.envProfile === request.envProfile && command.files.length === request.files.length
70
+ && command.files.every((file, index) => file === request.files[index]);
71
+ }
72
+
39
73
  interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }>; timedOut?: boolean;
74
+ /** Identity and complete selected inventory of this Pod sweep; only the producer may issue it. */
75
+ podSweep?: { id: string; shards: number; files: string[] };
76
+ /** Complete, source-bound one-file Job result for shadow comparison; absent means unmeasured. */
77
+ warmReference?: WarmWorkerFileResult | null;
78
+ warmAttemptId?: string;
79
+ warmShadow?: WarmShadowObservation;
40
80
  /** GATE-POD-STALL-FAILFAST ① — shard Pods whose tests the mid-run watch aborted in this sweep. */
41
81
  abortedShards?: number;
42
82
  /** Each contributing Pod leaf already passed failuresOf; repeated printed ids cannot add new failures. */
43
83
  validatedShards?: boolean }
84
+ function redactedGateLogTail(input: string): string {
85
+ // Redact before trimming: a truncated token might no longer match the redaction rule.
86
+ const bytes = Buffer.from(redactSecretText(input), 'utf8');
87
+ let start = Math.max(0, bytes.length - 16_384);
88
+ while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start++;
89
+ return bytes.subarray(start).toString('utf8');
90
+ }
44
91
  export interface GateRunner {
45
92
  command(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
46
93
  localCommand(cmd: string, args: string[], cwd: string, limitMs?: number): Promise<CommandResult>;
@@ -59,6 +106,10 @@ export interface GateRunner {
59
106
  }
60
107
  export interface GateOptions {
61
108
  commit: string;
109
+ /** The graph run that launched this gate; direct hard rehearsal instead pins source bytes explicitly. */
110
+ graphRunId?: string;
111
+ /** Only a direct hard rehearsal may pin graph bytes from its measured Git commit. */
112
+ rehearsalBaseline?: boolean;
62
113
  forceFreeze?: boolean;
63
114
  version: string;
64
115
  baselineVersion?: string;
@@ -76,6 +127,22 @@ export interface GateOptions {
76
127
  recheckFileLimit?: number;
77
128
  /** freshShards — GATE-SHARD-RESUME off: every shard starts a Pod even when this logDir already holds its result. */
78
129
  pod?: { pool: string; shards?: number; shardTimeoutSeconds?: number; durationSource?: string; bunCache?: string; freshShards?: boolean;
130
+ /** Opt-in gate shadow: provider fills the fenced lease identity. The Job remains authoritative. */
131
+ warmReplay?: (input: Omit<WarmWorkerCommand, 'leaseId' | 'generation' | 'workerId' | 'key'>) => Promise<{ command: WarmWorkerCommand | null; transport: WarmPodTransportResult }>;
132
+ warmQuarantine?: (command: WarmWorkerCommand, reason: string) => Promise<void>;
133
+ warmFinalize?: (command: WarmWorkerCommand) => Promise<void>;
134
+ /** Set only on the cut recheck; baseline Jobs never consume the cut worker. */
135
+ warmCutSha?: string;
136
+ warmBaselineSha?: string;
137
+ warmReuseConfig?: WarmReuseSwitchConfig;
138
+ /** At most three cut isolation files may request a shadow replay per verdict. */
139
+ warmEligible?: boolean;
140
+ /** An explicitly excluded file cannot race for the shared replay budget. */
141
+ warmReplayOwner?: boolean;
142
+ /** Internal budget shared by every recheck in one gate verdict, including timeout retries. */
143
+ warmShadowScope?: { requested: boolean; attempts?: number };
144
+ /** Internal shadow wait cap; a stuck provider cannot hold the Job verdict (tests may lower it). */
145
+ warmShadowTimeoutMs?: number;
79
146
  /** Maximum concurrent file-isolation fallback Jobs across the entire sweep; omitted = 4. */
80
147
  isolationConcurrency?: number;
81
148
  /** GATE-SKIP-KNOWN-RETRY — failure ids the previous release already had (and env-known ones): a shard that fails only
@@ -104,6 +171,9 @@ export interface GateOptions {
104
171
  liveLog?: (job: { name: string; context?: string }) => string | null;
105
172
  /** false = the stop did not take; the watch asks again every poll until it does (void = taken). */
106
173
  stopJob?: (job: { name: string; context?: string }) => boolean | void;
174
+ /** Failure-only Pod status and log reader; injected tests never call the live cluster. */
175
+ failureEvidence?: (job: { name: string; context?: string }) => { podName?: string; termination?: { reason?: string; message?: string; exitCode?: number; finishedAt?: string }; logTail?: string; error?: string };
176
+ failureKubectl?: typeof defaultKubectl;
107
177
  /** How often the stall watch reads the live log (ms); omitted = {@link GATE_POD_STALL_POLL_MS}. */
108
178
  stallPollMs?: number;
109
179
  /** GATE-POD-STALL-FAILFAST ① — seconds a shard may go without its part logs growing once test output began, before
@@ -124,6 +194,16 @@ export interface GateOptions {
124
194
  /** GATE-WARM-BOOT-SCAN — observation only: the sweep's test selection (list · filter · duration plan · shard plan) time. */
125
195
  onSelectionTimed?: (timed: { ms: number; files: number }) => void };
126
196
  }
197
+
198
+ /** Gate-profile opt-in wires the producer only for the process entry, never a test runner seam. */
199
+ export function withWarmGateHooks(opts: GateOptions, config: unknown, injectedRunner = false): GateOptions {
200
+ if (injectedRunner || !opts.pod || resolveWarmPoolProfile(config) !== 'gate') return opts;
201
+ const warmReuseConfig = config && typeof config === 'object' && 'pod' in config
202
+ && config.pod && typeof config.pod === 'object' && 'warmReuse' in config.pod
203
+ ? config.pod.warmReuse as WarmReuseSwitchConfig : undefined;
204
+ return { ...opts, pod: { ...opts.pod, warmReplay: requestWarmPodReplay,
205
+ warmFinalize: finalizeWarmPodReplay, warmQuarantine: quarantineWarmPodReplay, warmReuseConfig } };
206
+ }
127
207
  export interface GateResult {
128
208
  outcome: 'ok' | 'regression' | 'error';
129
209
  commit: string;
@@ -137,6 +217,8 @@ export interface GateResult {
137
217
  rcNonzeroNoFailures: Array<{ file: string; rc: number; pass: number; baseline: 'same' | 'clean' | 'missing' }>;
138
218
  /** Files left unmeasured after isolated recheck; these cannot seed a reusable baseline. */
139
219
  unmeasuredRecheckFiles?: string[];
220
+ /** Shadow only: Job verdict is unchanged even on a worker mismatch or failed quarantine. */
221
+ warmShadow?: WarmShadowObservation[];
140
222
  /** GATE-POD-STALL-FAILFAST ① — cut sweep shard Pods ended by the mid-run abort (their files went the timeout way). */
141
223
  abortedShards?: number;
142
224
  baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
@@ -149,9 +231,17 @@ export interface GateResult {
149
231
  selectionMs?: number;
150
232
  error?: string;
151
233
  stalledShards?: Array<{ shard: number; files: string[]; reason: 'incomplete' | 'no-output' | 'job-failed' | 'exit-code' | 'unattributed' | 'timeout'; lastFile?: string; summaryFailures?: number; namedFailures?: number; detail?: string;
234
+ /** Pool member that ran the final unmeasured attempt; enables a bounded cross-host tail remeasure. */
235
+ lastHost?: string;
236
+ /** Failure of the optional cross-host tail attempt; original stall fields remain authoritative. */
237
+ tailRemeasure?: { reason: string; host?: string; detail?: string };
152
238
  /** GATE-POD-STALL-FAILFAST — the shard (and its one fresh retry) printed no test output within the stall window: unmeasured. */
153
239
  stall?: { seconds: number; sinceCheckoutMs: number } }>;
154
240
  partialSummary?: { pass: number; fail: number; errors: number; ran: number; files: number };
241
+ /** Number of complete shards retained when an incomplete sweep is resumed. */
242
+ preservedShardCount?: number;
243
+ /** Completed shard measurements retained when another shard stalled, so same-commit resume can reuse them safely. */
244
+ partialFailures?: SweepFailures;
155
245
  /** GATE-PARTIAL — this verdict re-ran only these files and carried the prior gate's results for the rest. */
156
246
  partial?: { priorCommit: string; files: number };
157
247
  /** GATE-INTRO-RECHECK — new failures that were only the «timed out» shape and passed one isolated re-run with a
@@ -169,6 +259,124 @@ const versionPattern = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
169
259
  const failureFile = (root: string, version: string) => join(root, 'release', version, 'gate-failures.json');
170
260
  /** Judged identities live beside the machine release ledger; gate-failures.json keeps its raw baseline schema. */
171
261
  const verdictFile = (root: string, version: string) => join(root, 'release', version, 'gate-verdict.json');
262
+ const WARM_VERDICT_HISTORY_LIMIT = 5;
263
+ const WARM_REPLAY_BUDGET = 3;
264
+ interface WarmVerdictSample { version: string; commit: string; baselineCommit: string; warmShadow: WarmShadowObservation[] }
265
+ const recordValue = (value: unknown): Record<string, unknown> | null =>
266
+ value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
267
+ /** The verdict is atomically replaced; carry a bounded prior-verdict window in that same SSOT. */
268
+ function readWarmVerdictHistory(path: string): WarmVerdictSample[] | undefined {
269
+ if (!existsSync(path)) return [];
270
+ try {
271
+ const current = recordValue(JSON.parse(readFileSync(path, 'utf8')));
272
+ if (!current || !Array.isArray(current.warmHistory)) return undefined;
273
+ const history = [...current.warmHistory, { version: current.version, commit: current.commit,
274
+ baselineCommit: current.baselineCommit, warmShadow: current.warmShadow }].slice(-WARM_VERDICT_HISTORY_LIMIT);
275
+ if (history.some((entry) => { const row = recordValue(entry);
276
+ return !row || typeof row.version !== 'string' || typeof row.commit !== 'string'
277
+ || typeof row.baselineCommit !== 'string' || !Array.isArray(row.warmShadow); })) return undefined;
278
+ return history as WarmVerdictSample[];
279
+ } catch { return undefined; }
280
+ }
281
+ /** Every counted match has a measured, finalized worker with this exact cut, baseline and lock. */
282
+ export function warmEvidenceFromVerdicts(history: readonly WarmVerdictSample[] | undefined,
283
+ version: string, head: string, baseline: string, lock: string): WarmReuseEvidence | undefined {
284
+ if (!history?.length || !lock) return undefined;
285
+ const rows = history.flatMap((verdict) => {
286
+ if (verdict.version !== version || verdict.commit !== head || verdict.baselineCommit !== baseline
287
+ || !Array.isArray(verdict.warmShadow)) return [null];
288
+ return verdict.warmShadow.length ? verdict.warmShadow : [null];
289
+ });
290
+ const attempts = new Set<string>();
291
+ const key = rows.at(-1)?.workerKey;
292
+ if (rows.some((entry) => {
293
+ if (entry === null || !recordValue(entry) || !Array.isArray(entry.files) || !recordValue(entry.workerKey)) return true;
294
+ if (typeof entry.attemptId !== 'string' || !entry.attemptId || attempts.has(entry.attemptId)
295
+ || entry.files.length !== 1 || typeof entry.files[0] !== 'string'
296
+ || !/^(?:[\w.-]+\/)*[\w.-]+\.test\.[cm]?[jt]sx?$/.test(entry.files[0])
297
+ || entry.files[0].split('/').some((part) => part === '.' || part === '..')) return true;
298
+ attempts.add(entry.attemptId);
299
+ return entry.cutSha !== head || entry.baselineSha !== baseline || entry.workerKey?.lockHash !== lock
300
+ || typeof entry.workerKey.context !== 'string' || !entry.workerKey.context
301
+ || typeof entry.workerKey.imageDigest !== 'string' || !entry.workerKey.imageDigest
302
+ || typeof entry.workerKey.architecture !== 'string' || !entry.workerKey.architecture
303
+ || entry.workerKey.context !== key?.context || entry.workerKey.imageDigest !== key?.imageDigest
304
+ || entry.workerKey.architecture !== key?.architecture
305
+ || (entry.reason === 'match' && (entry.status !== 'observed' || entry.finalized !== true || entry.quarantined !== 'no'));
306
+ })) return undefined;
307
+ const recentMismatchCount = rows.filter((entry) => entry!.reason !== 'match').length;
308
+ let consecutiveMatchSamples = 0;
309
+ for (const row of [...rows].reverse()) {
310
+ if (row!.status !== 'observed' || row!.reason !== 'match' || row!.finalized !== true) break;
311
+ consecutiveMatchSamples++;
312
+ }
313
+ const latest = rows.at(-1)!;
314
+ return { consecutiveMatchSamples, recentMismatchCount,
315
+ leaseValid: latest!.finalized === true,
316
+ canaryPassed: latest!.status === 'observed' && latest!.reason === 'match' && latest!.finalized === true,
317
+ // A release cut is the pinned main commit; baselineSha is checked separately above.
318
+ expected: { head, main: head, lock }, worker: { head: latest!.cutSha, main: latest!.cutSha, lock: latest!.workerKey!.lockHash } };
319
+ }
320
+ /** Live reuse is serial and opt-in. Any uncertain lease, reply, or cleanup returns to the fresh Job. */
321
+ async function tryWarmGateAdoption(file: string, cutSha: string, baselineSha: string, expectedKey: WarmWorkerKey,
322
+ pod: NonNullable<GateOptions['pod']>): Promise<CommandResult | undefined> {
323
+ if (!pod.warmReplay || !pod.warmFinalize || !pod.warmQuarantine || !pod.warmShadowScope
324
+ || (pod.warmShadowScope.attempts ?? 0) >= WARM_REPLAY_BUDGET) return undefined;
325
+ pod.warmShadowScope.attempts = (pod.warmShadowScope.attempts ?? 0) + 1;
326
+ pod.warmShadowScope.requested = true;
327
+ const waitMs = pod.warmShadowTimeoutMs ?? 150_000;
328
+ const deadline = Date.now() + waitMs;
329
+ const request = { protocol: 1 as const, runId: randomUUID(), attemptId: randomUUID(), cutSha,
330
+ baselineSha, deadlineAt: deadline - 1_000, files: [file], envProfile: 'gate-v1' };
331
+ let command: WarmWorkerCommand | null = null;
332
+ let timedOut = false;
333
+ const bounded = async <T>(operation: () => Promise<T>): Promise<T> => {
334
+ const remainingMs = deadline - Date.now();
335
+ if (remainingMs <= 0) throw new Error('warm adoption timeout');
336
+ let timer: ReturnType<typeof setTimeout> | undefined;
337
+ try { return await Promise.race([Promise.resolve().then(operation), new Promise<never>((_, reject) => {
338
+ timer = setTimeout(() => reject(new Error('warm adoption timeout')), remainingMs);
339
+ })]); } finally { if (timer) clearTimeout(timer); }
340
+ };
341
+ try {
342
+ const pending = Promise.resolve().then(() => pod.warmReplay!(request));
343
+ void pending.then((late) => {
344
+ if (timedOut && late.command) void pod.warmQuarantine!(late.command, 'late-adoption-after-timeout')
345
+ .catch((error) => debug.log('release-loop.gate', 'warm-adoption-late-quarantine-failed', { file, error: String(error) }));
346
+ }, () => undefined);
347
+ let replay: WarmReplayResult;
348
+ try { replay = await bounded(() => pending); }
349
+ catch (error) { timedOut = true; throw error; }
350
+ command = replay.command;
351
+ if (!command || replay.transport.status !== 'observed' || replay.transport.reason !== 'complete'
352
+ || !warmCommandMatchesRequest(command, request) || command.key.context !== expectedKey.context
353
+ || command.key.imageDigest !== expectedKey.imageDigest || command.key.lockHash !== expectedKey.lockHash
354
+ || command.key.architecture !== expectedKey.architecture) throw new Error('warm adoption lease or identity');
355
+ const reply = parseWarmWorkerReply(replay.transport.reply);
356
+ const part = reply?.files[0];
357
+ if (!reply || !part || reply.files.length !== 1 || part.file !== file || part.rc !== 0 || part.junitComplete !== true
358
+ || part.failureIds.length !== 0 || part.errorIds.length !== 0
359
+ || reply.runId !== command.runId || reply.attemptId !== command.attemptId || reply.cutSha !== cutSha
360
+ || reply.baselineSha !== baselineSha || reply.leaseId !== command.leaseId || reply.generation !== command.generation
361
+ || reply.workerId !== command.workerId || reply.envProfile !== command.envProfile
362
+ || reply.key.context !== command.key.context || reply.key.imageDigest !== command.key.imageDigest
363
+ || reply.key.lockHash !== command.key.lockHash || reply.key.architecture !== command.key.architecture
364
+ || reply.finishedAt < reply.startedAt || reply.finishedAt > command.deadlineAt) throw new Error('warm adoption result');
365
+ await bounded(() => pod.warmFinalize!(command!));
366
+ debug.log('release-loop.gate', 'warm-adopted-recheck', { file, cutSha, attemptId: command.attemptId });
367
+ return { rc: 0, output: `${file}:\n(pass) warm isolated recheck\n1 pass\n0 fail\nRan 1 test across 1 file.\n` };
368
+ } catch (error) {
369
+ if (command) {
370
+ let quarantineTimer: ReturnType<typeof setTimeout> | undefined;
371
+ try { await Promise.race([pod.warmQuarantine!(command, 'adoption-fallback'),
372
+ new Promise<never>((_, reject) => { quarantineTimer = setTimeout(() => reject(new Error('warm quarantine timeout')), 10_000); })]); }
373
+ catch (fenceError) { debug.log('release-loop.gate', 'warm-adoption-quarantine-failed', { file, error: String(fenceError) }); }
374
+ finally { if (quarantineTimer) clearTimeout(quarantineTimer); }
375
+ }
376
+ debug.log('release-loop.gate', 'warm-adoption-fresh-job', { file, reason: String(error) });
377
+ return undefined;
378
+ }
379
+ }
172
380
  const quote = (s: string) => `'${s.replaceAll("'", "'\\''")}'`;
173
381
  const defaultRemoteMirror = '~/mirror/elanous-agent.git';
174
382
 
@@ -787,7 +995,7 @@ export function readShardArtifacts(dir: string, parts: readonly (readonly string
787
995
  export const gateUnfinishedHint = (relativePath: string, bytes: number) => `[gate] unfinished: ${relativePath} · ${bytes}B (진행 중 part 출력은 그 파일)`;
788
996
 
789
997
  // review-model-ab: Pod 에서 출력 없이 멈춤 2/2(10-04 0.2.11 · 로컬 단독 4/0 · 1.7s) — GATE-STALL 수리 전까지 Pod 밖.
790
- export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts', 'scripts/review-model-ab.test.ts'];
998
+ // The shared exclusion list lives in gate-baseline-receipt so radius verification uses the producer's rule.
791
999
  /** Nightly-only whole-repository cases; gate runs the fixture cases in these same files. */
792
1000
  export const GATE_NIGHTLY_AUDITS: readonly string[] = [
793
1001
  // Replaces F12's real-repo bucket B apps/pwa scope check and its whole-repo export partition check.
@@ -948,7 +1156,8 @@ export function readIsolatedRun(run: CommandResult, label: string, file: string)
948
1156
  }
949
1157
 
950
1158
  class StalledPodShards extends Error {
951
- constructor(readonly stalledShards: NonNullable<GateResult['stalledShards']>, readonly partialSummary: NonNullable<GateResult['partialSummary']>, readonly abortedShards = 0) {
1159
+ constructor(readonly stalledShards: NonNullable<GateResult['stalledShards']>, readonly partialSummary: NonNullable<GateResult['partialSummary']>,
1160
+ readonly partialFailures: SweepFailures, readonly preservedShardCount: number, readonly abortedShards = 0) {
952
1161
  const unmeasured = stalledShards.filter((item) => item.stall);
953
1162
  const exitCode = stalledShards.filter((item) => item.reason === 'exit-code');
954
1163
  super(`pod sweep incomplete: stalled shards ${stalledShards.map(({ shard }) => shard).join(', ')}`
@@ -958,6 +1167,33 @@ class StalledPodShards extends Error {
958
1167
  }
959
1168
  }
960
1169
 
1170
+ /** Read completed shard failures without letting one malformed shard hide the stalled-shard verdict. */
1171
+ export function partialFailuresOf(runs: readonly (CommandResult & { files: number })[]): SweepFailures {
1172
+ return runs.reduce<SweepFailures>((sum, run) => {
1173
+ try {
1174
+ const parsed = failuresOf({ rc: run.rc, output: run.output, validatedShards: true }, `pod partial shard ${run.files}`);
1175
+ sum.failures.push(...parsed.failures);
1176
+ sum.errors.push(...parsed.errors);
1177
+ } catch (error) {
1178
+ debug.log('release-loop.gate', 'pod-partial-failures-unreadable', {
1179
+ files: run.files,
1180
+ reason: error instanceof Error ? error.message : String(error),
1181
+ }, { level: 'warn' });
1182
+ }
1183
+ return sum;
1184
+ }, { failures: [], errors: [] });
1185
+ }
1186
+
1187
+ class WarmRecheckIncomplete extends Error {
1188
+ constructor(message: string, readonly file: string, readonly shadow: WarmShadowObservation) { super(message); }
1189
+ }
1190
+
1191
+ class SettledRecheckFailures extends Error {
1192
+ constructor(readonly failures: unknown[]) {
1193
+ super(failures[0] instanceof Error ? failures[0].message : String(failures[0]));
1194
+ }
1195
+ }
1196
+
961
1197
  /** Isolated re-runs (host stall re-run · cut and baseline re-checks) are time-limited; sweeps and installs keep their full time. */
962
1198
  export const GATE_ISOLATED_TIMEOUT_MS = 300_000;
963
1199
  /** Default final recheck window size; overflow continues in later windows. */
@@ -1098,6 +1334,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1098
1334
  }
1099
1335
  : localCommand);
1100
1336
  const podSweep = async (tree: string, logDir: string | undefined, pod: NonNullable<GateOptions['pod']>, only?: readonly string[]): Promise<CommandResult> => {
1337
+ const sweepId = randomUUID();
1101
1338
  const shardCount = pod.shards ?? 24;
1102
1339
  const isolationConcurrency = pod.isolationConcurrency ?? 4;
1103
1340
  // GATE-TIMEOUT-HEAL ①: the configured value is the floor; each shard's own deadline is shardDeadlineSeconds(planned).
@@ -1212,6 +1449,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1212
1449
  let abortedShards = 0;
1213
1450
  // GATE-MULTI-HOST: members found unreachable during this gate — skipped for placement while any other member is left.
1214
1451
  const downMembers = new Set<string>();
1452
+ let tailPlacedHost: string | undefined;
1215
1453
  const multiHost = poolScheduler.members.length > 1;
1216
1454
  const probeMember = pod.probeMember ?? (poolOverride ? undefined : defaultGateMemberProbe);
1217
1455
  const memberUnreadableSeconds = pod.memberUnreadableSeconds ?? GATE_MEMBER_UNREADABLE_SECONDS_DEFAULT;
@@ -1239,6 +1477,33 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1239
1477
  }
1240
1478
  return '';
1241
1479
  };
1480
+ const failureEvidence = pod.failureEvidence ?? ((job: { name: string; context?: string }) => {
1481
+ if (!job.context) return { error: '수거 실패 · Job 배치 context 없음' };
1482
+ const prefix = ['--context', job.context, '--request-timeout=5s', '-n', 'elanous-test'];
1483
+ const errors: string[] = [];
1484
+ let termination: { reason?: string; message?: string; exitCode?: number; finishedAt?: string } | undefined;
1485
+ let podName: string | undefined;
1486
+ let latestEndedAt = -Infinity;
1487
+ const status = (pod.failureKubectl ?? defaultKubectl)([...prefix, 'get', 'pods', '-l', `job-name=${job.name}`, '-o', 'json']);
1488
+ if (status.status !== 0) errors.push(`Pod 상태: ${status.stderr.trim() || `kubectl rc=${status.status}`}`);
1489
+ else try {
1490
+ const pods = JSON.parse(status.stdout) as { items?: Array<{ metadata?: { name?: string; creationTimestamp?: string }; status?: { containerStatuses?: Array<{ name?: string; state?: { terminated?: typeof termination } }> } }> };
1491
+ for (const item of pods.items ?? []) {
1492
+ const child = item.status?.containerStatuses?.find((container) => container.name === 'child');
1493
+ const ended = child?.state?.terminated;
1494
+ if (!ended || !item.metadata?.name) continue;
1495
+ const endedAt = Date.parse(ended.finishedAt ?? item.metadata.creationTimestamp ?? '');
1496
+ const rank = Number.isFinite(endedAt) ? endedAt : -Infinity;
1497
+ if (!termination || rank > latestEndedAt) { podName = item.metadata.name; termination = ended; latestEndedAt = rank; }
1498
+ }
1499
+ if (!termination) errors.push('Pod 상태: child 종료 사유 없음');
1500
+ } catch (error) { errors.push(`Pod 상태 JSON: ${String(error)}`); }
1501
+ const logs = podName ? (pod.failureKubectl ?? defaultKubectl)([...prefix, 'logs', `pod/${podName}`, '-c', 'child', '--tail=100']) : undefined;
1502
+ if (!logs) errors.push('로그: 종료된 Pod 이름 없음');
1503
+ else if (logs.status !== 0) errors.push(`로그: ${logs.stderr.trim() || `kubectl rc=${logs.status}`}`);
1504
+ return { ...(podName ? { podName } : {}), ...(termination ? { termination } : {}), ...(logs?.status === 0 ? { logTail: logs.stdout } : {}),
1505
+ ...(errors.length ? { error: `수거 실패 · ${errors.join(' · ')}` } : {}) };
1506
+ });
1242
1507
  type ShardRun = { output: string; rc: number; pass: number; fail: number; errors: number; ran: number; files: number; passedIds: string[] };
1243
1508
  type ShardReason = NonNullable<GateResult['stalledShards']>[number]['reason'];
1244
1509
  // 깊이 끝 OOM 조각은 «조각 전체»를 파일 하나씩 격리한다 — 상한 40 이면 나머지가 못 잰 채 남았다(09-30 ① 7번 조각 110 파일).
@@ -1287,12 +1552,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1287
1552
  /** The gate's scheduler, seen through one shard: a slot wait and the Pod start become row states. */
1288
1553
  // GATE-MEM-ADMIT: `placed` releases the shard's admission once a member is chosen — from then on the Job is
1289
1554
  // Pending/Running in the cluster and the next admission reading counts it, so holding the lease would count it twice.
1290
- const trackedPool = (shard: number, retry: boolean, placed: (member: NonNullable<Awaited<ReturnType<PodPoolScheduler['tryAcquire']>>>) => void = () => {}): PodPoolScheduler => ({
1555
+ const trackedPool = (shard: number, retry: boolean, placed: (member: NonNullable<Awaited<ReturnType<PodPoolScheduler['tryAcquire']>>>) => void = () => {}, skipMembers: ReadonlySet<string> = new Set()): PodPoolScheduler => ({
1291
1556
  members: poolScheduler.members,
1292
1557
  tryAcquire: async (...[skip, onEmpty, mode]: Parameters<PodPoolScheduler['tryAcquire']>) => {
1293
1558
  // GATE-MULTI-HOST: a lost member is skipped while another member is left (all lost = the old behaviour).
1294
1559
  const anyLeft = poolScheduler.members.some((m) => !downMembers.has(m.context));
1295
- const member = await poolScheduler.tryAcquire(downMembers.size && anyLeft ? new Set([...(skip ?? []), ...downMembers]) : skip, onEmpty, mode);
1560
+ const excluded = new Set([...(skip ?? []), ...skipMembers, ...(downMembers.size && anyLeft ? downMembers : [])]);
1561
+ const member = await poolScheduler.tryAcquire(excluded.size ? excluded : undefined, onEmpty, mode);
1296
1562
  if (member) placed(member);
1297
1563
  if (member) markShard(shard, { state: retry ? 'retry' : 'running', startedAt: board[shard]?.startedAt ?? new Date().toISOString(), host: member.context }, ['waitReason']);
1298
1564
  else markShard(shard, { waitReason: 'Pod 자리 대기' });
@@ -1300,7 +1566,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1300
1566
  },
1301
1567
  release: (member: Parameters<PodPoolScheduler['release']>[0]) => poolScheduler.release(member),
1302
1568
  }) as unknown as PodPoolScheduler;
1303
- const runShard = async (paths: string[], shard: number, depth = 0, branch = '', retriedNoOutput = false, fresh = pod.freshShards === true, parentAttempt?: string, hostRetries = 0, batchIsolation = false): Promise<{ runs: ShardRun[]; stalled: NonNullable<GateResult['stalledShards']> }> => {
1569
+ 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']> }> => {
1304
1570
  // 파일별 소요는 junit 으로 남긴다 — 콘솔 요약(판정 원천)은 그대로이고, 느린 시험 목록(K10 D4)·계층 분리(D2)의 자가 된다.
1305
1571
  // GATE-TIMEOUT-HEAL: parts (each with its own junit) under a soft deadline inside this shard's own Job deadline.
1306
1572
  const shardDeadline = shardDeadlineSeconds(batchIsolation ? Math.max(...paths.map((file) => estimated([file]))) * Math.ceil(paths.length / batchWidth) : estimated(paths), deadlineSeconds);
@@ -1317,6 +1583,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1317
1583
  const stamp = (phase: string) => { phaseTimes[phase] = new Date().toISOString(); };
1318
1584
  let podStages: Record<string, number> | undefined;
1319
1585
  let podLifecycle: Record<string, string> | undefined;
1586
+ let imageDigest: string | null = null;
1320
1587
  let podJobName: string | undefined;
1321
1588
  let batchArtifactsComplete = true;
1322
1589
  let junit: string | undefined;
@@ -1326,6 +1593,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1326
1593
  let lastFile: string | undefined;
1327
1594
  let jobFailed = false;
1328
1595
  let jobError = '';
1596
+ let failureCapture: ReturnType<NonNullable<NonNullable<GateOptions['pod']>['failureEvidence']>> | undefined;
1597
+ let jobNameForCapture: string | undefined;
1598
+ let jobContextForCapture: string | undefined;
1599
+ const captureFailure = () => {
1600
+ try { failureCapture = failureEvidence({ name: jobNameForCapture ?? 'unplaced', context: jobContextForCapture }); }
1601
+ catch (error) { failureCapture = { error: `수거 실패 · ${String(error)}` }; }
1602
+ };
1329
1603
  /** GATE-MULTI-HOST — the member this shard's Job was placed on (recorded in the shard json / log / board). */
1330
1604
  let placedOn: string | undefined;
1331
1605
  let timedOut = false;
@@ -1356,7 +1630,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1356
1630
  if (descent === 'retry') {
1357
1631
  // «descend» is only the way down; «resumed» comes from each leaf whose log reads cleanly (review r4).
1358
1632
  debug.log('release-loop.gate', 'pod-shard-descend', { shard, branch, files: paths.length, via: 'retry' });
1359
- return runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId, hostRetries, batchIsolation);
1633
+ return runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId, hostRetries, batchIsolation, skipMembers);
1360
1634
  }
1361
1635
  if (descent === 'split') {
1362
1636
  output = undefined;
@@ -1376,6 +1650,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1376
1650
  // The admission wait is not the shard's run time: deadline/timeout judgment and the measured duration start here.
1377
1651
  start = Date.now();
1378
1652
  const jobName = `gate-${randomUUID()}`;
1653
+ jobNameForCapture = jobName;
1379
1654
  // The member's context is known once the Job is placed; the live log is read (and the Job stopped) there.
1380
1655
  let jobContext: string | undefined;
1381
1656
  const liveJob = () => ({ name: jobName, ...(jobContext ? { context: jobContext } : {}) });
@@ -1397,7 +1672,17 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1397
1672
  } : {}) }) : undefined;
1398
1673
  const placed = admitted.release;
1399
1674
  const job = await podCommand({
1400
- pool: pod.pool, poolScheduler: trackedPool(shard, depth > 0 || branch !== '', (member) => { stamp('poolPlaced'); jobContext = member.context; placedOn = member.context; placed(); }), clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds: shardDeadline,
1675
+ provenanceGate: true,
1676
+ provenance: { actorKind: process.env.ELANOUS_SEAT ? 'seat' : 'loop', actor: process.env.ELANOUS_SEAT || 'release-loop',
1677
+ character: 'gate-shard-runner', launchHost: hostname(), entrance: process.env.ELANOUS_RELEASE_ENTRANCE?.trim() || 'release-run',
1678
+ kind: 'gate-shard', run: process.env.ELANOUS_RUN_ID || sweepId, phase: 'release-gate', requestedAt: new Date().toISOString(),
1679
+ graphNode: 'gate', version: pod.shardsFile?.version, commit, shard: `${shard + 1}-of-${shards.length}`,
1680
+ reason: `release gate shard ${shard + 1}/${shards.length}`, pool: pod.pool, cpuReq: cpu.request,
1681
+ memReq: memoryLimit ?? POD_ISOLATION_MEMORY_LIMIT, warmReused: false },
1682
+ pool: pod.pool, poolScheduler: trackedPool(shard, depth > 0 || branch !== '', (member) => {
1683
+ stamp('poolPlaced'); jobContext = member.context; jobContextForCapture = member.context; placedOn = member.context; placed();
1684
+ if (branch === '-tail') tailPlacedHost = member.context;
1685
+ }, skipMembers), clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds: shardDeadline,
1401
1686
  ...gateHostMirror(pod),
1402
1687
  ...(multiHost ? { statusUnreadableMs: memberUnreadableSeconds * 1000 } : {}),
1403
1688
  ...(bunCache ? { bunCache } : {}),
@@ -1410,13 +1695,20 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1410
1695
  }).finally(() => watch?.stop());
1411
1696
  stamp('jobReturned');
1412
1697
  podJobName = job.job;
1698
+ // poolOverride is the injected scheduler used by isolated tests; it has no authenticated kubectl
1699
+ // context. Those sweeps intentionally cannot mint a valid baseline receipt (missing observed digest).
1700
+ // Production pool sweeps always use this Kubernetes lookup; lookup failure is receipt-invalid only.
1413
1701
  if (!poolOverride && placedOn) {
1414
1702
  try {
1415
1703
  const got = defaultKubectl(['--context', placedOn, '-n', 'elanous-test', 'get', 'pods', '-l', `job-name=${job.job}`, '-o', 'json']);
1416
- if (got.status === 0) podLifecycle = readGatePodLifecycle(got.stdout);
1704
+ if (got.status === 0) {
1705
+ podLifecycle = readGatePodLifecycle(got.stdout);
1706
+ imageDigest = actualGatePodImageDigest(got.stdout);
1707
+ }
1417
1708
  } catch { /* timings never change the verdict */ }
1418
1709
  }
1419
1710
  jobExitCode = job.exitCode;
1711
+ if (job.exitCode !== 0) captureFailure();
1420
1712
  batchArtifactDir = job.artifactsDir;
1421
1713
  const read = readShardArtifacts(job.artifactsDir, parts);
1422
1714
  batchArtifactsComplete = !batchIsolation || parts.every((_, index) =>
@@ -1450,6 +1742,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1450
1742
  } catch (error) {
1451
1743
  jobFailed = true;
1452
1744
  jobError = error instanceof Error ? error.message : String(error);
1745
+ if (!failureCapture) captureFailure();
1453
1746
  }
1454
1747
  // A launch that failed before placement still gives its admission back (release is idempotent).
1455
1748
  admitted?.release();
@@ -1484,9 +1777,20 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1484
1777
  if (unfinishedLog !== undefined) writeFileSync(unfinishedDestination, unfinishedLog, { mode: 0o600 });
1485
1778
  else rmSync(unfinishedDestination, { force: true });
1486
1779
  writeFileSync(join(logDir, `pod-${shard}${branch}.json`), JSON.stringify({ durationMs, rc: rc ?? null,
1487
- ...(stall ? { reason: 'stall', finishedFiles: 0, stallSeconds: stall.seconds, sinceCheckoutMs: stall.sinceCheckoutMs } : timedOut ? { reason: 'timeout', finishedFiles: finished.length } : {}), files: paths, plannedSeconds: estimated(paths), deadlineSeconds: shardDeadline, shardCount: shards.length, commit, runKey, attemptId, ...(parentAttempt ? { parentAttempt } : {}), jobExitCode: jobExitCode ?? null, jobFailed, ...(placedOn ? { host: placedOn } : {}),
1780
+ ...(stall ? { reason: 'stall', finishedFiles: 0, stallSeconds: stall.seconds, sinceCheckoutMs: stall.sinceCheckoutMs } : timedOut ? { reason: 'timeout', finishedFiles: finished.length } : {}), files: paths, plannedSeconds: estimated(paths), deadlineSeconds: shardDeadline, shardCount: shards.length, sweepId, commit, runKey, attemptId, ...(parentAttempt ? { parentAttempt } : {}), jobExitCode: jobExitCode ?? null, jobFailed, ...(placedOn ? { host: placedOn } : {}),
1488
1781
  ...(Object.keys(phaseTimes).length ? { phaseTimes } : {}), ...(podStages ? { podStages } : {}),
1489
1782
  ...(podLifecycle && Object.keys(podLifecycle).length ? { podLifecycle } : {}),
1783
+ ...(failureCapture ? { failureCapture: {
1784
+ ...(failureCapture.podName ? { podName: failureCapture.podName } : {}),
1785
+ ...(failureCapture.termination ? { termination: {
1786
+ ...failureCapture.termination,
1787
+ ...(failureCapture.termination.reason ? { reason: redactSecretText(failureCapture.termination.reason) } : {}),
1788
+ ...(failureCapture.termination.message ? { message: redactSecretText(failureCapture.termination.message) } : {}),
1789
+ } } : {}),
1790
+ ...(failureCapture.logTail !== undefined ? { logTail: redactedGateLogTail(failureCapture.logTail) } : {}),
1791
+ ...(failureCapture.error ? { error: redactSecretText(failureCapture.error) } : {}),
1792
+ } } : {}),
1793
+ ...(imageDigest ? { imageDigest } : {}),
1490
1794
  ...(fileSeconds !== undefined ? { fileSeconds, testSeconds } : {}), ...(testWallSeconds !== undefined ? { testWallSeconds } : {}),
1491
1795
  ...(podOverheadSeconds !== undefined ? { podOverheadSeconds } : {}),
1492
1796
  ...(unfinishedLogBytes !== undefined ? { unfinishedLogBytes } : {}),
@@ -1560,14 +1864,14 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1560
1864
  // A reused result that the current tool cannot read cleanly is not a verdict: run that shard for real.
1561
1865
  if (reused && reason) {
1562
1866
  debug.log('release-loop.gate', 'pod-shard-resume-miss', { shard, branch, reason });
1563
- return runShard(paths, shard, depth, branch, retriedNoOutput, true, parentAttempt, hostRetries, batchIsolation);
1867
+ return runShard(paths, shard, depth, branch, retriedNoOutput, true, parentAttempt, hostRetries, batchIsolation, skipMembers, singleShotTail);
1564
1868
  }
1565
1869
  // GATE-MULTI-HOST: the Job failed because its member is gone (launch failed · status unreadable) — not the shard's
1566
1870
  // fault, so no split: the same files run again on the members left. Bounded by the roster size.
1567
- if (jobFailed && placedOn && hostRetries < poolScheduler.members.length && memberLost(placedOn, shard, jobError)
1871
+ if (!singleShotTail && jobFailed && placedOn && hostRetries < poolScheduler.members.length && memberLost(placedOn, shard, jobError)
1568
1872
  && poolScheduler.members.some((m) => !downMembers.has(m.context))) {
1569
1873
  debug.log('release-loop.gate', 'pod-shard-host-retry', { shard, branch, files: paths.length, lostMember: placedOn, attempt: hostRetries + 1 }, { level: 'warn' });
1570
- return runShard(paths, shard, depth, `${branch}-h${hostRetries}`, retriedNoOutput, undefined, attemptId, hostRetries + 1, batchIsolation);
1874
+ return runShard(paths, shard, depth, `${branch}-h${hostRetries}`, retriedNoOutput, undefined, attemptId, hostRetries + 1, batchIsolation, skipMembers, singleShotTail);
1571
1875
  }
1572
1876
  // GATE-TIMEOUT-HEAL ④: a Pod that died before any test ran (no outbox · jobExitCode≠0 — 0.2.19: isolated retries
1573
1877
  // ending with exit 5, the source checkout) gets the same one fresh retry as «no output» instead of stalling the sweep.
@@ -1575,13 +1879,18 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1575
1879
  const diedBeforeTests = reason === 'job-failed' && output === undefined && !jobFailed && jobExitCode !== undefined && jobExitCode !== 0
1576
1880
  && jobExitCode !== 137 && (paths.length === 1 || jobExitCode === 5);
1577
1881
  if ((reason === 'no-output' || diedBeforeTests) && !retriedNoOutput) {
1578
- const retry = await runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId, hostRetries, batchIsolation);
1882
+ const retry = await runShard(paths, shard, depth, `${branch}-retry`, true, undefined, attemptId, hostRetries, batchIsolation, skipMembers);
1579
1883
  debug.log('release-loop.gate', 'pod-shard-retry', { shard, files: paths, reason, outcome: retry.stalled[0]?.reason ?? 'ok' });
1580
1884
  return retry;
1581
1885
  }
1582
1886
  // GATE-POD-STALL-FAILFAST: a stall after the fresh retry is unmeasured — no split, no isolation: smaller Pods on the
1583
1887
  // same stalled node only re-measure the stall (10-10: retries and splits repeated it for 2h+).
1584
- if (stall && reason === 'no-output') return { runs: [], stalled: [{ shard, files: paths, reason, stall }] };
1888
+ if (stall && reason === 'no-output') return { runs: [], stalled: [{ shard, files: paths, reason, stall, ...(placedOn ? { lastHost: placedOn } : {}) }] };
1889
+ // GATE-STALLED-TAIL-REMEASURE: the alternate-host probe is a single observation. Do not split, isolate, or
1890
+ // retry its failed result; the sweep retains the original stall and records this attempt separately.
1891
+ if (singleShotTail && reason) return { runs: [], stalled: [{ shard, files: paths, reason,
1892
+ ...(lastFile ? { lastFile } : {}), ...(placedOn ? { lastHost: placedOn } : {}),
1893
+ ...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}) }] };
1585
1894
  // A shared Job may finish some file parts before another part loses its JUnit/log/rc. Keep every complete part's
1586
1895
  // original verdict, including a failure, and rerun only parts without the full artifact trio.
1587
1896
  if (batchIsolation && !batchArtifactsComplete && batchArtifactDir && reason) {
@@ -1714,8 +2023,43 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1714
2023
  const rejected = settled.find((result): result is PromiseRejectedResult => result.status === 'rejected');
1715
2024
  if (rejected) throw rejected.reason;
1716
2025
  const results = settled.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
1717
- const stalledShards = results.flatMap((result) => result.stalled);
2026
+ let stalledShards = results.flatMap((result) => result.stalled);
1718
2027
  const measured = results.flatMap((result) => result.runs);
2028
+ // GATE-STALLED-TAIL-REMEASURE: after the sweep, give only a one-file no-output leaf one fresh attempt on a different
2029
+ // pool member. Keep the original unmeasured evidence if no alternate is available or the remeasure is incomplete.
2030
+ const remainingStalls: typeof stalledShards = [];
2031
+ for (const stalled of stalledShards) {
2032
+ const alternatives = stalled.lastHost
2033
+ ? poolScheduler.members.filter((member) => member.context !== stalled.lastHost && !downMembers.has(member.context))
2034
+ : [];
2035
+ if (!stalled.stall || stalled.reason !== 'no-output' || stalled.files.length !== 1 || !stalled.lastHost || !alternatives.length) {
2036
+ remainingStalls.push(stalled);
2037
+ continue;
2038
+ }
2039
+ debug.log('release-loop.gate', 'pod-stalled-tail-remeasure-start', { shard: stalled.shard, file: stalled.files[0], fromHost: stalled.lastHost,
2040
+ availableHosts: alternatives.map((member) => member.context) });
2041
+ try {
2042
+ tailPlacedHost = undefined;
2043
+ const tail = await runShard(stalled.files, stalled.shard, 0, '-tail', true, true, undefined, 0, false, new Set([stalled.lastHost]), true);
2044
+ if (tail.stalled.length) {
2045
+ const unresolved = tail.stalled[0]!;
2046
+ remainingStalls.push({ ...stalled, tailRemeasure: { reason: unresolved.reason,
2047
+ ...(unresolved.lastHost ?? tailPlacedHost ? { host: unresolved.lastHost ?? tailPlacedHost } : {}),
2048
+ ...(unresolved.detail ? { detail: unresolved.detail } : {}) } });
2049
+ debug.log('release-loop.gate', 'pod-stalled-tail-remeasure-incomplete', { shard: stalled.shard, file: stalled.files[0],
2050
+ fromHost: stalled.lastHost, toHost: unresolved.lastHost ?? null, reason: unresolved.reason }, { level: 'warn' });
2051
+ } else {
2052
+ measured.push(...tail.runs);
2053
+ debug.log('release-loop.gate', 'pod-stalled-tail-remeasure-complete', { shard: stalled.shard, file: stalled.files[0],
2054
+ fromHost: stalled.lastHost, toHost: tailPlacedHost ?? null });
2055
+ }
2056
+ } catch (error) {
2057
+ remainingStalls.push({ ...stalled, tailRemeasure: { reason: 'error', ...(tailPlacedHost ? { host: tailPlacedHost } : {}), detail: String(error).slice(0, 500) } });
2058
+ debug.log('release-loop.gate', 'pod-stalled-tail-remeasure-error', { shard: stalled.shard, file: stalled.files[0],
2059
+ fromHost: stalled.lastHost, error: String(error).slice(0, 500) }, { level: 'warn' });
2060
+ }
2061
+ }
2062
+ stalledShards = remainingStalls;
1719
2063
  const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
1720
2064
  errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
1721
2065
  { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
@@ -1724,15 +2068,35 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1724
2068
  stallSeconds, measuredFiles: total.files }, { level: 'warn' });
1725
2069
  // An unmeasured (stalled) shard is never handed to the host re-run as an environment stall: the gate fails now.
1726
2070
  if (abortedShards) debug.log('release-loop.gate', 'pod-sweep-aborted', { shards: abortedShards, midRunStallSeconds }, { level: 'warn' });
1727
- if (stalledShards.some((item) => item.reason !== 'no-output' || item.files.length !== 1 || item.stall)) throw new StalledPodShards(stalledShards, total, abortedShards);
2071
+ if (stalledShards.some((item) => item.reason !== 'no-output' || item.files.length !== 1 || item.stall)) {
2072
+ const measuredFailures = partialFailuresOf(measured);
2073
+ throw new StalledPodShards(stalledShards, total, measuredFailures,
2074
+ results.filter((item) => item.stalled.length === 0).length, abortedShards);
2075
+ }
1728
2076
  if (total.ran === 0 && total.errors === 0 && !stalledShards.length) throw new Error('sweep incomplete: no tests ran');
1729
2077
  const outputs = measured.map((run) => run.output.replace(/(?:^|\n)\s*\d+ (?:pass|fail|errors?)\s*(?=\n|$)/g, '\n').replace(/Ran \d+ tests? across \d+ files?\.?/g, ''));
1730
2078
  return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n`, validatedShards: true,
2079
+ podSweep: { id: sweepId, shards: shards.length, files: assignable },
1731
2080
  passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort(),
1732
2081
  stalledEnv: stalledShards.flatMap((item) => item.files.map((file) => ({ file, reason: 'no-output' as const }))),
1733
2082
  ...(abortedShards ? { abortedShards } : {}) };
1734
2083
  };
1735
2084
  const runPodRecheckBatch = async (files: readonly string[], commit: string, pod: NonNullable<GateOptions['pod']>, timeoutMs: number, relaxed = false): Promise<Map<string, CommandResult>> => {
2085
+ const warmShadowScope = pod.warmShadowScope ?? { requested: false };
2086
+ const shadowTimeoutMs = pod.warmShadowTimeoutMs ?? 150_000;
2087
+ if (!Number.isSafeInteger(shadowTimeoutMs) || shadowTimeoutMs < 1 || shadowTimeoutMs > 150_000)
2088
+ throw new Error('invalid warm shadow timeout');
2089
+ let shadowDeadline = 0;
2090
+ const boundedShadow = async <T>(phase: string, operation: () => Promise<T>): Promise<T> => {
2091
+ const remainingMs = shadowDeadline - Date.now();
2092
+ if (remainingMs <= 0) throw new Error(`warm ${phase} timeout after ${shadowTimeoutMs}ms`);
2093
+ let timer: ReturnType<typeof setTimeout> | undefined;
2094
+ try {
2095
+ return await Promise.race([Promise.resolve().then(operation), new Promise<never>((_resolve, reject) => {
2096
+ timer = setTimeout(() => reject(new Error(`warm ${phase} timeout after ${shadowTimeoutMs}ms`)), remainingMs);
2097
+ })]);
2098
+ } finally { if (timer) clearTimeout(timer); }
2099
+ };
1736
2100
  if (!files.length || files.length > GATE_RECHECK_POD_BATCH_FILES || new Set(files).size !== files.length
1737
2101
  || files.some((file) => !/^(?:[\w.-]+\/)*[\w.-]+\.test\.[cm]?[jt]sx?$/.test(file)
1738
2102
  || file.split('/').some((part) => part === '.' || part === '..')))
@@ -1741,12 +2105,19 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1741
2105
  const installSlots = pod.installSlots ?? POD_INSTALL_SLOTS_DEFAULT;
1742
2106
  if (!Number.isSafeInteger(installSlots) || installSlots < 0) throw new Error('invalid pod recheck install slots');
1743
2107
  const bunCache = pod.bunCache?.trim() || process.env[POD_BUN_CACHE_HOST_PATH]?.trim();
2108
+ const attemptId = randomUUID();
1744
2109
  const partLimitSeconds = Math.ceil(timeoutMs / 1000);
1745
2110
  const deadlineSeconds = partLimitSeconds * Math.ceil(files.length / 2) + POD_SHARD_EMIT_RESERVE_SECONDS + 60;
1746
2111
  const shell = gateShardShell({ parts: files.map((file) => [file]), cdpPatterns: [], softSeconds: softDeadlineSeconds(deadlineSeconds),
1747
2112
  cachePrefix: bunCache ? podBunCacheVolume(bunCache).shellPrefix : '', installSlots, parallelParts: files.length > 1 ? 2 : 1,
1748
- partLimitSeconds, ...(relaxed ? { testTimeoutMs: GATE_TIMEOUT_RECHECK_MS } : {}) });
2113
+ partLimitSeconds, sourceBinding: { cutSha: commit, attemptId }, ...(relaxed ? { testTimeoutMs: GATE_TIMEOUT_RECHECK_MS } : {}) });
1749
2114
  const job = await podCommand({ pool: pod.pool, ...(poolOverride ? { poolScheduler: poolOverride } : {}), clone: true,
2115
+ provenanceGate: true,
2116
+ provenance: { actorKind: process.env.ELANOUS_SEAT ? 'seat' : 'loop', actor: process.env.ELANOUS_SEAT || 'release-loop',
2117
+ character: 'gate-shard-runner', launchHost: hostname(), entrance: 'release-recheck', kind: 'gate-recheck',
2118
+ run: process.env.ELANOUS_RUN_ID || attemptId, phase: 'release-gate', requestedAt: new Date().toISOString(),
2119
+ graphNode: 'gate-triage', commit, reason: `release gate recheck ${files.length} files`, pool: pod.pool,
2120
+ memReq: POD_ISOLATION_MEMORY_LIMIT, warmReused: false },
1750
2121
  source: { kind: 'commit', sha: commit }, deadlineSeconds, ...(bunCache ? { bunCache } : {}), ...gateHostMirror(pod),
1751
2122
  ...(installSlots > 0 ? { installSlots: POD_INSTALL_SLOTS_HOST_PATH } : {}), ...(pod.cpu ? { cpu: pod.cpu } : {}),
1752
2123
  memoryLimit: POD_ISOLATION_MEMORY_LIMIT, name: `gate-recheck-${randomUUID()}`, command: ['bash', '-lc', shell] });
@@ -1755,8 +2126,13 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1755
2126
  return existsSync(path) ? readFileSync(path, 'utf8') : undefined;
1756
2127
  };
1757
2128
  const installed = artifact('install.rc')?.trim() === '0';
2129
+ const warmEligible = files.length === 1 && pod.warmEligible === true && !!pod.warmReplay && !!pod.warmQuarantine && !!pod.warmFinalize
2130
+ && pod.warmCutSha === commit;
2131
+ const incomplete = (message: string, file: string): Error => warmEligible
2132
+ ? new WarmRecheckIncomplete(message, file, { files: [file], attemptId, status: 'unmeasured', reason: 'reference-unmeasured', quarantined: 'no' })
2133
+ : new Error(message);
1758
2134
  if (!installed || job.exitCode !== 0)
1759
- throw new Error(`pod isolated recheck setup incomplete: ${commit.slice(0, 9)} job=${job.job} exit=${job.exitCode} install=${installed}`);
2135
+ throw incomplete(`pod isolated recheck setup incomplete: ${commit.slice(0, 9)} job=${job.job} exit=${job.exitCode} install=${installed}`, files[0]!);
1760
2136
  const timedOut = artifact('shard.timeout') !== undefined;
1761
2137
  const results = new Map<string, CommandResult>();
1762
2138
  for (const [index, file] of files.entries()) {
@@ -1765,11 +2141,105 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
1765
2141
  const rc = rcText && /^\d+$/.test(rcText) ? Number(rcText) : undefined;
1766
2142
  const partTimedOut = artifact(`part-${index}.timeout`) !== undefined;
1767
2143
  if (rc === undefined || output === undefined || partTimedOut) {
1768
- if (!timedOut && !partTimedOut) throw new Error(`pod isolated recheck incomplete: ${file} @${commit.slice(0, 9)} job=${job.job} rc=${rc ?? 'missing'}`);
2144
+ if (!timedOut && !partTimedOut) throw incomplete(`pod isolated recheck incomplete: ${file} @${commit.slice(0, 9)} job=${job.job} rc=${rc ?? 'missing'}`, file);
1769
2145
  results.set(file, { rc: 124, output: output ?? '', timedOut: true });
1770
- } else results.set(file, { rc, output });
2146
+ } else {
2147
+ // The comparator only accepts a complete JUnit/log pair tied to this exact Job attempt.
2148
+ // A missing or damaged artifact leaves the existing Job verdict authoritative.
2149
+ let warmReference: WarmWorkerFileResult | null = null;
2150
+ if (warmEligible) {
2151
+ try {
2152
+ const prefix = join(job.artifactsDir, `recheck-${index}`);
2153
+ const part = (suffix: string) => join(job.artifactsDir, `part-${index}.${suffix}`);
2154
+ for (const suffix of ['log', 'rc', 'junit.xml']) if (existsSync(part(suffix))) copyFileSync(part(suffix), `${prefix}.${suffix}`);
2155
+ writeFileSync(`${prefix}.json`, JSON.stringify({ commit, attemptId, files: [file], rc,
2156
+ jobFailed: false, jobExitCode: job.exitCode }) + '\n', { mode: 0o600 });
2157
+ if (writeWarmJobManifest(prefix, file, commit, attemptId))
2158
+ warmReference = warmReferenceFromJobArtifacts(prefix, file, { cutSha: commit, attemptId });
2159
+ } catch (error) {
2160
+ debug.log('release-loop.gate', 'warm-reference-unmeasured', { file, attemptId, error: String(error) });
2161
+ }
2162
+ }
2163
+ results.set(file, { rc, output, warmReference, warmAttemptId: attemptId });
2164
+ }
1771
2165
  debug.log('release-loop.gate', 'pod-isolated-recheck', { file, commit, job: job.job, rc: rc ?? 124, relaxed });
1772
2166
  }
2167
+ if (warmEligible && pod.warmReplay && pod.warmQuarantine && pod.warmFinalize) {
2168
+ shadowDeadline = Date.now() + shadowTimeoutMs;
2169
+ const first = results.get(files[0]!)!;
2170
+ const reference = first.warmReference;
2171
+ if (!reference) {
2172
+ first.warmShadow = { files: [...files], attemptId, status: 'unmeasured', reason: 'reference-unmeasured', quarantined: 'no' };
2173
+ debug.log('release-loop.gate', 'warm-shadow-recheck', first.warmShadow);
2174
+ } else if (pod.warmReplayOwner !== false && (warmShadowScope.attempts ?? (warmShadowScope.requested ? 1 : 0)) < WARM_REPLAY_BUDGET) {
2175
+ warmShadowScope.attempts = (warmShadowScope.attempts ?? (warmShadowScope.requested ? 1 : 0)) + 1;
2176
+ warmShadowScope.requested = true;
2177
+ let observation: WarmShadowObservation;
2178
+ try {
2179
+ const request = { protocol: 1 as const, runId: randomUUID(), attemptId, cutSha: commit,
2180
+ ...(pod.warmBaselineSha ? { baselineSha: pod.warmBaselineSha } : {}),
2181
+ deadlineAt: Math.min(Date.now() + timeoutMs * files.length + 60_000, shadowDeadline - 1_000),
2182
+ files, envProfile: 'gate-v1' };
2183
+ // The producer honors deadlineAt. An injected/slow producer may still settle after our wait cap:
2184
+ // fence any late lease before it can be mistaken for a reusable worker.
2185
+ let replayTimedOut = false;
2186
+ const pendingReplay = Promise.resolve().then(() => pod.warmReplay!(request));
2187
+ void pendingReplay.then((late) => {
2188
+ if (replayTimedOut && late.command) void Promise.resolve().then(() => pod.warmQuarantine!(late.command!, 'late-replay-after-timeout'))
2189
+ .catch((error) => debug.log('release-loop.gate', 'warm-shadow-late-quarantine-failed', { attemptId, error: String(error) }));
2190
+ }, (error) => {
2191
+ if (replayTimedOut) debug.log('release-loop.gate', 'warm-shadow-late-replay-failed', { attemptId, error: String(error) });
2192
+ });
2193
+ let replay: WarmReplayResult;
2194
+ try { replay = await boundedShadow('replay', () => pendingReplay); }
2195
+ catch (error) { replayTimedOut = true; throw error; }
2196
+ if (!replay.command) {
2197
+ observation = { files: [...files], attemptId, status: 'fallback', reason: replay.transport.reason, quarantined: 'no' };
2198
+ } else if (replay.transport.status !== 'observed') {
2199
+ let quarantined: WarmShadowObservation['quarantined'] = 'yes';
2200
+ try { await boundedShadow('quarantine', () => pod.warmQuarantine!(replay.command!, `transport-${replay.transport.reason}`)); }
2201
+ catch (error) {
2202
+ quarantined = 'failed';
2203
+ debug.log('release-loop.gate', 'warm-shadow-quarantine-failed', { attemptId,
2204
+ reason: replay.transport.reason, error: String(error) });
2205
+ }
2206
+ observation = { files: [...files], attemptId, status: 'fallback', reason: replay.transport.reason, quarantined };
2207
+ } else {
2208
+ const bound = warmCommandMatchesRequest(replay.command, request);
2209
+ const decision = bound ? assessWarmWorkerShadow(replay.command, [reference], replay.transport.reply)
2210
+ : { reason: 'identity' as const, quarantineWorker: true };
2211
+ let reason: string = decision.reason;
2212
+ let status: WarmShadowObservation['status'] = 'observed';
2213
+ let quarantined: WarmShadowObservation['quarantined'] = 'no';
2214
+ let finalized = false;
2215
+ if (decision.reason === 'match') {
2216
+ try { await boundedShadow('finalize', () => pod.warmFinalize!(replay.command!)); finalized = true; }
2217
+ catch (error) {
2218
+ reason = 'finalize-failed';
2219
+ status = 'fallback';
2220
+ debug.log('release-loop.gate', 'warm-shadow-finalize-failed', { attemptId, error: String(error) });
2221
+ }
2222
+ }
2223
+ if (!finalized) {
2224
+ try { await boundedShadow('quarantine', () => pod.warmQuarantine!(replay.command!, reason)); quarantined = 'yes'; }
2225
+ catch (error) {
2226
+ quarantined = 'failed';
2227
+ debug.log('release-loop.gate', 'warm-shadow-quarantine-failed', { attemptId, reason, error: String(error) });
2228
+ }
2229
+ }
2230
+ observation = { files: [...files], attemptId, status, reason, quarantined, finalized,
2231
+ cutSha: replay.command.cutSha, baselineSha: replay.command.baselineSha, workerKey: replay.command.key };
2232
+ }
2233
+ } catch (error) {
2234
+ observation = { files: [...files], attemptId, status: 'unmeasured', reason: String(error), quarantined: 'unavailable' };
2235
+ }
2236
+ first.warmShadow = observation;
2237
+ debug.log('release-loop.gate', 'warm-shadow-recheck', observation);
2238
+ } else {
2239
+ first.warmShadow = { files: [...files], attemptId, status: 'fallback', reason: 'replay-budget-exhausted', quarantined: 'no' };
2240
+ debug.log('release-loop.gate', 'warm-shadow-recheck', first.warmShadow);
2241
+ }
2242
+ }
1773
2243
  return results;
1774
2244
  };
1775
2245
  return {
@@ -1941,8 +2411,11 @@ export function saveBaselineFileCache(ledger: string, commit: string, rows: Base
1941
2411
  } finally { if (existsSync(temp)) rmSync(temp); }
1942
2412
  }
1943
2413
 
1944
- export async function judgeGate(opts: GateOptions, runner: GateRunner = createGateRunner(opts.repo ?? process.cwd(), opts.remote)): Promise<GateResult> {
2414
+ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGateRunner(opts.repo ?? process.cwd(), opts.remote),
2415
+ imageProbe: typeof preflightGateImageDigest = preflightGateImageDigest): Promise<GateResult> {
1945
2416
  const start = Date.now();
2417
+ if (opts.pod?.warmReplay) opts = { ...opts, pod: { ...opts.pod,
2418
+ warmShadowScope: { requested: false, attempts: 0 } } };
1946
2419
  const explicitConfig = getElanousConfigDirOverride();
1947
2420
  const root = explicitConfig ? effectiveInstanceRoot() : (opts.instanceRoot ?? effectiveInstanceRoot());
1948
2421
  // The freeze is read from the same root the gate runs in (a config-dir override must not move it elsewhere).
@@ -1969,17 +2442,28 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
1969
2442
  };
1970
2443
  const repo = resolve(opts.repo ?? process.cwd());
1971
2444
  const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], rcNonzeroNoFailures: [], durationMs: 0 };
2445
+ let baselineCommitForVerdict = opts.baselineCommit ?? '';
1972
2446
  const preexistingIds: string[] = [];
1973
2447
  let knownEnvIds: string[] = [];
1974
2448
  let work: string | undefined;
1975
2449
  let baseSnapshot = false;
1976
2450
  let cutFailures: SweepFailures | undefined;
2451
+ let cutSweepUsable = false;
2452
+ let cutSweepRc: number | null = null;
2453
+ let radiusShadow: GateRadiusShadowPlan | undefined;
1977
2454
  const trees: string[] = [];
1978
2455
  try {
1979
2456
  if (!sha.test(opts.commit) || !versionPattern.test(opts.version)
1980
2457
  || (opts.baselineVersion && !versionPattern.test(opts.baselineVersion))
1981
2458
  || (opts.baselineCommit && !sha.test(opts.baselineCommit))) throw new Error('invalid commit or version');
1982
2459
  if (!opts.baselineVersion) throw new Error('previous release version required (--baseline-version)');
2460
+ if (opts.rehearsalBaseline && (opts.graphRunId || !opts.pod || !/-rc\.\d+$/.test(opts.version)))
2461
+ throw new Error('rehearsal baseline requires a direct Pod gate with an isolated rc version');
2462
+ // Observation only. Failure to plan cannot change the gate selector or verdict.
2463
+ try { radiusShadow = planGateRadiusShadow({ repo, ledgerRoot: ledger, version: opts.version,
2464
+ baselineVersion: opts.baselineVersion, forCommit: opts.commit, instanceRoot: root,
2465
+ expectedBaselineCommit: opts.baselineCommit }); }
2466
+ catch (error) { debug.log('release-loop.gate', 'radius-shadow-unavailable', { reason: String(error) }); }
1983
2467
  if (!Number.isSafeInteger(opts.recheckFileLimit ?? GATE_HOST_RECHECK_FILE_LIMIT) || (opts.recheckFileLimit ?? GATE_HOST_RECHECK_FILE_LIMIT) < 1)
1984
2468
  throw new Error('invalid gate recheck file limit');
1985
2469
  if (opts.pod && (!opts.pod.pool || !Number.isSafeInteger(opts.pod.shards ?? 24) || (opts.pod.shards ?? 24) < 1
@@ -2023,12 +2507,31 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2023
2507
  for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
2024
2508
  // GATE-PARTIAL: a plan made by `release resume --from gate --partial` for exactly this commit re-runs only its files.
2025
2509
  // Its logs go to cut-partial/ — the full cut/ logs stay whole (the next release reads them as a baseline source).
2026
- const partial = readPartialPlan(roots, opts.version, opts.commit);
2027
- if (partial) debug.log('release-loop.gate', 'partial-regate', { version: opts.version, priorCommit: partial.priorCommit, commit: opts.commit, files: partial.files.length });
2510
+ const partialRead = readPartialPlanResult(roots, opts.version, opts.commit);
2511
+ const explicitPartial = partialRead.plan;
2512
+ const rehearsalStampPresent = ['hard-5h', 'hard-mid'].some((step) =>
2513
+ existsSync(join(root, 'release', opts.version, 'rehearsal', `${step}.json`)));
2514
+ const currentImage = !explicitPartial && opts.graphRunId && opts.pod && rehearsalStampPresent
2515
+ ? imageProbe(opts.pod.pool) : null;
2516
+ const radius = !explicitPartial && opts.graphRunId && opts.pod && rehearsalStampPresent
2517
+ ? planHardRehearsalRadius({ root, repo, version: opts.version, forCommit: opts.commit,
2518
+ graphPins: gateGraphPins(root, opts.graphRunId), envFingerprint: gateEnvFingerprint(process.env), imageDigest: currentImage })
2519
+ : null;
2520
+ if (radius) debug.log('release-loop.gate', 'radius-live-plan', { version: opts.version, reason: radius.reason,
2521
+ files: radius.plan?.files.length ?? null, rehearsalRunId: radius.rehearsalRunId ?? null });
2522
+ let partial = explicitPartial ?? radius?.plan ?? null;
2523
+ debug.log('release-loop.gate', partial ? 'partial-regate' : 'partial-fallback', {
2524
+ version: opts.version, priorCommit: partial?.priorCommit ?? null, commit: opts.commit,
2525
+ planKind: partial?.kind ?? 'full', remeasureFileCount: partial?.files.length ?? 0,
2526
+ preservedShardCount: partial?.kind === 'recheck' && 'preservedShardCount' in partial
2527
+ && typeof partial.preservedShardCount === 'number' ? partial.preservedShardCount : 0,
2528
+ fallbackReason: partial ? undefined : partialRead.fallbackReason,
2529
+ });
2028
2530
  if (partial?.kind === 'carry') {
2531
+ const carryPriorCommit = partial.priorCommit;
2029
2532
  // The plan alone is not evidence: the old cut and its verdict must still be complete and clean at this SHA.
2030
- if (!verifyDocsOnlyCarry(repo, partial.priorCommit, opts.commit))
2031
- throw new Error(`carry plan diff is not docs-only: ${partial.priorCommit}..${opts.commit}`);
2533
+ if (!verifyDocsOnlyCarry(repo, carryPriorCommit, opts.commit))
2534
+ throw new Error(`carry plan diff is not docs-only: ${carryPriorCommit}..${opts.commit}`);
2032
2535
  const trusted = roots.some((location) => {
2033
2536
  const dir = join(location, 'release', opts.version);
2034
2537
  const failuresPath = join(dir, 'gate-failures.json');
@@ -2036,11 +2539,11 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2036
2539
  if (!existsSync(failuresPath) || !existsSync(verdictPath)) return false;
2037
2540
  const failures = JSON.parse(readFileSync(failuresPath, 'utf8')) as { commit?: unknown; failures?: unknown; errors?: unknown };
2038
2541
  const verdict = JSON.parse(readFileSync(verdictPath, 'utf8')) as { commit?: unknown; outcome?: unknown };
2039
- return failures.commit === partial.priorCommit && Array.isArray(failures.failures) && failures.failures.length === 0
2542
+ return failures.commit === carryPriorCommit && Array.isArray(failures.failures) && failures.failures.length === 0
2040
2543
  && (failures.errors === undefined || (Array.isArray(failures.errors) && failures.errors.length === 0))
2041
- && verdict.commit === partial.priorCommit && verdict.outcome === 'ok';
2544
+ && verdict.commit === carryPriorCommit && verdict.outcome === 'ok';
2042
2545
  });
2043
- if (!trusted) throw new Error(`carry plan has no complete clean gate at ${partial.priorCommit}`);
2546
+ if (!trusted) throw new Error(`carry plan has no complete clean gate at ${carryPriorCommit}`);
2044
2547
  }
2045
2548
  // GATE-SKIP-KNOWN-RETRY — the ids the verdict will not count as introduced (the recorded baseline ⊕ env-known), read
2046
2549
  // before the sweep so a shard failing only with them skips its isolation retries. No recorded baseline = no skip.
@@ -2055,12 +2558,26 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2055
2558
  } catch (error) { debug.log('release-loop.gate', 'known-failures-unavailable', { error: String(error) }); }
2056
2559
  }
2057
2560
  const durationSource = join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut');
2058
- const cutRun = partial?.kind === 'carry' ? undefined : await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', partial ? 'cut-partial' : 'cut'),
2561
+ const runCut = (selected: typeof partial) => selected?.kind === 'carry' ? undefined : runner.sweep(cutTree,
2562
+ join(root, 'release', opts.version, 'gate-logs', selected ? 'cut-partial' : 'cut'),
2059
2563
  opts.pod ? { ...opts.pod, durationSource, ...(knownFailures.length ? { knownFailures } : {}), onSelectionTimed: ({ ms }) => { result.selectionMs = ms; },
2060
2564
  // GATE-LIVE-OBS: the release ledger's shards.json (UX #24782) — never the run tree's universe; an explicit
2061
2565
  // --ledger-root (tests) keeps it in that ledger.
2062
2566
  shardsFile: { path: opts.ledgerRoot ? gateShardsPath(opts.version, opts.ledgerRoot) : gateShardsPath(opts.version), version: opts.version } } : undefined,
2063
- partial?.files);
2567
+ selected?.files);
2568
+ let cutRun = await runCut(partial);
2569
+ const radiusSelectionMatched = partial?.files && cutRun?.podSweep?.files
2570
+ && JSON.stringify([...partial.files].sort()) === JSON.stringify([...cutRun.podSweep.files].sort());
2571
+ if (radius?.plan && (!radiusSelectionMatched
2572
+ || !partialGateImagesMatch(root, opts.version, currentImage!, opts.commit, cutRun?.podSweep))) {
2573
+ debug.log('release-loop.gate', 'radius-live-image-drift', { version: opts.version,
2574
+ rehearsalRunId: radius.rehearsalRunId, action: 'full-sweep' }, { level: 'warn' });
2575
+ partial = null;
2576
+ cutRun = await runCut(null);
2577
+ }
2578
+ cutSweepRc = cutRun?.rc ?? null;
2579
+ cutSweepUsable = !!cutRun && (cutRun.rc === 0 || cutRun.rc === 1) && !cutRun.timedOut
2580
+ && !cutRun.abortedShards && !cutRun.stalledEnv?.length;
2064
2581
  if (cutRun?.abortedShards) result.abortedShards = cutRun.abortedShards;
2065
2582
  const localStalled = async (run: CommandResult, tree: string, commit: string, label: string) => {
2066
2583
  const stalled = run.stalledEnv ?? [];
@@ -2145,6 +2662,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2145
2662
  if (partial) result.partial = { priorCommit: partial.priorCommit, files: partial.files.length };
2146
2663
  cutFailures = cut;
2147
2664
  const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
2665
+ baselineCommitForVerdict = baseSha;
2148
2666
  const cached = cachedBaseline(opts.baselineVersion, baseSha);
2149
2667
  const recorded = cached ? undefined : baselineFromCutLogs(join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut'), baseSha);
2150
2668
  const trusted = cached?.saved ?? (recorded?.complete ? recorded : undefined);
@@ -2221,6 +2739,29 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2221
2739
  catch (error) { debug.log('release.gate', 'baseline-cache-write-failed', { commit: row.commit, file: row.file, error: String(error) }); }
2222
2740
  };
2223
2741
  const recheckFiles = [...newFailureFiles].sort();
2742
+ const eligible = recheckFiles.filter((file) => !cutHostFailed.has(file) && !prefetchedCut.has(file));
2743
+ const warmEligibleFiles = new Set(opts.pod?.warmReplay && opts.pod.warmQuarantine && opts.pod.warmFinalize
2744
+ ? eligible.slice(0, WARM_REPLAY_BUDGET) : []);
2745
+ // The source-bound lock comes from the cut commit itself, never from the producer's claim.
2746
+ const history = readWarmVerdictHistory(verdictFile(ledger, opts.version));
2747
+ let warmLockHash = '';
2748
+ if (opts.pod?.warmReuseConfig?.gateRecheck && history?.length) {
2749
+ try {
2750
+ const lock = await runner.localCommand('git', ['show', `${opts.commit}:bun.lock`], repo);
2751
+ if (lock.rc === 0 && lock.output) warmLockHash = createHash('sha256').update(lock.output).digest('hex');
2752
+ } catch { /* Unmeasured evidence keeps reuse off. */ }
2753
+ }
2754
+ const warmEvidence = warmEvidenceFromVerdicts(history, opts.version, opts.commit, baseSha, warmLockHash);
2755
+ const warmDecision = decideWarmReuse('gate-recheck', opts.pod?.warmReuseConfig,
2756
+ warmEvidence ? { 'gate-recheck': warmEvidence } : undefined);
2757
+ const verifiedWorkerKey = history?.at(-1)?.warmShadow.at(-1)?.workerKey;
2758
+ if (warmDecision.live && verifiedWorkerKey && opts.pod && runner.recheckPod) {
2759
+ // Only the first two files may adopt; replay them serially before scheduling fresh fallbacks.
2760
+ for (const file of eligible.slice(0, 2)) {
2761
+ const adopted = await tryWarmGateAdoption(file, opts.commit, baseSha, verifiedWorkerKey, opts.pod);
2762
+ if (adopted) prefetchedCut.set(file, adopted);
2763
+ }
2764
+ }
2224
2765
  const recheckLimit = opts.recheckFileLimit ?? GATE_HOST_RECHECK_FILE_LIMIT;
2225
2766
  const recheckConcurrency = opts.pod?.isolationConcurrency ?? 4;
2226
2767
  if (opts.pod && (!Number.isSafeInteger(recheckConcurrency) || recheckConcurrency < 1)) throw new Error('invalid pod recheck concurrency');
@@ -2230,13 +2771,19 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2230
2771
  concurrency: opts.pod && runner.recheckPod ? recheckConcurrency : 1, executor: opts.pod && runner.recheckPod ? 'pod' : 'host',
2231
2772
  remote: opts.remote ?? null, podBatchFiles: opts.pod && runner.recheckPod && runner.recheckPodBatch ? GATE_RECHECK_POD_BATCH_FILES : 1 });
2232
2773
  const recheckStartedAt = Date.now();
2774
+ const recordWarmShadow = (shadow?: WarmShadowObservation) => {
2775
+ if (shadow && !result.warmShadow?.some((entry) => entry.attemptId === shadow.attemptId))
2776
+ (result.warmShadow ??= []).push(shadow);
2777
+ };
2233
2778
  const recheckFile = async (file: string, index: number) => {
2234
2779
  debug.log('release-loop.gate', 'isolated-recheck-progress',
2235
2780
  { index: index + 1, total: recheckFiles.length, file, elapsedSec: Math.round((Date.now() - recheckStartedAt) / 1000) });
2236
2781
  // 0.2.15: the cut re-check of a new-failure file (InsidePage.test.tsx · headless Chrome) hung for good — limit it like the host re-run.
2237
2782
  const cutCheck = cutHostFailed.has(file) ? undefined : prefetchedCut.get(file) ?? (opts.pod && runner.recheckPod
2238
- ? await runner.recheckPod(file, opts.commit, opts.pod, GATE_ISOLATED_TIMEOUT_MS)
2783
+ ? await runner.recheckPod(file, opts.commit, { ...opts.pod, warmCutSha: opts.commit, warmBaselineSha: baseSha,
2784
+ warmEligible: warmEligibleFiles.has(file) }, GATE_ISOLATED_TIMEOUT_MS)
2239
2785
  : await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree, GATE_ISOLATED_TIMEOUT_MS));
2786
+ recordWarmShadow(cutCheck?.warmShadow);
2240
2787
  if (cutCheck?.timedOut) {
2241
2788
  debug.log('release-loop.gate', 'isolated-timeout', { file, label: 'cut isolated', limitMs: GATE_ISOLATED_TIMEOUT_MS });
2242
2789
  result.stalledEnv.push({ file, reason: 'isolated-timeout', local: 'timeout' });
@@ -2258,8 +2805,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2258
2805
  const shaped = candidates.filter((id) => timedOut.has(id) && isolatedCut.failures.includes(id));
2259
2806
  if (shaped.length) {
2260
2807
  const relaxed = opts.pod && runner.recheckPod
2261
- ? await runner.recheckPod(file, opts.commit, opts.pod, GATE_ISOLATED_TIMEOUT_MS, true)
2808
+ ? await runner.recheckPod(file, opts.commit, { ...opts.pod, warmCutSha: opts.commit, warmBaselineSha: baseSha,
2809
+ warmEligible: warmEligibleFiles.has(file) }, GATE_ISOLATED_TIMEOUT_MS, true)
2262
2810
  : await runner.command('bun', ['run', 'test:deterministic', asPath(file), '--timeout', String(GATE_TIMEOUT_RECHECK_MS)], cutTree, GATE_ISOLATED_TIMEOUT_MS);
2811
+ recordWarmShadow(relaxed.warmShadow);
2263
2812
  let flaky: string[] = [];
2264
2813
  let recheck: 'read' | 'timeout' | 'unreadable' = 'read';
2265
2814
  if (relaxed.timedOut) recheck = 'timeout';
@@ -2329,20 +2878,51 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2329
2878
  const cap = opts.pod && runner.recheckPod ? recheckConcurrency : 1;
2330
2879
  const settleStarted = async <T>(jobs: Promise<T>[]): Promise<T[]> => {
2331
2880
  const settled = await Promise.allSettled(jobs);
2332
- const rejected = settled.find((item): item is PromiseRejectedResult => item.status === 'rejected');
2333
- if (rejected) throw rejected.reason;
2881
+ const rejected = settled.filter((item): item is PromiseRejectedResult => item.status === 'rejected');
2882
+ if (rejected.length) throw new SettledRecheckFailures(rejected.map((item) => item.reason));
2334
2883
  return settled.map((item) => (item as PromiseFulfilledResult<T>).value);
2335
2884
  };
2885
+ // Always attempt both eligible references before a failed Job can end the verdict. A cap of one
2886
+ // still runs the second Job, and successes retain their shadow if a peer throws.
2887
+ if (warmEligibleFiles.size && opts.pod && runner.recheckPod) {
2888
+ const completed = new Map<string, CommandResult>();
2889
+ const failures: unknown[] = [];
2890
+ const warmFiles = [...warmEligibleFiles].filter((file) => !prefetchedCut.has(file));
2891
+ for (let start = 0; start < warmFiles.length; start += cap) {
2892
+ const settled = await Promise.allSettled(warmFiles.slice(start, start + cap).map(async (file) => {
2893
+ const pod = { ...opts.pod!, warmCutSha: opts.commit, warmBaselineSha: baseSha, warmEligible: true,
2894
+ warmReplayOwner: true };
2895
+ return runner.recheckPodBatch
2896
+ ? (await runner.recheckPodBatch([file], opts.commit, pod, GATE_ISOLATED_TIMEOUT_MS)).get(file)
2897
+ : await runner.recheckPod!(file, opts.commit, pod, GATE_ISOLATED_TIMEOUT_MS);
2898
+ }));
2899
+ for (const [index, item] of settled.entries()) {
2900
+ if (item.status === 'rejected') failures.push(item.reason);
2901
+ else if (item.value) completed.set(warmFiles[start + index]!, item.value);
2902
+ else failures.push(new Error(`pod isolated recheck missing result: ${warmFiles[start + index]}`));
2903
+ }
2904
+ }
2905
+ for (const [file, run] of completed) {
2906
+ recordWarmShadow(run.warmShadow);
2907
+ prefetchedCut.set(file, run);
2908
+ }
2909
+ if (failures.length) throw new SettledRecheckFailures(failures);
2910
+ }
2336
2911
  for (let start = 0; start < recheckFiles.length; start += recheckLimit) {
2337
2912
  const window = recheckFiles.slice(start, start + recheckLimit);
2338
2913
  debug.log('release-loop.gate', 'isolated-recheck-window', { start, files: window.length, total: recheckFiles.length });
2339
2914
  if (opts.pod && runner.recheckPod && runner.recheckPodBatch) {
2340
2915
  const pending = window.filter((file) => !cutHostFailed.has(file) && !prefetchedCut.has(file));
2341
- const groups = Array.from({ length: Math.ceil(pending.length / GATE_RECHECK_POD_BATCH_FILES) }, (_, index) =>
2342
- pending.slice(index * GATE_RECHECK_POD_BATCH_FILES, (index + 1) * GATE_RECHECK_POD_BATCH_FILES));
2916
+ const warmSingles = pending.filter((file) => warmEligibleFiles.has(file)).length;
2917
+ const groups = [
2918
+ ...pending.slice(0, warmSingles).map((file) => [file]),
2919
+ ...Array.from({ length: Math.ceil((pending.length - warmSingles) / GATE_RECHECK_POD_BATCH_FILES) }, (_, index) =>
2920
+ pending.slice(warmSingles + index * GATE_RECHECK_POD_BATCH_FILES, warmSingles + (index + 1) * GATE_RECHECK_POD_BATCH_FILES)),
2921
+ ];
2343
2922
  for (let next = 0; next < groups.length; next += cap) {
2344
2923
  const batches = await settleStarted(groups.slice(next, next + cap).map((files) =>
2345
- runner.recheckPodBatch!(files, opts.commit, opts.pod!, GATE_ISOLATED_TIMEOUT_MS)));
2924
+ runner.recheckPodBatch!(files, opts.commit, { ...opts.pod!, warmCutSha: opts.commit, warmBaselineSha: baseSha,
2925
+ warmEligible: files.length === 1 && warmEligibleFiles.has(files[0]!) }, GATE_ISOLATED_TIMEOUT_MS)));
2346
2926
  for (const batch of batches) for (const [file, run] of batch) prefetchedCut.set(file, run);
2347
2927
  }
2348
2928
  }
@@ -2375,9 +2955,18 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2375
2955
  }
2376
2956
  } catch (error) {
2377
2957
  result.error = error instanceof Error ? error.message : String(error);
2958
+ const warmFailures = (error instanceof SettledRecheckFailures ? error.failures : [error])
2959
+ .filter((failure): failure is WarmRecheckIncomplete => failure instanceof WarmRecheckIncomplete);
2960
+ if (warmFailures.length) {
2961
+ (result.warmShadow ??= []).push(...warmFailures.map((failure) => failure.shadow));
2962
+ result.unmeasuredRecheckFiles = [...new Set([...(result.unmeasuredRecheckFiles ?? []),
2963
+ ...warmFailures.map((failure) => failure.file)])].sort();
2964
+ }
2378
2965
  if (error instanceof StalledPodShards) {
2379
2966
  result.stalledShards = error.stalledShards;
2380
2967
  result.partialSummary = error.partialSummary;
2968
+ result.partialFailures = error.partialFailures;
2969
+ result.preservedShardCount = error.preservedShardCount;
2381
2970
  if (error.abortedShards) result.abortedShards = error.abortedShards;
2382
2971
  }
2383
2972
  } finally {
@@ -2416,6 +3005,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2416
3005
  }
2417
3006
  }
2418
3007
  {
3008
+ const baselineCommit = baselineCommitForVerdict;
2419
3009
  const ids = {
2420
3010
  introduced: [...result.introduced].sort(),
2421
3011
  knownEnv: [...knownEnvIds].sort(),
@@ -2425,7 +3015,15 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2425
3015
  ...(result.unmeasuredErrors?.length ? { unmeasuredErrors: result.unmeasuredErrors } : {}),
2426
3016
  ...(result.interferenceErrors?.length ? { interferenceErrors: [...result.interferenceErrors].sort() } : {}),
2427
3017
  };
2428
- const observed = result.selectionMs !== undefined ? { selectionMs: result.selectionMs } : {};
3018
+ const observed = { ...(result.selectionMs !== undefined ? { selectionMs: result.selectionMs } : {}),
3019
+ ...(result.warmShadow?.length ? { warmShadow: result.warmShadow } : {}) };
3020
+ // Receipt is advisory for a later radius gate. Missing identity makes reuse invalid, never this gate's verdict.
3021
+ const baselineReceipt = gateBaselineReceipt({ root, version: opts.version, commit: opts.commit,
3022
+ graphPins: gateGraphPins(root, opts.graphRunId) ?? (opts.rehearsalBaseline ? gateSourcePins(repo, opts.commit) : null),
3023
+ envFingerprint: gateEnvFingerprint(process.env),
3024
+ fullSweep: result.outcome === 'ok' && cutSweepUsable && !result.partial && !!cutFailures
3025
+ && !result.rcNonzeroNoFailures.length && !result.stalledShards?.length && !result.stalledEnv.length
3026
+ && !result.unmeasuredRecheckFiles?.length && !result.unmeasuredErrors?.length });
2429
3027
  try {
2430
3028
  for (const location of roots) {
2431
3029
  const path = verdictFile(location, opts.version);
@@ -2436,7 +3034,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2436
3034
  mkdirSync(dirname(path), { recursive: true });
2437
3035
  const temp = join(dirname(path), `.gate-verdict-${process.pid}-${randomUUID()}.tmp`);
2438
3036
  try {
2439
- writeFileSync(temp, JSON.stringify({ version: opts.version, commit: opts.commit, outcome: result.outcome, ...ids, ...observed }, null, 2) + '\n', { mode: 0o600 });
3037
+ const warmHistory = readWarmVerdictHistory(path) ?? [];
3038
+ writeFileSync(temp, JSON.stringify({ version: opts.version, commit: opts.commit,
3039
+ outcome: result.outcome, ...ids, ...observed, baselineReceipt,
3040
+ ...(opts.pod?.warmReplay ? { baselineCommit, warmHistory } : {}) }, null, 2) + '\n', { mode: 0o600 });
2440
3041
  chmodSync(temp, 0o600);
2441
3042
  renameSync(temp, path);
2442
3043
  } finally { if (existsSync(temp)) rmSync(temp); }
@@ -2449,7 +3050,10 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2449
3050
  mkdirSync(dirname(path), { recursive: true });
2450
3051
  const temp = join(dirname(path), `.gate-verdict-${process.pid}-${randomUUID()}.tmp`);
2451
3052
  try {
2452
- writeFileSync(temp, JSON.stringify({ version: opts.version, commit: opts.commit, outcome: result.outcome, ...ids, ...observed }, null, 2) + '\n', { mode: 0o600 });
3053
+ const warmHistory = readWarmVerdictHistory(path) ?? [];
3054
+ writeFileSync(temp, JSON.stringify({ version: opts.version, commit: opts.commit,
3055
+ outcome: result.outcome, ...ids, ...observed, baselineReceipt,
3056
+ ...(opts.pod?.warmReplay ? { baselineCommit, warmHistory } : {}) }, null, 2) + '\n', { mode: 0o600 });
2453
3057
  chmodSync(temp, 0o600);
2454
3058
  renameSync(temp, path);
2455
3059
  } finally { if (existsSync(temp)) rmSync(temp); }
@@ -2465,6 +3069,20 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
2465
3069
  ...(result.outcome === 'error' ? { error: result.error } : {}),
2466
3070
  });
2467
3071
  }
3072
+ if (radiusShadow) {
3073
+ try {
3074
+ const record = { plan: radiusShadow, recordedAt: new Date().toISOString(),
3075
+ actual: shadowActual({ plan: radiusShadow, outcome: result.outcome,
3076
+ introduced: result.introduced, cutFailures: cutFailures?.failures ?? [], cutErrors: cutFailures?.errors ?? [], cutRc: cutSweepRc,
3077
+ fullSweep: cutSweepUsable && !result.partial && !!cutFailures && result.outcome !== 'error'
3078
+ && !result.rcNonzeroNoFailures.length && !result.stalledShards?.length
3079
+ && !result.stalledEnv.length && !result.unmeasuredRecheckFiles?.length
3080
+ && !result.unmeasuredErrors?.length }) };
3081
+ const path = writeGateRadiusShadow(ledger, record);
3082
+ debug.log('release-loop.gate', 'radius-shadow-recorded', { path, proposed: radiusShadow.proposed,
3083
+ missedIntroduced: record.actual.missedIntroduced.length });
3084
+ } catch (error) { debug.log('release-loop.gate', 'radius-shadow-unavailable', { reason: String(error) }); }
3085
+ }
2468
3086
  // GATE-LIVE-OBS-RC-WORDING ⓒ — the shards board says «판정 전» until this lands; then «새 실패 N» only (UX · gate-shards.ts).
2469
3087
  // Same ledger as the board's own writes (sweep's shardsFile). A write failure is logged and never changes the verdict.
2470
3088
  if (result.outcome !== 'error' && opts.pod) {
@@ -2563,6 +3181,7 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
2563
3181
  const arg = args[i];
2564
3182
  if (arg === '--help') return 'help';
2565
3183
  if (arg === '--json') continue;
3184
+ if (arg === '--rehearsal-baseline') { opts.rehearsalBaseline = true; continue; }
2566
3185
  // 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).
2567
3186
  if (arg === '--fresh-shards') { freshShards = true; continue; }
2568
3187
  if (arg === '--rehearsal-tui') { opts.rehearsalTui = true; continue; }
@@ -2606,19 +3225,26 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
2606
3225
 
2607
3226
  /** The node's run: judge, emit the graph result line, and return the process exit code (ok 0 · regression 1 · else 2).
2608
3227
  * `runner` is a seam for tests; the process entry passes none. */
2609
- export async function runGateNode(argv: string[], env: NodeJS.ProcessEnv, runner?: GateRunner): Promise<number> {
3228
+ export async function runGateNode(argv: string[], env: NodeJS.ProcessEnv, runner?: GateRunner,
3229
+ judge: (opts: GateOptions, runner?: GateRunner) => Promise<GateResult> = judgeGate): Promise<number> {
2610
3230
  const start = Date.now();
2611
3231
  let result: GateResult;
2612
3232
  let version = '';
2613
3233
  let placement: { executor: string | null; host: string | null } | undefined;
2614
3234
  try {
2615
- const opts = parseOptions(argv, env);
3235
+ const parsed = parseOptions(argv, env);
3236
+ let config: unknown = null;
3237
+ if (parsed !== 'help' && !runner && parsed.pod) {
3238
+ try { config = getUserConfig(); }
3239
+ catch (error) { debug.log('release-loop.gate', 'warm-profile-unavailable', { error: String(error) }); }
3240
+ }
3241
+ const opts = parsed === 'help' ? parsed : withWarmGateHooks(parsed, config, !!runner);
2616
3242
  if (opts !== 'help') {
2617
3243
  placement = gateExecutorOf(opts);
2618
3244
  debug.log('release-loop.gate', 'executor', { version: opts.version, ...placement, requested: opts.executor ?? null });
2619
3245
  }
2620
3246
  if (opts === 'help') {
2621
- 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>] [--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).');
3247
+ 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.');
2622
3248
  return 0;
2623
3249
  }
2624
3250
  version = opts.version;
@@ -2631,7 +3257,7 @@ export async function runGateNode(argv: string[], env: NodeJS.ProcessEnv, runner
2631
3257
  throw new Error('release gate host reservation requires graph runId and pinned state root');
2632
3258
  const releaseHosts = context && hosts.length
2633
3259
  ? reserveReleaseGateHosts(reservationRoot!, context.runId!, hosts) : () => undefined;
2634
- try { result = await (runner ? judgeGate(opts, runner) : judgeGate(opts)); }
3260
+ try { result = await judge(context?.runId ? { ...opts, graphRunId: context.runId } : opts, runner); }
2635
3261
  finally { releaseHosts(); }
2636
3262
  } catch (error) {
2637
3263
  result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], rcNonzeroNoFailures: [], durationMs: Date.now() - start, error: String(error) };
@@ -2647,5 +3273,5 @@ if (import.meta.main) {
2647
3273
  // The graph spawns this file as its own process, which inherits no daemon sink — without this every
2648
3274
  // `release-loop.gate`/`release.gate`/`pod.pool` event stayed in the file trail and `elanous logs` read 0 rows.
2649
3275
  await registerStandaloneLogSink('release-loop');
2650
- process.exitCode = await runGateNode(process.argv.slice(2), process.env);
3276
+ process.exitCode = await runWithReleaseGateTraffic(process.env, () => runGateNode(process.argv.slice(2), process.env));
2651
3277
  }