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