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