@ebrindley/build 0.0.0-stage → 1.42.1-rc28
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/.build-project/providers/catalog.json +13951 -0
- package/.build-project/providers/pricing.json +1978 -0
- package/.build-project/providers/registry.json +4103 -0
- package/.github/SECURITY.md +75 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +661 -0
- package/CODE_OF_CONDUCT.md +44 -0
- package/CONTRIBUTING.md +24 -0
- package/INSTALL.md +623 -0
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/THIRD_PARTY_NOTICES.txt +26 -0
- package/dist/ai-judge-UE5UOYOO.js +191 -0
- package/dist/allowlist-grounding-KPLN6NC5.js +16 -0
- package/dist/allowlist-utils-MZZKKXRD.js +31 -0
- package/dist/apply-completion-mode-EM37UZHB.js +110 -0
- package/dist/artifact-migrator-WPACNSEP.js +276 -0
- package/dist/ask-2OZ4JFQR.js +176 -0
- package/dist/auth-6UKBTJUQ.js +185 -0
- package/dist/auth-H5KJL6HL.js +165 -0
- package/dist/auth-LVJRJDMU.js +12 -0
- package/dist/auth-NHZOCNRR.js +94 -0
- package/dist/auth-PBXHWXGW.js +95 -0
- package/dist/auth-T7Y5DCAH.js +91 -0
- package/dist/auth-TVVKCHQW.js +13 -0
- package/dist/auth-UHB2UCSS.js +97 -0
- package/dist/auth-VACA3VUL.js +98 -0
- package/dist/auth-VVLC77S3.js +44 -0
- package/dist/auth-liveness-XO4AGDDD.js +53 -0
- package/dist/autoloop-cleanup-SI3VGAAR.js +106 -0
- package/dist/autoloop-evidence-gates-J5GG5YQB.js +39 -0
- package/dist/autoloop-gate-policy-SUNAEP6Q.js +169 -0
- package/dist/autoloop-helpers-VQGQNTT6.js +52 -0
- package/dist/autoloop-ledger-P6TIDP7C.js +96 -0
- package/dist/autoloop-ledger-subscriber-HFAGOZXH.js +161 -0
- package/dist/autoloop-run-defaults-GWGZBNT7.js +27 -0
- package/dist/autoloop-run-lock-GBWRM5A2.js +197 -0
- package/dist/autoloop-success-C32QVWOP.js +93 -0
- package/dist/autoloop-termination-summary-GI6IBQWD.js +58 -0
- package/dist/autoloop-trajectory-AMKSQISD.js +70 -0
- package/dist/autoloop-wall-clock-T7ZVIQOC.js +12 -0
- package/dist/automatic-branch-cleanup-R62X2C5O.js +49 -0
- package/dist/autonomous-loop-controller-2WL363LW.js +4386 -0
- package/dist/backend-YUTFDZXZ.js +30 -0
- package/dist/background-executor-Z3ODTMD7.js +260 -0
- package/dist/backlog-75QPEAMW.js +403 -0
- package/dist/backlog-execution-intent-GWUV5TUK.js +19 -0
- package/dist/backup-active-store-XHWLN34M.js +16 -0
- package/dist/backup-machine-stores-UI4LTJOY.js +251 -0
- package/dist/backup-telemetry-db-Z4ICYCNW.js +26 -0
- package/dist/branch-archive-manager-FMSQ7YJF.js +18 -0
- package/dist/branch-cleanup-manager-ST6FJGND.js +35 -0
- package/dist/build-gate-7NKWPY62.js +68 -0
- package/dist/build-root-S7DINODY.js +18 -0
- package/dist/change-summary-R6CB6VXJ.js +50 -0
- package/dist/chunk-24MNKKOM.js +173 -0
- package/dist/chunk-2H6VZFIO.js +235 -0
- package/dist/chunk-2JIX3EKV.js +39 -0
- package/dist/chunk-2KO7HVZM.js +106 -0
- package/dist/chunk-2L36U25O.js +100 -0
- package/dist/chunk-2NMNFUQA.js +475 -0
- package/dist/chunk-2OUAEH67.js +83 -0
- package/dist/chunk-2VLJUXJU.js +115 -0
- package/dist/chunk-2VUEDIUM.js +77 -0
- package/dist/chunk-2Y2IIHPI.js +430 -0
- package/dist/chunk-2YRG3LHZ.js +220 -0
- package/dist/chunk-2Z5Q3SUY.js +65 -0
- package/dist/chunk-32IYXIE5.js +873 -0
- package/dist/chunk-33IGPPUI.js +292 -0
- package/dist/chunk-352PCJLK.js +283 -0
- package/dist/chunk-35J4NE6S.js +226 -0
- package/dist/chunk-3C7WBIVK.js +1016 -0
- package/dist/chunk-3H3EY6U7.js +1225 -0
- package/dist/chunk-3H4MWYIC.js +198 -0
- package/dist/chunk-3HHWQR3N.js +2479 -0
- package/dist/chunk-3JKNRYEE.js +14 -0
- package/dist/chunk-3LECCYZY.js +128 -0
- package/dist/chunk-3LOY6YZI.js +852 -0
- package/dist/chunk-3M57DADF.js +44 -0
- package/dist/chunk-3NOTLVLG.js +3803 -0
- package/dist/chunk-3OPP2OZB.js +1480 -0
- package/dist/chunk-3P5IN5E7.js +9 -0
- package/dist/chunk-3PL22IYZ.js +20 -0
- package/dist/chunk-3REDAJUJ.js +14 -0
- package/dist/chunk-3S2R4XJI.js +203 -0
- package/dist/chunk-3UBZI5DW.js +61 -0
- package/dist/chunk-3VV5OBGG.js +308 -0
- package/dist/chunk-3WY5TU2O.js +823 -0
- package/dist/chunk-43OKHTW4.js +82 -0
- package/dist/chunk-45KUJBDI.js +112 -0
- package/dist/chunk-47X5LM7B.js +224 -0
- package/dist/chunk-4B6FQJ77.js +268 -0
- package/dist/chunk-4BTDUT2S.js +42 -0
- package/dist/chunk-4DJL2VXA.js +24 -0
- package/dist/chunk-4EBSXZ73.js +48 -0
- package/dist/chunk-4QKP6I4C.js +294 -0
- package/dist/chunk-4U6XK2DG.js +300 -0
- package/dist/chunk-4UMRUFBD.js +132 -0
- package/dist/chunk-4US4SYJ4.js +569 -0
- package/dist/chunk-4ZPLWEZO.js +154 -0
- package/dist/chunk-56WPYDJH.js +624 -0
- package/dist/chunk-57WRCJJW.js +50 -0
- package/dist/chunk-5AW4RMNG.js +24 -0
- package/dist/chunk-5EQN56OZ.js +155 -0
- package/dist/chunk-5F43TUIN.js +38 -0
- package/dist/chunk-5FKHIKTS.js +115 -0
- package/dist/chunk-5HDP7XZG.js +91 -0
- package/dist/chunk-5KQETTNS.js +152 -0
- package/dist/chunk-5PDVN2KO.js +498 -0
- package/dist/chunk-5UBIFVRD.js +509 -0
- package/dist/chunk-5YDE3Q62.js +46 -0
- package/dist/chunk-626AJVMU.js +302 -0
- package/dist/chunk-633XZ7UF.js +3583 -0
- package/dist/chunk-63DHNHGA.js +236 -0
- package/dist/chunk-666656AR.js +568 -0
- package/dist/chunk-67YRJPJO.js +19 -0
- package/dist/chunk-6CAU7VBA.js +20 -0
- package/dist/chunk-6CB3KJRY.js +21 -0
- package/dist/chunk-6DBMALXL.js +29 -0
- package/dist/chunk-6E25C4IA.js +269 -0
- package/dist/chunk-6FIBNIQU.js +73 -0
- package/dist/chunk-6G57FYBV.js +189 -0
- package/dist/chunk-6KZKRTG3.js +4885 -0
- package/dist/chunk-6LAH2UV2.js +525 -0
- package/dist/chunk-6PZY2TTU.js +921 -0
- package/dist/chunk-6T6NHXK4.js +230 -0
- package/dist/chunk-6TBEX4R4.js +432 -0
- package/dist/chunk-6TRD7TEE.js +911 -0
- package/dist/chunk-6XCP2RAI.js +47 -0
- package/dist/chunk-726G6WDL.js +351 -0
- package/dist/chunk-75EMT7BF.js +616 -0
- package/dist/chunk-762ACDZJ.js +4696 -0
- package/dist/chunk-77I6G4CO.js +1 -0
- package/dist/chunk-7DJ6SQH3.js +510 -0
- package/dist/chunk-7HPCIOMA.js +1383 -0
- package/dist/chunk-7HYBSRPZ.js +17503 -0
- package/dist/chunk-7KBEOPVB.js +73 -0
- package/dist/chunk-7LHJDJB3.js +2222 -0
- package/dist/chunk-7NVJMH52.js +306 -0
- package/dist/chunk-7RMARDVH.js +33 -0
- package/dist/chunk-7SEWU4QG.js +36 -0
- package/dist/chunk-7TUTZTI2.js +911 -0
- package/dist/chunk-7VBDFPUM.js +340 -0
- package/dist/chunk-7VRJHTGZ.js +6406 -0
- package/dist/chunk-7YAOUUKK.js +493 -0
- package/dist/chunk-AAAODV76.js +860 -0
- package/dist/chunk-AB4HUJVQ.js +339 -0
- package/dist/chunk-AEQWV746.js +611 -0
- package/dist/chunk-AHU3FKPA.js +11639 -0
- package/dist/chunk-AISTV5XU.js +226 -0
- package/dist/chunk-AJBTKGJZ.js +48 -0
- package/dist/chunk-AJJNW7XT.js +248 -0
- package/dist/chunk-AJN5Y5YZ.js +160 -0
- package/dist/chunk-ALDTFNZ3.js +150 -0
- package/dist/chunk-ARARVIZK.js +281 -0
- package/dist/chunk-ATBM6AW5.js +53 -0
- package/dist/chunk-ATKEKM3E.js +206 -0
- package/dist/chunk-AUJLW6JZ.js +92 -0
- package/dist/chunk-AY3MHEFM.js +24 -0
- package/dist/chunk-B7PYTV72.js +236 -0
- package/dist/chunk-B7YRSULE.js +1423 -0
- package/dist/chunk-BAWJNMWF.js +339 -0
- package/dist/chunk-BBPIILO2.js +88 -0
- package/dist/chunk-BD3PGW6A.js +957 -0
- package/dist/chunk-BDPYFAB4.js +44 -0
- package/dist/chunk-BIUV23HH.js +729 -0
- package/dist/chunk-BQ3VAGXH.js +243 -0
- package/dist/chunk-BQJOGAWT.js +31 -0
- package/dist/chunk-BT7EG7CO.js +68 -0
- package/dist/chunk-BV3XQQLU.js +27 -0
- package/dist/chunk-BY6ODHNZ.js +78 -0
- package/dist/chunk-BZRLSZNV.js +79 -0
- package/dist/chunk-C3DH3NCY.js +2773 -0
- package/dist/chunk-C3KCMAJP.js +756 -0
- package/dist/chunk-C7AGUJ5K.js +267 -0
- package/dist/chunk-CBSWKQCX.js +342 -0
- package/dist/chunk-CE5OZBYY.js +21 -0
- package/dist/chunk-CFCVYHSW.js +373 -0
- package/dist/chunk-CHA2HDE5.js +126 -0
- package/dist/chunk-CIHWPGS5.js +146 -0
- package/dist/chunk-CJBSZX3F.js +127 -0
- package/dist/chunk-CLA6QIJM.js +43 -0
- package/dist/chunk-CPSVQR2I.js +365 -0
- package/dist/chunk-CRPRVNQ4.js +254 -0
- package/dist/chunk-CSIE4Y7C.js +1093 -0
- package/dist/chunk-CSOPSYDY.js +142 -0
- package/dist/chunk-CSXLLEGB.js +205 -0
- package/dist/chunk-CTDLERWU.js +293 -0
- package/dist/chunk-CULBXMSK.js +110 -0
- package/dist/chunk-D2M2Y5ID.js +21 -0
- package/dist/chunk-D37WY7SH.js +1060 -0
- package/dist/chunk-D4VJFCGZ.js +89 -0
- package/dist/chunk-D6WTZMZB.js +48 -0
- package/dist/chunk-DBHOWDUH.js +976 -0
- package/dist/chunk-DCACX7BV.js +19364 -0
- package/dist/chunk-DFHX4XYA.js +128 -0
- package/dist/chunk-DIKBRRKE.js +176 -0
- package/dist/chunk-DJG5RIOR.js +9 -0
- package/dist/chunk-DNIOIISW.js +616 -0
- package/dist/chunk-DOA5G7IA.js +315 -0
- package/dist/chunk-DP7FKP6W.js +855 -0
- package/dist/chunk-DS2JZ677.js +79 -0
- package/dist/chunk-DSRLVYBS.js +82 -0
- package/dist/chunk-EAEVKNJA.js +209 -0
- package/dist/chunk-EEUESN3T.js +575 -0
- package/dist/chunk-EJ4LUU5Y.js +73 -0
- package/dist/chunk-EJIWTPSZ.js +1063 -0
- package/dist/chunk-EJVCDTAN.js +116 -0
- package/dist/chunk-EKLUBDBQ.js +627 -0
- package/dist/chunk-EN62WLFD.js +849 -0
- package/dist/chunk-ENYRCL43.js +26 -0
- package/dist/chunk-EP22ZMFO.js +273 -0
- package/dist/chunk-EP4SVAVI.js +87 -0
- package/dist/chunk-EPAMVGI3.js +1562 -0
- package/dist/chunk-EPRMH6HO.js +24 -0
- package/dist/chunk-ES6NWJ36.js +4057 -0
- package/dist/chunk-ETA2K4KI.js +30 -0
- package/dist/chunk-ETAD4NKD.js +31 -0
- package/dist/chunk-EUWIIDQ7.js +93 -0
- package/dist/chunk-EVE3OD2W.js +141 -0
- package/dist/chunk-EXA26FFC.js +459 -0
- package/dist/chunk-EXSPAA35.js +244 -0
- package/dist/chunk-F55YABKV.js +258 -0
- package/dist/chunk-F5DNGU5W.js +27 -0
- package/dist/chunk-F622MJYV.js +103 -0
- package/dist/chunk-F6NIQD74.js +5111 -0
- package/dist/chunk-FAWJTC34.js +43 -0
- package/dist/chunk-FAZBRHGG.js +47 -0
- package/dist/chunk-FIEUSGNJ.js +39 -0
- package/dist/chunk-FKCG7K6H.js +1146 -0
- package/dist/chunk-FOVVXV45.js +229 -0
- package/dist/chunk-FRBPKBFE.js +30 -0
- package/dist/chunk-FSA5BPFI.js +268 -0
- package/dist/chunk-FSJ5TTZ3.js +29 -0
- package/dist/chunk-FT7SGLXT.js +193 -0
- package/dist/chunk-FUUVZZUB.js +62 -0
- package/dist/chunk-FYMRVUNZ.js +1697 -0
- package/dist/chunk-FYTAA5FG.js +162 -0
- package/dist/chunk-G3VB2UCZ.js +3892 -0
- package/dist/chunk-G6OIMVXH.js +405 -0
- package/dist/chunk-G7PQCXVH.js +228 -0
- package/dist/chunk-GAB6UW52.js +1087 -0
- package/dist/chunk-GAFJEYLK.js +29 -0
- package/dist/chunk-GC4CRMTS.js +642 -0
- package/dist/chunk-GFHSIWHL.js +5297 -0
- package/dist/chunk-GI5M67YA.js +735 -0
- package/dist/chunk-GIJWHUWF.js +191 -0
- package/dist/chunk-GO6DRECI.js +1 -0
- package/dist/chunk-GOFVBTMN.js +314 -0
- package/dist/chunk-GQIP72ZJ.js +13 -0
- package/dist/chunk-GQXLXRFA.js +28 -0
- package/dist/chunk-GT6HFKJB.js +48 -0
- package/dist/chunk-GVJGSSKM.js +174 -0
- package/dist/chunk-GWHGTG37.js +36 -0
- package/dist/chunk-GZAJHLKO.js +18 -0
- package/dist/chunk-GZK62I3N.js +314 -0
- package/dist/chunk-H3HGMISG.js +483 -0
- package/dist/chunk-H3ISZJMJ.js +403 -0
- package/dist/chunk-H4HXVWEC.js +9 -0
- package/dist/chunk-H6MJGKAW.js +120 -0
- package/dist/chunk-HBTTGVR5.js +353 -0
- package/dist/chunk-HBZ322WZ.js +35 -0
- package/dist/chunk-HEP3ILP4.js +34 -0
- package/dist/chunk-HGFXODKO.js +106 -0
- package/dist/chunk-HHWDTB6S.js +59 -0
- package/dist/chunk-HKO45NLZ.js +286 -0
- package/dist/chunk-HLVTEA6A.js +89 -0
- package/dist/chunk-HS3SY6HN.js +160 -0
- package/dist/chunk-HT5OCGKN.js +899 -0
- package/dist/chunk-HXIIEMGF.js +78 -0
- package/dist/chunk-I56TH7VM.js +46 -0
- package/dist/chunk-I6FN3R5D.js +352 -0
- package/dist/chunk-I7SCBHJN.js +655 -0
- package/dist/chunk-IJM4GKAT.js +529 -0
- package/dist/chunk-IMEYU36R.js +344 -0
- package/dist/chunk-INJ3SS6A.js +936 -0
- package/dist/chunk-IUKOPCCE.js +441 -0
- package/dist/chunk-IVEPZQJO.js +33 -0
- package/dist/chunk-IVZEONFC.js +43 -0
- package/dist/chunk-IWBHRSG7.js +936 -0
- package/dist/chunk-IYY3HQAD.js +484 -0
- package/dist/chunk-J2ICMYDA.js +48 -0
- package/dist/chunk-J2QCFLUN.js +505 -0
- package/dist/chunk-J5HM6V5P.js +1039 -0
- package/dist/chunk-J6NZX6QH.js +398 -0
- package/dist/chunk-JBBB2Y47.js +432 -0
- package/dist/chunk-JDCZQZU7.js +42 -0
- package/dist/chunk-JEDNXGSS.js +169 -0
- package/dist/chunk-JHP7OT6K.js +89 -0
- package/dist/chunk-JLS65NUQ.js +14 -0
- package/dist/chunk-JNMWYMC6.js +38 -0
- package/dist/chunk-JNSTWNNW.js +25 -0
- package/dist/chunk-JOSK6GEU.js +9564 -0
- package/dist/chunk-JQUMSIGC.js +100 -0
- package/dist/chunk-JVHGMURQ.js +1106 -0
- package/dist/chunk-JYXS47FQ.js +951 -0
- package/dist/chunk-K2ZMW2Z5.js +2147 -0
- package/dist/chunk-K7FAFORM.js +14 -0
- package/dist/chunk-KBJXX3ON.js +333 -0
- package/dist/chunk-KHFKVQWY.js +486 -0
- package/dist/chunk-KHMEI3DQ.js +173 -0
- package/dist/chunk-KHW3UD7B.js +69 -0
- package/dist/chunk-KKQKWAMF.js +365 -0
- package/dist/chunk-KOKXJV2U.js +455 -0
- package/dist/chunk-KQ4NGOJI.js +14 -0
- package/dist/chunk-KQYQOXMS.js +342 -0
- package/dist/chunk-KTMW65UX.js +14 -0
- package/dist/chunk-KUEWV2JH.js +593 -0
- package/dist/chunk-KXYDV3HP.js +1523 -0
- package/dist/chunk-KZ4JMGZ5.js +96 -0
- package/dist/chunk-KZWGBUIO.js +2659 -0
- package/dist/chunk-L2CBG5IX.js +1892 -0
- package/dist/chunk-L2S235CU.js +314 -0
- package/dist/chunk-L32WD57F.js +98 -0
- package/dist/chunk-LDJCKAJP.js +123 -0
- package/dist/chunk-LDWNBEN7.js +541 -0
- package/dist/chunk-LJVH57G2.js +241 -0
- package/dist/chunk-LPQ7NZEN.js +209 -0
- package/dist/chunk-LRGH3CCV.js +148 -0
- package/dist/chunk-LULOGMMM.js +1133 -0
- package/dist/chunk-LWGKN2GY.js +8284 -0
- package/dist/chunk-LX4F24ZH.js +157 -0
- package/dist/chunk-M4BUEXXF.js +839 -0
- package/dist/chunk-M6UHOC4I.js +553 -0
- package/dist/chunk-MCBNWZNO.js +657 -0
- package/dist/chunk-MEMSY6VY.js +223 -0
- package/dist/chunk-MHLEZQQI.js +145 -0
- package/dist/chunk-MJ6WNSIX.js +192 -0
- package/dist/chunk-MKASIKWZ.js +442 -0
- package/dist/chunk-MNOVBYFU.js +60 -0
- package/dist/chunk-MO2BYAAF.js +2896 -0
- package/dist/chunk-MP6P7Y2A.js +180 -0
- package/dist/chunk-MQ4TFJAG.js +3365 -0
- package/dist/chunk-MT2WAIR3.js +131 -0
- package/dist/chunk-MTU7VTNN.js +1122 -0
- package/dist/chunk-MULMXX4V.js +150 -0
- package/dist/chunk-MW2DWWVA.js +147 -0
- package/dist/chunk-MY3ZPUN4.js +151 -0
- package/dist/chunk-MYLH2S4G.js +64 -0
- package/dist/chunk-MZ5WYKNA.js +28 -0
- package/dist/chunk-N3Q2JNPV.js +249 -0
- package/dist/chunk-N5DX4JAK.js +12 -0
- package/dist/chunk-NAINGKHF.js +35 -0
- package/dist/chunk-NEQUQHOX.js +223 -0
- package/dist/chunk-NHNPEZS4.js +1408 -0
- package/dist/chunk-NHTIXJCR.js +17 -0
- package/dist/chunk-NKLPMBNV.js +1154 -0
- package/dist/chunk-NMVZ5YES.js +199 -0
- package/dist/chunk-NRFRY3GE.js +256 -0
- package/dist/chunk-NTD6CSI6.js +1266 -0
- package/dist/chunk-NUO7B3SH.js +318 -0
- package/dist/chunk-NVOHIJKG.js +1319 -0
- package/dist/chunk-NXH3RX7O.js +523 -0
- package/dist/chunk-NXZ3DURF.js +257 -0
- package/dist/chunk-O26L6F7F.js +209 -0
- package/dist/chunk-O5RRIUEP.js +124 -0
- package/dist/chunk-O5X6BKAO.js +10 -0
- package/dist/chunk-O6ZQUVTR.js +31 -0
- package/dist/chunk-O7FE75QZ.js +306 -0
- package/dist/chunk-OC7Z6AJQ.js +81 -0
- package/dist/chunk-OH7IEZ63.js +307 -0
- package/dist/chunk-OI734GRS.js +115 -0
- package/dist/chunk-OJBH62DK.js +684 -0
- package/dist/chunk-OKJLVSTJ.js +247 -0
- package/dist/chunk-OMX5VL43.js +79 -0
- package/dist/chunk-ONHAMBN6.js +12 -0
- package/dist/chunk-ONYPLIVE.js +753 -0
- package/dist/chunk-OSK7QWPD.js +637 -0
- package/dist/chunk-OTXLMJKP.js +438 -0
- package/dist/chunk-OV5PKQMA.js +213 -0
- package/dist/chunk-OXK5HA4B.js +31 -0
- package/dist/chunk-OYDWFQA4.js +272 -0
- package/dist/chunk-OZAUKWHB.js +22 -0
- package/dist/chunk-P5P2EKGH.js +94 -0
- package/dist/chunk-P6SSWAKY.js +29 -0
- package/dist/chunk-P7CQGPLS.js +30 -0
- package/dist/chunk-P7RVCHQV.js +197 -0
- package/dist/chunk-PEBWDYIW.js +726 -0
- package/dist/chunk-PFCXFO7A.js +9193 -0
- package/dist/chunk-PFLDWMX6.js +168 -0
- package/dist/chunk-PHZGLQSK.js +15883 -0
- package/dist/chunk-PNFTPNSR.js +20 -0
- package/dist/chunk-PPHXX4W7.js +175 -0
- package/dist/chunk-PPZESPJT.js +262 -0
- package/dist/chunk-PRDHI2L5.js +296 -0
- package/dist/chunk-PSTKWHHN.js +32 -0
- package/dist/chunk-PV53ODOT.js +555 -0
- package/dist/chunk-PVJBTI7P.js +24 -0
- package/dist/chunk-PXKERBKX.js +100 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-Q3DHEZA5.js +85 -0
- package/dist/chunk-Q5RZDVQK.js +204 -0
- package/dist/chunk-Q64IR2Z2.js +4933 -0
- package/dist/chunk-Q6Z5UGPL.js +398 -0
- package/dist/chunk-QAMAV4DT.js +627 -0
- package/dist/chunk-QBWF4444.js +112 -0
- package/dist/chunk-QDKYT4CI.js +22805 -0
- package/dist/chunk-QFLBRWCJ.js +126 -0
- package/dist/chunk-QILAQACE.js +107 -0
- package/dist/chunk-QJ6OEH4K.js +3683 -0
- package/dist/chunk-QK3NLYTV.js +320 -0
- package/dist/chunk-QOFLUS24.js +535 -0
- package/dist/chunk-QOIASNEJ.js +98 -0
- package/dist/chunk-QP4UTSOV.js +3587 -0
- package/dist/chunk-QP5ST7RI.js +904 -0
- package/dist/chunk-QS5AGRZN.js +167 -0
- package/dist/chunk-QTAOU53N.js +107 -0
- package/dist/chunk-QTCLJHLZ.js +351 -0
- package/dist/chunk-QTOLTG4L.js +119 -0
- package/dist/chunk-QWDV4OSL.js +142 -0
- package/dist/chunk-QWMQ74KD.js +508 -0
- package/dist/chunk-QX6LOMMH.js +158 -0
- package/dist/chunk-QXBKIHY7.js +238 -0
- package/dist/chunk-QXBKTJT6.js +270 -0
- package/dist/chunk-QYQOVOLR.js +196 -0
- package/dist/chunk-R2CKS6WT.js +73 -0
- package/dist/chunk-R2TIYG7U.js +68 -0
- package/dist/chunk-R2W3LESH.js +465 -0
- package/dist/chunk-R5EXHMUN.js +100 -0
- package/dist/chunk-RBBTBCHX.js +289 -0
- package/dist/chunk-RBKLOUXA.js +121 -0
- package/dist/chunk-RCP2Y6DQ.js +86 -0
- package/dist/chunk-RCYEJSD5.js +260 -0
- package/dist/chunk-RFDVSI4N.js +134 -0
- package/dist/chunk-RGJU63FV.js +1717 -0
- package/dist/chunk-RI4EX2QE.js +18 -0
- package/dist/chunk-RIMARPCF.js +152 -0
- package/dist/chunk-RJ6RSNT6.js +42 -0
- package/dist/chunk-RNI2GUIF.js +131 -0
- package/dist/chunk-RPHUWJ3K.js +264 -0
- package/dist/chunk-RPIMAMV7.js +361 -0
- package/dist/chunk-RQFX6YRN.js +49 -0
- package/dist/chunk-RRC5BL7B.js +285 -0
- package/dist/chunk-RSYTSBOO.js +79 -0
- package/dist/chunk-RWQQIKIC.js +35 -0
- package/dist/chunk-RZUBOZH2.js +373 -0
- package/dist/chunk-S2E632FQ.js +380 -0
- package/dist/chunk-S2VQCZO4.js +22 -0
- package/dist/chunk-S2XIMQJQ.js +681 -0
- package/dist/chunk-S47ISY27.js +183 -0
- package/dist/chunk-S6I4KISU.js +142 -0
- package/dist/chunk-S6TAFQI5.js +2403 -0
- package/dist/chunk-SBBNT5ZJ.js +27 -0
- package/dist/chunk-SDJV2DTQ.js +491 -0
- package/dist/chunk-SDPIFNLD.js +33 -0
- package/dist/chunk-SF6YK3JI.js +835 -0
- package/dist/chunk-SK3ZFONO.js +976 -0
- package/dist/chunk-SKTFRFQ7.js +1775 -0
- package/dist/chunk-SNGM452K.js +2002 -0
- package/dist/chunk-STW7OFOA.js +581 -0
- package/dist/chunk-SU6UFUQY.js +1351 -0
- package/dist/chunk-SVJM3LJA.js +27 -0
- package/dist/chunk-SVUNZOPG.js +129 -0
- package/dist/chunk-T5XLUYV3.js +34 -0
- package/dist/chunk-T7E5JS6N.js +57 -0
- package/dist/chunk-TA3N5MMO.js +450 -0
- package/dist/chunk-TAJPCXSB.js +13 -0
- package/dist/chunk-TDEMDRN2.js +1639 -0
- package/dist/chunk-TDSEX5CF.js +24 -0
- package/dist/chunk-TGCVL74Y.js +28 -0
- package/dist/chunk-TGXXYDL4.js +1012 -0
- package/dist/chunk-TIKTYUJO.js +296 -0
- package/dist/chunk-TIZAJD4M.js +6029 -0
- package/dist/chunk-TLSEZZCG.js +5549 -0
- package/dist/chunk-TMBDRHAX.js +19 -0
- package/dist/chunk-TRURQUBP.js +1217 -0
- package/dist/chunk-TSQ256N5.js +152 -0
- package/dist/chunk-TUQMMFEQ.js +2244 -0
- package/dist/chunk-U424MJLC.js +81 -0
- package/dist/chunk-U4DRS6ZW.js +11 -0
- package/dist/chunk-U5KS22IN.js +204 -0
- package/dist/chunk-UAPHFJPN.js +673 -0
- package/dist/chunk-UB7LQITD.js +1040 -0
- package/dist/chunk-UBVRHGAZ.js +237 -0
- package/dist/chunk-UDNNAQN5.js +98 -0
- package/dist/chunk-UJP3XM54.js +342 -0
- package/dist/chunk-UJYDH2MW.js +305 -0
- package/dist/chunk-UKMPASIC.js +399 -0
- package/dist/chunk-UKUL3EHK.js +763 -0
- package/dist/chunk-UPHU64NB.js +1327 -0
- package/dist/chunk-URMSZXAD.js +43 -0
- package/dist/chunk-US3RT6CY.js +525 -0
- package/dist/chunk-UTDEOPKV.js +30 -0
- package/dist/chunk-UUUDKV7N.js +13 -0
- package/dist/chunk-UVKWU3SM.js +39 -0
- package/dist/chunk-V2TVPO3X.js +521 -0
- package/dist/chunk-V3KLVK7B.js +398 -0
- package/dist/chunk-V3YFFHDR.js +17 -0
- package/dist/chunk-VAG6GUBR.js +1162 -0
- package/dist/chunk-VBENAHDA.js +1814 -0
- package/dist/chunk-VBNCKCCI.js +16 -0
- package/dist/chunk-VCGSWN5M.js +1300 -0
- package/dist/chunk-VE2O2I7M.js +115 -0
- package/dist/chunk-VIFHYTI5.js +31 -0
- package/dist/chunk-VJER3FUS.js +5080 -0
- package/dist/chunk-VLMDM56Q.js +268 -0
- package/dist/chunk-VMFZEVVX.js +99 -0
- package/dist/chunk-VNDPQVUZ.js +1853 -0
- package/dist/chunk-VSPJRLGA.js +4783 -0
- package/dist/chunk-VSWC5ZXI.js +103 -0
- package/dist/chunk-VYI4DSZP.js +2017 -0
- package/dist/chunk-W4BAIQ6L.js +1212 -0
- package/dist/chunk-W7VQAL2K.js +2203 -0
- package/dist/chunk-WEI3ZDZH.js +33 -0
- package/dist/chunk-WEIUS5NT.js +26 -0
- package/dist/chunk-WEW7UFWE.js +546 -0
- package/dist/chunk-WGQAXLNA.js +384 -0
- package/dist/chunk-WJAL4JL3.js +206 -0
- package/dist/chunk-WKFOJ5ZG.js +141 -0
- package/dist/chunk-WMWQEFLN.js +2177 -0
- package/dist/chunk-WNECKKO2.js +154 -0
- package/dist/chunk-WPKLYOAM.js +195 -0
- package/dist/chunk-WSH37YQN.js +243 -0
- package/dist/chunk-WV3KQDPE.js +84 -0
- package/dist/chunk-WZFC4V4U.js +389 -0
- package/dist/chunk-XCOIBNET.js +13 -0
- package/dist/chunk-XDM65LBX.js +53 -0
- package/dist/chunk-XGU6PBO5.js +645 -0
- package/dist/chunk-XHOI4V62.js +43 -0
- package/dist/chunk-XIBVFMLQ.js +6858 -0
- package/dist/chunk-XJ2E7YEJ.js +117 -0
- package/dist/chunk-XON4ZCYX.js +69 -0
- package/dist/chunk-XQPEWJPI.js +127 -0
- package/dist/chunk-XSJTIFUY.js +31 -0
- package/dist/chunk-XT7OVWM3.js +64 -0
- package/dist/chunk-XTXYIISX.js +1104 -0
- package/dist/chunk-XV7NZL4B.js +57 -0
- package/dist/chunk-XWIZ6B5S.js +109 -0
- package/dist/chunk-XWLIKOBX.js +240 -0
- package/dist/chunk-XZOXEIN4.js +156 -0
- package/dist/chunk-XZRS7I7W.js +3673 -0
- package/dist/chunk-Y4ZDDHB6.js +44 -0
- package/dist/chunk-Y733RB7T.js +194 -0
- package/dist/chunk-YBHUB4XR.js +14931 -0
- package/dist/chunk-YKC75XK4.js +147 -0
- package/dist/chunk-YKMXFVK7.js +27 -0
- package/dist/chunk-YO5BAJ55.js +225 -0
- package/dist/chunk-YPDCLXNZ.js +568 -0
- package/dist/chunk-YSQPGVFK.js +116 -0
- package/dist/chunk-YTVVTIWJ.js +716 -0
- package/dist/chunk-YVCMDVBB.js +698 -0
- package/dist/chunk-YYTOA6DP.js +114 -0
- package/dist/chunk-Z5WGNULV.js +88 -0
- package/dist/chunk-Z6NGVVXG.js +66 -0
- package/dist/chunk-Z7CDQNH3.js +20 -0
- package/dist/chunk-ZH4G3FU5.js +187 -0
- package/dist/chunk-ZJB6SQPA.js +257 -0
- package/dist/chunk-ZL6E4HMY.js +42 -0
- package/dist/chunk-ZMLOC3DW.js +62 -0
- package/dist/chunk-ZORW64QH.js +101 -0
- package/dist/chunk-ZVVD2XGV.js +1369 -0
- package/dist/cli-utils-ER33ODC5.js +31 -0
- package/dist/cli-validation-6CHPTANV.js +146 -0
- package/dist/commit-utils-3STAY7C7.js +49 -0
- package/dist/compatibility-notice-IHNXR2JR.js +21 -0
- package/dist/compete-config-resolver-V3GDNJMM.js +125 -0
- package/dist/compete-results-XUM3PJHS.js +318 -0
- package/dist/competition-outcome-tracker-NWSMVUPG.js +62 -0
- package/dist/config-4HHE4OIK.js +238 -0
- package/dist/config-D6YAZASH.js +10 -0
- package/dist/config-MIPRDYYV.js +46 -0
- package/dist/config-audit-GA5DCUAI.js +371 -0
- package/dist/config-explain-4LMO556H.js +71 -0
- package/dist/config-loader-RGP634PC.js +51 -0
- package/dist/config-manager-J2UTO635.js +114 -0
- package/dist/config-merger-TVUJBZTS.js +14 -0
- package/dist/config-validator-H4ZVIE56.js +122 -0
- package/dist/cost-HSIV5U2Y.js +85 -0
- package/dist/data-migration-state-OAUL2WPQ.js +35 -0
- package/dist/db-migrator-NUOW5NKR.js +34 -0
- package/dist/default-paths-UILB62BN.js +49 -0
- package/dist/discovery-V6GYUJUC.js +53 -0
- package/dist/doctor-BBSMCDWE.js +271 -0
- package/dist/doctor-context-blocks-LPY75VZA.js +231 -0
- package/dist/doctor-instructions-import-JXHDDOE5.js +70 -0
- package/dist/doctor-purpose-6YJOXPBE.js +30 -0
- package/dist/domain-analyzer-S35ABUNA.js +48 -0
- package/dist/ensure-initialized-Q6PEU55O.js +78 -0
- package/dist/entry.js +922 -0
- package/dist/error-parser-core-MZFMFANN.js +70 -0
- package/dist/evaluation-context-GQBYDAEV.js +63 -0
- package/dist/evidence-plan-resolver-5FUAP3SA.js +111 -0
- package/dist/execution-data-writer-YCG5UH37.js +72 -0
- package/dist/execution-policy-applier-PWZM5LNA.js +63 -0
- package/dist/execution-preflight-DASJWWQF.js +223 -0
- package/dist/execution-roles-KNSNNB2D.js +112 -0
- package/dist/exit-code-error-QIY5DOKF.js +18 -0
- package/dist/external-temp-cleanup-CAW7LQ6R.js +360 -0
- package/dist/factory-YCYCB7AE.js +209 -0
- package/dist/file-utils-ZQYNBQBW.js +45 -0
- package/dist/flywheel-allowlist-revert-H22HA6JE.js +21 -0
- package/dist/flywheel-autoloop-executor-PDOASJEZ.js +316 -0
- package/dist/flywheel-autoloop-unverified-TOKYFCDC.js +36 -0
- package/dist/flywheel-competition-executor-YJUMMEJO.js +320 -0
- package/dist/flywheel-executor-JWU43KAY.js +437 -0
- package/dist/flywheel-git-isolation-FQ2TRWMB.js +56 -0
- package/dist/flywheel-manifest-HDPNMWWF.js +114 -0
- package/dist/flywheel-model-defaults-UHAKTZT3.js +110 -0
- package/dist/flywheel-preflight-III2C7TH.js +163 -0
- package/dist/flywheel-resume-2O45MS7Y.js +31 -0
- package/dist/flywheel-resume-execution-lease-P2BK257J.js +59 -0
- package/dist/flywheel-safety-AVAVFXIJ.js +19 -0
- package/dist/flywheel-scope-decision-ALJUUNGG.js +14 -0
- package/dist/get-telemetry-logger-MPDVO2JA.js +155 -0
- package/dist/git-message-style-KCXGMTDT.js +104 -0
- package/dist/guidance-profile-config-G3K5YQOO.js +113 -0
- package/dist/guidance-profile-renderer-TX4TNDFL.js +23 -0
- package/dist/identity-XLQJ4YJF.js +25 -0
- package/dist/index-query-6NUY4FKX.js +37 -0
- package/dist/index.js +85588 -0
- package/dist/indexer-DL54CM3K.js +44 -0
- package/dist/init-OUZ72344.js +245 -0
- package/dist/instructions-import-offer-NSYXSITC.js +363 -0
- package/dist/instructions-run-NBII7W36.js +114 -0
- package/dist/interrupted-work-E4TLKJLE.js +43 -0
- package/dist/invoke-provider-auth-NEWEIHCI.js +38 -0
- package/dist/judge-scoring.json +64 -0
- package/dist/lab-mode-YXQ2Y3NG.js +44 -0
- package/dist/lab-utils-5QAMDJR5.js +29 -0
- package/dist/linux-host-class-YDWQX65O.js +31 -0
- package/dist/llm-judge-executor-OJSNCTFJ.js +179 -0
- package/dist/local-ci-config-VZ4WWDT5.js +48 -0
- package/dist/local-executor-DCMDDRX2.js +280 -0
- package/dist/logger-5KNKS3O4.js +154 -0
- package/dist/matrix-prompt-builder-4ZBOGZSB.js +527 -0
- package/dist/matrix-result-parser-EZJM4YXP.js +14 -0
- package/dist/merge-batch-RTEEMVDB.js +313 -0
- package/dist/merge-batch-members-VIWGVRHP.js +24 -0
- package/dist/merge-queue-TIIUSOY6.js +126 -0
- package/dist/merge-queue-trust-DUNZ4J65.js +21 -0
- package/dist/metadata-UKSMWM6J.js +29 -0
- package/dist/model-catalog-readiness-R5KJBP5M.js +202 -0
- package/dist/model-invocations-db-XTUPPQ34.js +70 -0
- package/dist/model-resolver-JFZ2MHOJ.js +39 -0
- package/dist/model-resolver-SCQ57KAQ.js +217 -0
- package/dist/model-roles-D3OR4NAH.js +110 -0
- package/dist/monitor-F4SIU5G2.js +96 -0
- package/dist/nested-worktrees-2WUWQ75N.js +23 -0
- package/dist/next-S3IRLVER.js +96 -0
- package/dist/next-UKPFRXOO.js +148 -0
- package/dist/opportunistic-TL4S26VX.js +86 -0
- package/dist/optimization-config-resolver-NAEIYU6A.js +148 -0
- package/dist/package-HJR4QJT4.js +10 -0
- package/dist/parse-git-status-7PIYFPS2.js +21 -0
- package/dist/path-security-5WJSY56G.js +41 -0
- package/dist/paths-JUI7C4KU.js +45 -0
- package/dist/plan-backlog-list-fast-SSTQL5XP.js +89 -0
- package/dist/plan-backlog-show-fast-PFRAQ3JB.js +114 -0
- package/dist/plan-sprint-list-fast-HD2KBZSY.js +110 -0
- package/dist/plan-sprint-status-fast-YFYH7WQF.js +116 -0
- package/dist/plan-task-status-fast-JU5PNLUU.js +114 -0
- package/dist/plan-to-flywheel-PY5MSY2H.js +529 -0
- package/dist/preflight-PJHGTJNZ.js +12 -0
- package/dist/process-registry-B4ZUCN7T.js +28 -0
- package/dist/process-scanner-TU3QHWVB.js +23 -0
- package/dist/processes-6FHVGT72.js +28 -0
- package/dist/processes-json-fast-YWTMUWXB.js +72 -0
- package/dist/profile-HWTC56SG.js +209 -0
- package/dist/project-runtime-paths-2UQNG5Y4.js +41 -0
- package/dist/protected-branches-N36LDBFY.js +71 -0
- package/dist/provider-aware-resource-manager-YTFAHNGJ.js +40 -0
- package/dist/provider-matrix-2KO2V5OM.js +214 -0
- package/dist/provider-matrix-renderer-MEC3PER7.js +139 -0
- package/dist/provider-output-forwarding-2AHL7YMA.js +26 -0
- package/dist/provider-registry-WCZ65QHC.js +136 -0
- package/dist/provider-temp-cleanup-IUNPJCZF.js +14 -0
- package/dist/prune-engine-EOQVWEP6.js +448 -0
- package/dist/publication-turn-S42RZTQF.js +44 -0
- package/dist/quality-gate-JQMVCKOO.js +72 -0
- package/dist/quickstart-3O6FRCFB.js +105 -0
- package/dist/readiness-GW6ABRHB.js +139 -0
- package/dist/readiness-KFZCXDTT.js +215 -0
- package/dist/registry-7XG376SY.js +217 -0
- package/dist/repair-helpers-TTV5FEF4.js +117 -0
- package/dist/repo-root-YG6MQZDW.js +28 -0
- package/dist/resolution-engine-FFEK7HQ2.js +187 -0
- package/dist/resolve-provider-cli-ZEXVZKET.js +44 -0
- package/dist/restore-telemetry-db-E7BR5PO7.js +459 -0
- package/dist/routing-events-AW6SRZ6G.js +13 -0
- package/dist/routing-history-store-FMDDQL6W.js +39 -0
- package/dist/routing-planner-C6OHO3EY.js +221 -0
- package/dist/runner-KHDNQ5KU.js +1194 -0
- package/dist/runner-PIS2BW7Y.js +1801 -0
- package/dist/safety-E3JJVSFL.js +67 -0
- package/dist/safety-stanza-HE6GXP4G.js +49 -0
- package/dist/sandbox-7LBZC5PE.js +72 -0
- package/dist/sandbox-ASWCDYZL.js +80 -0
- package/dist/schema-extensions.sql +234 -0
- package/dist/security-JI22TBEB.js +17 -0
- package/dist/setup-bb-plugin-FQXLA76Y.js +531 -0
- package/dist/setup-claude-code-NF4ZSHVE.js +197 -0
- package/dist/setup-temp-cleanup-2APZ4HMA.js +29 -0
- package/dist/shared-utils-34HJZ6JX.js +40 -0
- package/dist/simple-artifact-goal-MQLDDMMX.js +29 -0
- package/dist/sprint-N7JFTEGL.js +403 -0
- package/dist/sprint-bridge-KJXIBNRJ.js +117 -0
- package/dist/sprint-completion-dependents-34SR7JVL.js +43 -0
- package/dist/sprint-execution-service-A6JPUBWY.js +376 -0
- package/dist/sprint-manager-MU2ZUWN2.js +140 -0
- package/dist/sqlite-timeout-config-U2CT4LKU.js +13 -0
- package/dist/sqlite-wrapper-FSGJ2QEW.js +19 -0
- package/dist/stale-cleanup-orchestrator-4X6324VF.js +71 -0
- package/dist/storage-access-I3V5LER7.js +31 -0
- package/dist/store-XVG64ZR2.js +25 -0
- package/dist/task-classifier-MN2DYE5R.js +558 -0
- package/dist/task-list-fast-7AT6LTTA.js +112 -0
- package/dist/task-manager-LHPBBRRE.js +136 -0
- package/dist/task-report-OLPAWBTK.js +64 -0
- package/dist/task-type-detector-6RG3YNZF.js +27 -0
- package/dist/task-type-resolver-EQHOUIHT.js +64 -0
- package/dist/telemetry-5VYSK772.js +212 -0
- package/dist/telemetry-health-tracker-4GFW2EMP.js +12 -0
- package/dist/telemetry-path-resolver-H3NFHHQY.js +34 -0
- package/dist/telemetry-reliability-URJDTNPZ.js +76 -0
- package/dist/transports-NREZQTJK.js +106 -0
- package/dist/unified-cost-tracker-5MTBJ2CL.js +59 -0
- package/dist/unified-state-cleanup-GGAXLHXF.js +292 -0
- package/dist/universal-cost-calculator-JTFEJVWK.js +70 -0
- package/dist/user-agents-QLIGPLP2.js +30 -0
- package/dist/variant-execution-I4ZZUAWY.js +319 -0
- package/dist/variant-opt-parser-JHCXKY2K.js +251 -0
- package/dist/variant-request-input-PL37N3AN.js +11 -0
- package/dist/variant-state-cleanup-KHMXRBZN.js +287 -0
- package/dist/variant-worker-COB5YTJX.js +4517 -0
- package/dist/variant-worker.js +26 -0
- package/dist/variants-XGDSXYG3.js +267 -0
- package/dist/verification-command-YSPU4VBH.js +66 -0
- package/dist/verify-command-timeout-DQUYH33Z.js +11 -0
- package/dist/verify-commands-LOHMOFKC.js +65 -0
- package/dist/warning-JSKSFKBT.js +13 -0
- package/dist/work-edge-K65NHVWY.js +50 -0
- package/dist/work-item-writer-KY5ZWZWW.js +30 -0
- package/dist/worker-JNXXEIT6.js +1504 -0
- package/dist/worker.js +21 -0
- package/dist/workspace-tree-identity-JO5PSKTW.js +52 -0
- package/dist/worktree-environment-WTVABAQW.js +64 -0
- package/dist/worktree-metrics-VZRQI26T.js +66 -0
- package/dist/writeback-TQFQBJNU.js +50 -0
- package/docs/CLI_REFERENCE.md +8287 -0
- package/docs/PROVIDER_SETUP.md +1737 -0
- package/docs/README.md +96 -0
- package/docs/TROUBLESHOOTING.md +2209 -0
- package/docs/getting-started/QUICK_START.md +260 -0
- package/docs/getting-started/README.md +198 -0
- package/docs/getting-started/SETUP.md +25 -0
- package/docs/reference/AGENT_CONTEXT.md +84 -0
- package/docs/reference/DELIVERY_ONBOARDING.md +198 -0
- package/docs/reference/PROVIDER_RELEASE_TIERS.md +34 -0
- package/docs/reference/SECURITY.md +291 -0
- package/docs/reference/TELEMETRY_PRIVACY.md +259 -0
- package/integrations/bb-plugin-build/README.md +391 -0
- package/integrations/bb-plugin-build/app.css +431 -0
- package/integrations/bb-plugin-build/app.tsx +6210 -0
- package/integrations/bb-plugin-build/package.json +45 -0
- package/integrations/bb-plugin-build/server.ts +488 -0
- package/integrations/bb-plugin-build/src/adapter.ts +3672 -0
- package/integrations/bb-plugin-build/src/backlog-authoring-model.ts +150 -0
- package/integrations/bb-plugin-build/src/backlog-authoring-schema.ts +458 -0
- package/integrations/bb-plugin-build/src/backlog-authoring-service.ts +467 -0
- package/integrations/bb-plugin-build/src/backlog-authoring-view.ts +1025 -0
- package/integrations/bb-plugin-build/src/command-evidence-schema.ts +13 -0
- package/integrations/bb-plugin-build/src/competition-defaults-schema.ts +122 -0
- package/integrations/bb-plugin-build/src/competition-defaults-service.ts +105 -0
- package/integrations/bb-plugin-build/src/competition-defaults-view.ts +138 -0
- package/integrations/bb-plugin-build/src/contract.ts +718 -0
- package/integrations/bb-plugin-build/src/execution-activity-view.ts +98 -0
- package/integrations/bb-plugin-build/src/finalize-model.ts +205 -0
- package/integrations/bb-plugin-build/src/finalize-schema.ts +150 -0
- package/integrations/bb-plugin-build/src/finalize-service.ts +330 -0
- package/integrations/bb-plugin-build/src/finalize-view.ts +159 -0
- package/integrations/bb-plugin-build/src/judge-operations-readback.ts +303 -0
- package/integrations/bb-plugin-build/src/judge-operations-schema.ts +33 -0
- package/integrations/bb-plugin-build/src/judge-operations-view.ts +459 -0
- package/integrations/bb-plugin-build/src/model.ts +1318 -0
- package/integrations/bb-plugin-build/src/monitor-service.ts +233 -0
- package/integrations/bb-plugin-build/src/panel-read-ux.ts +97 -0
- package/integrations/bb-plugin-build/src/patch-preview-schema.ts +166 -0
- package/integrations/bb-plugin-build/src/patch-preview-service.ts +480 -0
- package/integrations/bb-plugin-build/src/patch-preview-view.ts +109 -0
- package/integrations/bb-plugin-build/src/planning-readback.ts +228 -0
- package/integrations/bb-plugin-build/src/planning-service.ts +647 -0
- package/integrations/bb-plugin-build/src/planning-sync-evidence.ts +37 -0
- package/integrations/bb-plugin-build/src/planning-workspace-readback.ts +33 -0
- package/integrations/bb-plugin-build/src/planning-workspace-schema.ts +131 -0
- package/integrations/bb-plugin-build/src/planning-workspace.ts +333 -0
- package/integrations/bb-plugin-build/src/poll-handoff.ts +136 -0
- package/integrations/bb-plugin-build/src/project-target-server.ts +21 -0
- package/integrations/bb-plugin-build/src/project-target.ts +45 -0
- package/integrations/bb-plugin-build/src/reference-index.ts +218 -0
- package/integrations/bb-plugin-build/src/repository-identity.ts +17 -0
- package/integrations/bb-plugin-build/src/repository-schema.ts +30 -0
- package/integrations/bb-plugin-build/src/result-explorer-view.ts +597 -0
- package/integrations/bb-plugin-build/src/result-readback-model.ts +163 -0
- package/integrations/bb-plugin-build/src/result-readback-schema.ts +319 -0
- package/integrations/bb-plugin-build/src/result-readback-service.ts +333 -0
- package/integrations/bb-plugin-build/src/run-page-view.ts +332 -0
- package/integrations/bb-plugin-build/src/setup-config-schema.ts +293 -0
- package/integrations/bb-plugin-build/src/setup-config-service.ts +620 -0
- package/integrations/bb-plugin-build/src/setup-config-view.ts +388 -0
- package/integrations/bb-plugin-build/src/sprint-close-model.ts +150 -0
- package/integrations/bb-plugin-build/src/sprint-close-schema.ts +128 -0
- package/integrations/bb-plugin-build/src/sprint-close-service.ts +293 -0
- package/integrations/bb-plugin-build/src/sprint-close-view.ts +116 -0
- package/integrations/bb-plugin-build/src/sprint-composition-model.ts +709 -0
- package/integrations/bb-plugin-build/src/sprint-composition-readback.ts +231 -0
- package/integrations/bb-plugin-build/src/sprint-composition-schema.ts +597 -0
- package/integrations/bb-plugin-build/src/sprint-composition-service.ts +773 -0
- package/integrations/bb-plugin-build/src/sprint-composition-view.ts +1221 -0
- package/integrations/bb-plugin-build/src/sprint-execution-schema.ts +188 -0
- package/integrations/bb-plugin-build/src/sprint-execution-service.ts +289 -0
- package/integrations/bb-plugin-build/src/sprint-execution-view.ts +54 -0
- package/integrations/bb-plugin-build/src/task-authoring-model.ts +563 -0
- package/integrations/bb-plugin-build/src/task-authoring-schema.ts +629 -0
- package/integrations/bb-plugin-build/src/task-authoring-service.ts +842 -0
- package/integrations/bb-plugin-build/src/task-authoring-view.ts +1227 -0
- package/integrations/bb-plugin-build/src/task-catalog-schema.ts +46 -0
- package/integrations/bb-plugin-build/src/task-catalog-service.ts +44 -0
- package/integrations/bb-plugin-build/src/task-detail-readback.ts +56 -0
- package/integrations/bb-plugin-build/src/task-detail-schema.ts +171 -0
- package/integrations/bb-plugin-build/src/task-navigation.ts +446 -0
- package/integrations/bb-plugin-build/src/workbench-route.ts +74 -0
- package/package.json +306 -4
- package/schemas/README.md +80 -0
- package/schemas/build.config.schema.json +2110 -0
- package/schemas/config-v1.schema.json +136 -0
- package/schemas/execution-config.schema.json +47 -0
- package/schemas/judge-scoring.schema.json +257 -0
- package/schemas/provider-config.schema.json +179 -0
- package/schemas/telemetry-config.schema.json +79 -0
- package/schemas/user-preferences.schema.json +141 -0
- package/scripts/assert-node-runtime.mjs +146 -0
- package/scripts/preflight-native.mjs +49 -0
- package/scripts/setup-git-hooks.mjs +39 -0
- package/scripts/sync.sh +2871 -0
- package/scripts/write-node-launcher.sh +138 -0
- package/src/resources/gemini/slash-packs/default/plan.toml +15 -0
- package/src/resources/gemini/slash-packs/default/summary.toml +16 -0
- package/src/resources/gemini/slash-packs/default/tests.toml +16 -0
- package/templates/.build-project/README.md +41 -0
- package/templates/.build-project/agents/README.md +50 -0
- package/templates/.build-project/agents/architect.md +10 -0
- package/templates/.build-project/agents/code-reviewer.md +10 -0
- package/templates/.build-project/agents/debugger.md +10 -0
- package/templates/.build-project/agents/documenter.md +10 -0
- package/templates/.build-project/agents/test-automator.md +10 -0
- package/templates/.build-project/config/build.config.jsonc +64 -0
- package/templates/.build-project/config/project-context.template.json +6 -0
- package/templates/.build-project/config/task-type-aliases.presets/kanban.yaml +14 -0
- package/templates/.build-project/config/task-type-aliases.presets/scrum.yaml +17 -0
- package/templates/.build-project/config/task-type-aliases.presets/xp.yaml +12 -0
- package/templates/.build-project/config/task-type-aliases.yaml +29 -0
- package/templates/.build-project/gitignore.template +70 -0
- package/templates/.build-project/task-types/analysis.yaml +38 -0
- package/templates/.build-project/task-types/architecture.yaml +35 -0
- package/templates/.build-project/task-types/doc.yaml +34 -0
- package/templates/.build-project/task-types/feature.yaml +22 -0
- package/templates/.build-project/task-types/general.yaml +4 -0
- package/templates/.build-project/task-types/security.yaml +38 -0
- package/templates/AGENTS.template.md +85 -0
- package/templates/CLAUDE.template.md +1 -0
- package/templates/GEMINI.template.md +1 -0
- package/templates/builtin-workflows/code-review.yaml +73 -0
- package/templates/builtin-workflows/skills/code-review/SKILL.md +61 -0
- package/templates/builtin-workflows/skills/execution-contract-analysis/SKILL.md +24 -0
- package/templates/builtin-workflows/skills/execution-contract-code/SKILL.md +19 -0
- package/templates/builtin-workflows/skills/execution-contract-structured/SKILL.md +17 -0
- package/templates/ci/incident-intake.mjs +787 -0
- package/templates/ci/incidents.yml.template +103 -0
- package/templates/ci/node.yml.template +17 -0
- package/templates/ci/python.yml.template +17 -0
- package/templates/guard-kit/GUARD_SETUP.md.template +258 -0
- package/templates/guard-kit/build-guard.yml.template +189 -0
- package/templates/guard-kit/check.mjs.template +932 -0
- package/templates/guard-kit/config.json.template +7 -0
- package/templates/guard-kit/policy.json.template +1 -0
- package/templates/profiles/README.md +57 -0
- package/templates/profiles/frontier-claude.json +23 -0
- package/templates/profiles/frontier-codex.json +23 -0
|
@@ -0,0 +1,4885 @@
|
|
|
1
|
+
import {
|
|
2
|
+
abortPendingSquashMerge,
|
|
3
|
+
acquireFlywheelApplyLock,
|
|
4
|
+
describeVerificationFailedStages,
|
|
5
|
+
failedOnlyOnUnavailableVerification,
|
|
6
|
+
readStagesUnverifiedDisposition,
|
|
7
|
+
renderWarn,
|
|
8
|
+
selectStageablePaths
|
|
9
|
+
} from "./chunk-QDKYT4CI.js";
|
|
10
|
+
import {
|
|
11
|
+
recordCompetitionOutcomeEvent,
|
|
12
|
+
resolveLatestEvaluationId
|
|
13
|
+
} from "./chunk-TSQ256N5.js";
|
|
14
|
+
import {
|
|
15
|
+
isNodeVerificationPath,
|
|
16
|
+
resolverVitestRun
|
|
17
|
+
} from "./chunk-PHZGLQSK.js";
|
|
18
|
+
import {
|
|
19
|
+
buildCompetitionOutcomeEvent
|
|
20
|
+
} from "./chunk-YBHUB4XR.js";
|
|
21
|
+
import {
|
|
22
|
+
openConfiguredTelemetryDb
|
|
23
|
+
} from "./chunk-KHW3UD7B.js";
|
|
24
|
+
import {
|
|
25
|
+
isRunnableProjectTestFile
|
|
26
|
+
} from "./chunk-JOSK6GEU.js";
|
|
27
|
+
import {
|
|
28
|
+
resolvePlanArtifactPath
|
|
29
|
+
} from "./chunk-V3YFFHDR.js";
|
|
30
|
+
import {
|
|
31
|
+
resolveGitMessageStyle
|
|
32
|
+
} from "./chunk-3REDAJUJ.js";
|
|
33
|
+
import {
|
|
34
|
+
beginManagedSync,
|
|
35
|
+
capCommitSubject,
|
|
36
|
+
codeCheckoutCarriesSharedBuckets,
|
|
37
|
+
publishManagedSync
|
|
38
|
+
} from "./chunk-TRURQUBP.js";
|
|
39
|
+
import {
|
|
40
|
+
countArchivedTasksForSprint,
|
|
41
|
+
getCurrentSprint,
|
|
42
|
+
listTasksInSprint,
|
|
43
|
+
refreshSprintMarkdown
|
|
44
|
+
} from "./chunk-L2CBG5IX.js";
|
|
45
|
+
import {
|
|
46
|
+
TaskNotFoundError,
|
|
47
|
+
applyTaskTransitionOutcome,
|
|
48
|
+
completeTask,
|
|
49
|
+
createTask,
|
|
50
|
+
deriveTransitionIdentity,
|
|
51
|
+
getTask,
|
|
52
|
+
restoreTask
|
|
53
|
+
} from "./chunk-7LHJDJB3.js";
|
|
54
|
+
import {
|
|
55
|
+
cleanupWorkflowRelatedBranchesAndWorktrees
|
|
56
|
+
} from "./chunk-ES6NWJ36.js";
|
|
57
|
+
import {
|
|
58
|
+
WorktreeManagerPresets
|
|
59
|
+
} from "./chunk-57WRCJJW.js";
|
|
60
|
+
import {
|
|
61
|
+
trackWorktreeCleaned
|
|
62
|
+
} from "./chunk-UJYDH2MW.js";
|
|
63
|
+
import {
|
|
64
|
+
listFlywheelBranches
|
|
65
|
+
} from "./chunk-75EMT7BF.js";
|
|
66
|
+
import {
|
|
67
|
+
getPlanningRoot,
|
|
68
|
+
withDeferredIndexRegeneration
|
|
69
|
+
} from "./chunk-CSXLLEGB.js";
|
|
70
|
+
import {
|
|
71
|
+
getBucketArchivePathByKind,
|
|
72
|
+
getBucketByName,
|
|
73
|
+
getBucketPathByKind,
|
|
74
|
+
getBuckets
|
|
75
|
+
} from "./chunk-V3KLVK7B.js";
|
|
76
|
+
import {
|
|
77
|
+
isSystemWritePath
|
|
78
|
+
} from "./chunk-XZOXEIN4.js";
|
|
79
|
+
import {
|
|
80
|
+
getArtifactDir,
|
|
81
|
+
getArtifactDirForRoot,
|
|
82
|
+
getManifestStatus,
|
|
83
|
+
getProjectRoot,
|
|
84
|
+
getResultPath,
|
|
85
|
+
getTrackedCleanupTargets,
|
|
86
|
+
loadManifest,
|
|
87
|
+
loadTriage,
|
|
88
|
+
updateManifestApplyStatus
|
|
89
|
+
} from "./chunk-6TRD7TEE.js";
|
|
90
|
+
import {
|
|
91
|
+
signalCoordinator
|
|
92
|
+
} from "./chunk-GC4CRMTS.js";
|
|
93
|
+
import {
|
|
94
|
+
pruneGitWorktreeEntries
|
|
95
|
+
} from "./chunk-33IGPPUI.js";
|
|
96
|
+
import {
|
|
97
|
+
isWorktreeRunSentinelPath
|
|
98
|
+
} from "./chunk-2NMNFUQA.js";
|
|
99
|
+
import {
|
|
100
|
+
canonicalizeVariantIdentifier
|
|
101
|
+
} from "./chunk-TGXXYDL4.js";
|
|
102
|
+
import {
|
|
103
|
+
execGitChecked
|
|
104
|
+
} from "./chunk-XHOI4V62.js";
|
|
105
|
+
import {
|
|
106
|
+
detectResolverUnavailableSignal,
|
|
107
|
+
detectUnavailableSignal
|
|
108
|
+
} from "./chunk-6TBEX4R4.js";
|
|
109
|
+
import {
|
|
110
|
+
createVerificationCommandEnv
|
|
111
|
+
} from "./chunk-S2E632FQ.js";
|
|
112
|
+
import {
|
|
113
|
+
isRecord
|
|
114
|
+
} from "./chunk-XJ2E7YEJ.js";
|
|
115
|
+
import {
|
|
116
|
+
wrapWithSandbox
|
|
117
|
+
} from "./chunk-B7YRSULE.js";
|
|
118
|
+
import {
|
|
119
|
+
CURRENT_BRANCH_REV_PARSE_ARGS,
|
|
120
|
+
assertBranchAllowsCodeApply,
|
|
121
|
+
classifyBranchRole,
|
|
122
|
+
isDisposableBranchRole,
|
|
123
|
+
parseCurrentBranchRef
|
|
124
|
+
} from "./chunk-XGU6PBO5.js";
|
|
125
|
+
import {
|
|
126
|
+
buildConflictBranchName
|
|
127
|
+
} from "./chunk-F55YABKV.js";
|
|
128
|
+
import {
|
|
129
|
+
readTrackedToolOutputRoots,
|
|
130
|
+
shouldExcludeStatusPath
|
|
131
|
+
} from "./chunk-5EQN56OZ.js";
|
|
132
|
+
import {
|
|
133
|
+
classifyRepoPath,
|
|
134
|
+
normalizeRepoPath
|
|
135
|
+
} from "./chunk-RBBTBCHX.js";
|
|
136
|
+
import {
|
|
137
|
+
buildProjectVerificationGuidance,
|
|
138
|
+
deriveDependencySetupCommandsForVerification,
|
|
139
|
+
detectMissingInstallForVerification,
|
|
140
|
+
detectProjectCapabilities,
|
|
141
|
+
isAllowedVerifyCommand,
|
|
142
|
+
isBaselineComparableTypeScriptCommand,
|
|
143
|
+
isBiomeAvailableForTarget,
|
|
144
|
+
partitionVerifyCommands,
|
|
145
|
+
selectFailureOutputTail,
|
|
146
|
+
targetSignalsVitest,
|
|
147
|
+
verificationCommandsCoverChangedPaths
|
|
148
|
+
} from "./chunk-AHU3FKPA.js";
|
|
149
|
+
import {
|
|
150
|
+
isProjectTestPath
|
|
151
|
+
} from "./chunk-RPIMAMV7.js";
|
|
152
|
+
import {
|
|
153
|
+
parseGitStatus
|
|
154
|
+
} from "./chunk-5KQETTNS.js";
|
|
155
|
+
import {
|
|
156
|
+
TelemetryPathResolver,
|
|
157
|
+
isLabModeFromEnv
|
|
158
|
+
} from "./chunk-C3KCMAJP.js";
|
|
159
|
+
import {
|
|
160
|
+
execGit
|
|
161
|
+
} from "./chunk-MHLEZQQI.js";
|
|
162
|
+
import {
|
|
163
|
+
normalizeReasoningEffortInput
|
|
164
|
+
} from "./chunk-4BTDUT2S.js";
|
|
165
|
+
import {
|
|
166
|
+
escapeShellArg
|
|
167
|
+
} from "./chunk-HGFXODKO.js";
|
|
168
|
+
import {
|
|
169
|
+
quotePosixShellArg
|
|
170
|
+
} from "./chunk-N5DX4JAK.js";
|
|
171
|
+
import {
|
|
172
|
+
isQuietOutputRequested
|
|
173
|
+
} from "./chunk-XWIZ6B5S.js";
|
|
174
|
+
import {
|
|
175
|
+
GIT_HOOK_COMMIT_TIMEOUT_MS
|
|
176
|
+
} from "./chunk-I56TH7VM.js";
|
|
177
|
+
import {
|
|
178
|
+
BUILD_AUTOMERGE_MODE,
|
|
179
|
+
BUILD_PRESERVE_WORKTREES,
|
|
180
|
+
emitCustomWarning,
|
|
181
|
+
emitWarning,
|
|
182
|
+
readEnv
|
|
183
|
+
} from "./chunk-7VRJHTGZ.js";
|
|
184
|
+
|
|
185
|
+
// src/planning/task-status-sync.ts
|
|
186
|
+
function emitTaskStatusSyncWarning(message, metadata) {
|
|
187
|
+
emitCustomWarning(
|
|
188
|
+
"WARN" /* WARN */,
|
|
189
|
+
"validation" /* VALIDATION */,
|
|
190
|
+
message,
|
|
191
|
+
{
|
|
192
|
+
taskId: "task-status-sync",
|
|
193
|
+
subsystem: "planning/task-status-sync",
|
|
194
|
+
executionPhase: "execution"
|
|
195
|
+
},
|
|
196
|
+
{ metadata, skipConsole: true }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
async function calculateSprintProgress(sprintId) {
|
|
200
|
+
try {
|
|
201
|
+
const tasks = await listTasksInSprint(sprintId);
|
|
202
|
+
const [archivedTotal, archivedDone] = await Promise.all([
|
|
203
|
+
countArchivedTasksForSprint(sprintId),
|
|
204
|
+
countArchivedTasksForSprint(sprintId, "done")
|
|
205
|
+
]);
|
|
206
|
+
const total = tasks.length + archivedTotal;
|
|
207
|
+
if (total === 0) return 0;
|
|
208
|
+
const completedTasks = tasks.filter((t) => t.status === "done");
|
|
209
|
+
return (completedTasks.length + archivedDone) / total * 100;
|
|
210
|
+
} catch {
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function formatSyncSummary(result) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
lines.push(`Tasks updated: ${result.tasksUpdated}`);
|
|
217
|
+
lines.push(`Tasks completed: ${result.tasksCompleted}`);
|
|
218
|
+
lines.push(`Tasks already satisfied: ${result.tasksAlreadySatisfied}`);
|
|
219
|
+
lines.push(`Tasks failed: ${result.tasksFailed}`);
|
|
220
|
+
lines.push(`Tasks ephemeral (skipped): ${result.tasksEphemeral}`);
|
|
221
|
+
lines.push(`Sprint progress: ${result.sprintProgress.toFixed(1)}%`);
|
|
222
|
+
const sections = [
|
|
223
|
+
["Skipped:", result.details.filter((d) => d.error && d.ephemeral)],
|
|
224
|
+
["Errors:", result.details.filter((d) => d.error && !d.ephemeral)]
|
|
225
|
+
];
|
|
226
|
+
for (const [heading, details] of sections) {
|
|
227
|
+
if (details.length === 0) continue;
|
|
228
|
+
lines.push("", heading);
|
|
229
|
+
for (const detail of details) {
|
|
230
|
+
lines.push(` ${detail.taskId}: ${detail.error}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|
|
235
|
+
async function applyTaskTransitions(sprintId, transitions, options) {
|
|
236
|
+
const details = [];
|
|
237
|
+
let tasksUpdated = 0;
|
|
238
|
+
let tasksCompleted = 0;
|
|
239
|
+
let tasksAlreadySatisfied = 0;
|
|
240
|
+
let tasksFailed = 0;
|
|
241
|
+
let tasksEphemeral = 0;
|
|
242
|
+
const statusPriority = {
|
|
243
|
+
done: 4,
|
|
244
|
+
review: 3,
|
|
245
|
+
in_progress: 2,
|
|
246
|
+
blocked: 1,
|
|
247
|
+
todo: 0
|
|
248
|
+
};
|
|
249
|
+
function mergeTransitionGroup(group, winner) {
|
|
250
|
+
const resultArtifactPath = winner.resultArtifactPath ?? group.find((t) => t.resultArtifactPath)?.resultArtifactPath;
|
|
251
|
+
const reason = winner.reason ?? group.find((t) => t.reason)?.reason;
|
|
252
|
+
return {
|
|
253
|
+
taskId: winner.taskId,
|
|
254
|
+
targetStatus: winner.targetStatus,
|
|
255
|
+
...resultArtifactPath ? { resultArtifactPath } : {},
|
|
256
|
+
...reason ? { reason } : {}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const deduplicatedTransitions = [];
|
|
260
|
+
const byTaskId = /* @__PURE__ */ new Map();
|
|
261
|
+
for (const t of transitions) {
|
|
262
|
+
const existing = byTaskId.get(t.taskId);
|
|
263
|
+
if (existing) {
|
|
264
|
+
existing.push(t);
|
|
265
|
+
} else {
|
|
266
|
+
byTaskId.set(t.taskId, [t]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const [, group] of byTaskId) {
|
|
270
|
+
if (group.length === 1) {
|
|
271
|
+
deduplicatedTransitions.push({
|
|
272
|
+
merged: group[0],
|
|
273
|
+
constituents: group
|
|
274
|
+
});
|
|
275
|
+
} else {
|
|
276
|
+
const allSame = group.every((t) => t.targetStatus === group[0]?.targetStatus);
|
|
277
|
+
if (allSame) {
|
|
278
|
+
const winner = group[0];
|
|
279
|
+
deduplicatedTransitions.push({
|
|
280
|
+
merged: mergeTransitionGroup(group, winner),
|
|
281
|
+
constituents: group
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
const winner = group.reduce((best, t) => {
|
|
285
|
+
const bp = statusPriority[best.targetStatus] ?? 0;
|
|
286
|
+
const tp = statusPriority[t.targetStatus] ?? 0;
|
|
287
|
+
return tp > bp ? t : best;
|
|
288
|
+
});
|
|
289
|
+
const statuses = group.map((t) => t.targetStatus).join(", ");
|
|
290
|
+
emitTaskStatusSyncWarning(
|
|
291
|
+
`[task-status-sync] Conflicting transitions for task ${winner.taskId}: [${statuses}] - keeping "${winner.targetStatus}"`,
|
|
292
|
+
{
|
|
293
|
+
taskId: winner.taskId,
|
|
294
|
+
statuses: group.map((t) => t.targetStatus),
|
|
295
|
+
selectedStatus: winner.targetStatus,
|
|
296
|
+
operation: "deduplicate-transitions"
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
deduplicatedTransitions.push({
|
|
300
|
+
merged: mergeTransitionGroup(group, winner),
|
|
301
|
+
constituents: group
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
let sprintTaskIds = /* @__PURE__ */ new Set();
|
|
307
|
+
let sprintQueryable = true;
|
|
308
|
+
try {
|
|
309
|
+
const sprintTasks = await listTasksInSprint(sprintId);
|
|
310
|
+
sprintTaskIds = new Set(sprintTasks.map((task) => task.id));
|
|
311
|
+
} catch {
|
|
312
|
+
sprintQueryable = false;
|
|
313
|
+
}
|
|
314
|
+
const effectiveStatusOf = (targetStatus) => options?.promoteToDone && targetStatus === "review" ? "done" : targetStatus;
|
|
315
|
+
const routeDoneThroughCompleteTask = (effectiveTargetStatus) => options?.promoteToDone === true && effectiveTargetStatus === "done";
|
|
316
|
+
const ancillaryTargetStatus = (transition, task, effectiveTargetStatus) => {
|
|
317
|
+
if (!routeDoneThroughCompleteTask(effectiveTargetStatus)) {
|
|
318
|
+
return effectiveTargetStatus;
|
|
319
|
+
}
|
|
320
|
+
if (transition.targetStatus === "review") {
|
|
321
|
+
return "review";
|
|
322
|
+
}
|
|
323
|
+
return task.status;
|
|
324
|
+
};
|
|
325
|
+
const pendingCompletions = [];
|
|
326
|
+
let sprintProgress = await withDeferredIndexRegeneration(getPlanningRoot(), async () => {
|
|
327
|
+
for (const prepared of deduplicatedTransitions) {
|
|
328
|
+
const transition = prepared.merged;
|
|
329
|
+
const detail = {
|
|
330
|
+
taskId: transition.taskId,
|
|
331
|
+
previousStatus: "todo",
|
|
332
|
+
updated: false
|
|
333
|
+
};
|
|
334
|
+
try {
|
|
335
|
+
if (!sprintQueryable) {
|
|
336
|
+
detail.error = `Sprint artifact not queryable for ${sprintId}; transition skipped as ephemeral.`;
|
|
337
|
+
detail.ephemeral = true;
|
|
338
|
+
tasksEphemeral++;
|
|
339
|
+
details.push(detail);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!sprintTaskIds.has(transition.taskId)) {
|
|
343
|
+
if (options?.promoteToDone) {
|
|
344
|
+
try {
|
|
345
|
+
const archivedLookup = await getTask(transition.taskId, { includeArchive: true });
|
|
346
|
+
if (archivedLookup.isArchived) {
|
|
347
|
+
detail.previousStatus = archivedLookup.yaml.status;
|
|
348
|
+
detail.newStatus = "done";
|
|
349
|
+
tasksAlreadySatisfied++;
|
|
350
|
+
details.push(detail);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
detail.error = `Task ${transition.taskId} is not linked to sprint ${sprintId}; transition skipped as ephemeral.`;
|
|
357
|
+
detail.ephemeral = true;
|
|
358
|
+
tasksEphemeral++;
|
|
359
|
+
details.push(detail);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const { yaml: task, isArchived } = await getTask(transition.taskId, {
|
|
363
|
+
includeArchive: true
|
|
364
|
+
});
|
|
365
|
+
detail.previousStatus = task.status;
|
|
366
|
+
const effectiveTargetStatus = effectiveStatusOf(transition.targetStatus);
|
|
367
|
+
const finalizeViaCompleteTask = routeDoneThroughCompleteTask(effectiveTargetStatus);
|
|
368
|
+
if (finalizeViaCompleteTask && isArchived) {
|
|
369
|
+
detail.newStatus = "done";
|
|
370
|
+
tasksAlreadySatisfied++;
|
|
371
|
+
details.push(detail);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const outcomeTargetStatus = ancillaryTargetStatus(transition, task, effectiveTargetStatus);
|
|
375
|
+
const constituents = prepared.constituents.map((c) => ({
|
|
376
|
+
identity: deriveTransitionIdentity({
|
|
377
|
+
taskId: c.taskId,
|
|
378
|
+
targetStatus: effectiveStatusOf(c.targetStatus),
|
|
379
|
+
hoursLogged: c.hoursLogged,
|
|
380
|
+
resultArtifactPath: c.resultArtifactPath,
|
|
381
|
+
transitionId: c.transitionId
|
|
382
|
+
}),
|
|
383
|
+
hoursLogged: c.hoursLogged
|
|
384
|
+
}));
|
|
385
|
+
const outcome = await applyTaskTransitionOutcome(transition.taskId, {
|
|
386
|
+
targetStatus: outcomeTargetStatus,
|
|
387
|
+
resultArtifactPath: transition.resultArtifactPath,
|
|
388
|
+
reason: transition.reason,
|
|
389
|
+
constituents
|
|
390
|
+
});
|
|
391
|
+
if (outcome.hoursApplied > 0) {
|
|
392
|
+
detail.hoursLogged = outcome.hoursApplied;
|
|
393
|
+
}
|
|
394
|
+
if (finalizeViaCompleteTask) {
|
|
395
|
+
pendingCompletions.push({
|
|
396
|
+
taskId: transition.taskId,
|
|
397
|
+
detail,
|
|
398
|
+
previousStatus: task.status,
|
|
399
|
+
ancillaryFileUpdated: outcome.fileUpdated
|
|
400
|
+
});
|
|
401
|
+
details.push(detail);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
detail.newStatus = effectiveTargetStatus;
|
|
405
|
+
if (outcome.unverifiedLabel) detail.unverifiedLabel = outcome.unverifiedLabel;
|
|
406
|
+
if (outcome.statusChanged) {
|
|
407
|
+
detail.updated = true;
|
|
408
|
+
tasksUpdated++;
|
|
409
|
+
if (effectiveTargetStatus === "done") {
|
|
410
|
+
tasksCompleted++;
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
detail.fileUpdated = outcome.fileUpdated;
|
|
414
|
+
tasksAlreadySatisfied++;
|
|
415
|
+
}
|
|
416
|
+
details.push(detail);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
419
|
+
const missingTask = error instanceof TaskNotFoundError && error.taskId === transition.taskId || error?.code === "ENOENT";
|
|
420
|
+
if (missingTask) {
|
|
421
|
+
detail.error = `Task artifact not queryable for ${transition.taskId}; transition skipped as ephemeral.`;
|
|
422
|
+
detail.ephemeral = true;
|
|
423
|
+
tasksEphemeral++;
|
|
424
|
+
} else {
|
|
425
|
+
detail.error = errorMsg;
|
|
426
|
+
tasksFailed++;
|
|
427
|
+
}
|
|
428
|
+
details.push(detail);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const progress = await calculateSprintProgress(sprintId);
|
|
432
|
+
try {
|
|
433
|
+
await refreshSprintMarkdown(sprintId);
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
return progress;
|
|
437
|
+
});
|
|
438
|
+
const planningRoot = getPlanningRoot();
|
|
439
|
+
let completionsSucceeded = 0;
|
|
440
|
+
for (const pending of pendingCompletions) {
|
|
441
|
+
try {
|
|
442
|
+
const completion = await completeTask(pending.taskId, {
|
|
443
|
+
completionContractRoot: planningRoot
|
|
444
|
+
});
|
|
445
|
+
completionsSucceeded++;
|
|
446
|
+
pending.detail.previousStatus = pending.previousStatus;
|
|
447
|
+
pending.detail.newStatus = "done";
|
|
448
|
+
pending.detail.updated = true;
|
|
449
|
+
pending.detail.archivedPath = completion.archivedPath;
|
|
450
|
+
if (completion.unverifiedLabel) pending.detail.unverifiedLabel = completion.unverifiedLabel;
|
|
451
|
+
if (completion.unblockedTaskIds.length > 0) {
|
|
452
|
+
pending.detail.unblockedTaskIds = [...completion.unblockedTaskIds];
|
|
453
|
+
}
|
|
454
|
+
tasksUpdated++;
|
|
455
|
+
tasksCompleted++;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
458
|
+
pending.detail.error = errorMsg;
|
|
459
|
+
pending.detail.updated = false;
|
|
460
|
+
pending.detail.newStatus = "review";
|
|
461
|
+
if (pending.ancillaryFileUpdated) {
|
|
462
|
+
pending.detail.fileUpdated = true;
|
|
463
|
+
}
|
|
464
|
+
tasksFailed++;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (completionsSucceeded > 0) {
|
|
468
|
+
sprintProgress = await calculateSprintProgress(sprintId);
|
|
469
|
+
try {
|
|
470
|
+
await refreshSprintMarkdown(sprintId);
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
tasksUpdated,
|
|
476
|
+
tasksCompleted,
|
|
477
|
+
tasksAlreadySatisfied,
|
|
478
|
+
tasksFailed,
|
|
479
|
+
tasksEphemeral,
|
|
480
|
+
sprintProgress,
|
|
481
|
+
details
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/automation/flywheel-merge.ts
|
|
486
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
487
|
+
import { existsSync } from "fs";
|
|
488
|
+
import * as fs4 from "fs/promises";
|
|
489
|
+
import * as os from "os";
|
|
490
|
+
import * as path4 from "path";
|
|
491
|
+
import chalk4 from "chalk";
|
|
492
|
+
import { execa as execa2 } from "execa";
|
|
493
|
+
import yaml from "js-yaml";
|
|
494
|
+
|
|
495
|
+
// src/core/git/github-host-probe.ts
|
|
496
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
497
|
+
function isNotGithubHostMessage(text) {
|
|
498
|
+
return /point to a known github host/i.test(text);
|
|
499
|
+
}
|
|
500
|
+
async function probeGithubHost(projectRoot, runner) {
|
|
501
|
+
try {
|
|
502
|
+
await runner("gh", ["repo", "view", "--json", "nameWithOwner"], {
|
|
503
|
+
cwd: projectRoot,
|
|
504
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
505
|
+
env: { GH_PROMPT_DISABLED: "1" }
|
|
506
|
+
});
|
|
507
|
+
return "github";
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const failure = error;
|
|
510
|
+
const text = [failure?.stderr, failure?.message].filter((part) => typeof part === "string").join("\n");
|
|
511
|
+
return isNotGithubHostMessage(text) ? "not-github" : "unknown";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/core/project/merge-normalization.ts
|
|
516
|
+
import path from "path";
|
|
517
|
+
var BIOME_SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
518
|
+
".js",
|
|
519
|
+
".cjs",
|
|
520
|
+
".mjs",
|
|
521
|
+
".jsx",
|
|
522
|
+
".ts",
|
|
523
|
+
".cts",
|
|
524
|
+
".mts",
|
|
525
|
+
".tsx",
|
|
526
|
+
".json",
|
|
527
|
+
".jsonc",
|
|
528
|
+
".css",
|
|
529
|
+
".graphql",
|
|
530
|
+
".gql"
|
|
531
|
+
]);
|
|
532
|
+
function filterBiomePaths(files) {
|
|
533
|
+
return files.filter((file) => BIOME_SUPPORTED_EXTENSIONS.has(path.extname(file).toLowerCase()));
|
|
534
|
+
}
|
|
535
|
+
function resolveMergeNormalization(root, stagedFiles) {
|
|
536
|
+
const available = isBiomeAvailableForTarget(root);
|
|
537
|
+
const files = filterBiomePaths(stagedFiles);
|
|
538
|
+
if (!available) {
|
|
539
|
+
return {
|
|
540
|
+
kind: "unavailable",
|
|
541
|
+
message: files.length > 0 ? ` Biome merge gate unavailable for this target (${files.length} Biome-lintable file(s) staged but the resolver does not declare Biome available); lint/format is verified by the resolver-derived commands.` : " Biome merge gate unavailable for this target language (no Node lint surface); lint/format is verified by the resolver-derived commands."
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (files.length === 0) return { kind: "not-required" };
|
|
545
|
+
return {
|
|
546
|
+
kind: "available",
|
|
547
|
+
label: "Biome",
|
|
548
|
+
files,
|
|
549
|
+
safeWrite: { command: "npx", args: ["--no-install", "biome", "check", "--write", ...files] },
|
|
550
|
+
check: {
|
|
551
|
+
command: "npx",
|
|
552
|
+
args: ["--no-install", "biome", "check", "--max-diagnostics", "50", ...files]
|
|
553
|
+
},
|
|
554
|
+
// Unsafe fixes remain manual guidance, never an automatic operation.
|
|
555
|
+
manualFixCommand: `npx --no-install biome check --write --unsafe ${files.slice(0, 8).join(" ")}` + (files.length > 8 ? " ..." : ""),
|
|
556
|
+
manualFixCommitSubject: "chore: satisfy biome"
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/automation/flywheel-contract-drift.ts
|
|
561
|
+
var CROSS_SUBSTAGE_CONTRACT_DRIFT = "cross-substage contract drift";
|
|
562
|
+
var DRIFT_PATTERNS = [
|
|
563
|
+
/expected\s+\{[^}]*\}\s+to\s+have\s+property\s+['"]length['"]/i,
|
|
564
|
+
/\w+\s*\.\s*map\s+is\s+not\s+a\s+function/i,
|
|
565
|
+
/\w+\s*\.\s*filter\s+is\s+not\s+a\s+function/i,
|
|
566
|
+
/\w+\s*\.\s*forEach\s+is\s+not\s+a\s+function/i,
|
|
567
|
+
/\w+\s*\.\s*reduce\s+is\s+not\s+a\s+function/i,
|
|
568
|
+
/\w+\s*\.\s*find\s+is\s+not\s+a\s+function/i,
|
|
569
|
+
/is\s+not\s+iterable/i,
|
|
570
|
+
/Cannot\s+read\s+propert(?:y|ies)\s+of\s+undefined.*\.\s*(?:length|map|filter|find|reduce)/i,
|
|
571
|
+
/TypeError.*\.\s*(?:map|filter|forEach|find|reduce)\s+is\s+not\s+a\s+function/i
|
|
572
|
+
];
|
|
573
|
+
var SYMBOL_EXTRACTION_PATTERN = /(\b\w+(?:\.\w+)*)\s*\.\s*(?:map|filter|forEach|find|reduce|length)\b/gi;
|
|
574
|
+
function detectContractDrift(output) {
|
|
575
|
+
if (!output || typeof output !== "string") {
|
|
576
|
+
return { isDrift: false, symbols: [], matchedPatterns: [] };
|
|
577
|
+
}
|
|
578
|
+
const matchedPatterns = [];
|
|
579
|
+
for (const pattern of DRIFT_PATTERNS) {
|
|
580
|
+
if (pattern.test(output)) {
|
|
581
|
+
matchedPatterns.push(pattern.source);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (matchedPatterns.length === 0) {
|
|
585
|
+
return { isDrift: false, symbols: [], matchedPatterns: [] };
|
|
586
|
+
}
|
|
587
|
+
const symbolSet = /* @__PURE__ */ new Set();
|
|
588
|
+
let match;
|
|
589
|
+
const extractor = new RegExp(SYMBOL_EXTRACTION_PATTERN.source, "gi");
|
|
590
|
+
match = extractor.exec(output);
|
|
591
|
+
while (match !== null) {
|
|
592
|
+
const sym = match[1];
|
|
593
|
+
if (sym && sym.length > 0 && sym !== "a" && sym !== "is") {
|
|
594
|
+
symbolSet.add(sym);
|
|
595
|
+
}
|
|
596
|
+
match = extractor.exec(output);
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
isDrift: true,
|
|
600
|
+
symbols: [...symbolSet],
|
|
601
|
+
matchedPatterns
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function formatContractDriftMessage(detection, failedCommand) {
|
|
605
|
+
if (!detection.isDrift) {
|
|
606
|
+
return "";
|
|
607
|
+
}
|
|
608
|
+
const symbolDesc = detection.symbols.length > 0 ? `Symbols involved: ${detection.symbols.slice(0, 5).join(", ")}.` : "Symbol name could not be extracted from output.";
|
|
609
|
+
return `Failure classification: ${CROSS_SUBSTAGE_CONTRACT_DRIFT}. A sibling substage generated tests asserting an incompatible return shape for a shared symbol. ${symbolDesc} Failed command: ${failedCommand}.`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/automation/flywheel-merge-apply.ts
|
|
613
|
+
import chalk from "chalk";
|
|
614
|
+
import { execa } from "execa";
|
|
615
|
+
var SquashMergeConflictError = class extends Error {
|
|
616
|
+
conflictFiles;
|
|
617
|
+
constructor(message, conflictFiles) {
|
|
618
|
+
super(message);
|
|
619
|
+
this.name = "SquashMergeConflictError";
|
|
620
|
+
this.conflictFiles = conflictFiles;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
function formatPathList(paths, max = 8) {
|
|
624
|
+
const shown = paths.slice(0, max);
|
|
625
|
+
const suffix = paths.length > shown.length ? ` ... (+${paths.length - shown.length} more)` : "";
|
|
626
|
+
return shown.join(", ") + suffix;
|
|
627
|
+
}
|
|
628
|
+
async function squashMergeRunBranch(params) {
|
|
629
|
+
const { cwd, integrationBranch } = params;
|
|
630
|
+
console.log(chalk.cyan(" Squash-merging run branch..."));
|
|
631
|
+
const mergeResult = await execa("git", ["merge", "--squash", integrationBranch], {
|
|
632
|
+
cwd,
|
|
633
|
+
stdio: "pipe",
|
|
634
|
+
timeout: 6e4,
|
|
635
|
+
reject: false
|
|
636
|
+
});
|
|
637
|
+
if (mergeResult.timedOut) {
|
|
638
|
+
throw new Error("Squash merge timed out after 60000ms");
|
|
639
|
+
}
|
|
640
|
+
if (mergeResult.exitCode !== 0) {
|
|
641
|
+
if (mergeResult.exitCode === 128) {
|
|
642
|
+
const fatalMsg = (mergeResult.stderr ?? "").trim() || "Fatal git error during squash merge";
|
|
643
|
+
throw new Error(`Fatal git error: ${fatalMsg}`);
|
|
644
|
+
}
|
|
645
|
+
if (mergeResult.exitCode !== 1) {
|
|
646
|
+
const genericMsg = (mergeResult.stderr ?? "").trim() || `Squash merge failed with exit code ${mergeResult.exitCode}`;
|
|
647
|
+
throw new Error(`Squash merge failed: ${genericMsg}`);
|
|
648
|
+
}
|
|
649
|
+
const { stdout: conflictStdout } = await execa(
|
|
650
|
+
"git",
|
|
651
|
+
["diff", "--name-only", "--diff-filter=U"],
|
|
652
|
+
{
|
|
653
|
+
cwd,
|
|
654
|
+
stdio: "pipe",
|
|
655
|
+
timeout: 3e4,
|
|
656
|
+
reject: false
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
const conflicts = conflictStdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
660
|
+
const msg = `Squash-merge failed due to conflicts (${conflicts.length}).
|
|
661
|
+
Conflicts: ${conflicts.length > 0 ? formatPathList(conflicts) : "(unknown)"}`;
|
|
662
|
+
throw new SquashMergeConflictError(msg, conflicts);
|
|
663
|
+
}
|
|
664
|
+
if (mergeResult.stdout) process.stdout.write(mergeResult.stdout);
|
|
665
|
+
if (mergeResult.stderr) process.stderr.write(mergeResult.stderr);
|
|
666
|
+
}
|
|
667
|
+
var PlanningSnapshotError = class extends Error {
|
|
668
|
+
constructor(message) {
|
|
669
|
+
super(message);
|
|
670
|
+
this.name = "PlanningSnapshotError";
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
async function snapshotDirtyPlanningPaths(params) {
|
|
674
|
+
const { cwd, allowlistPrefixes } = params;
|
|
675
|
+
const status = await execa(
|
|
676
|
+
"git",
|
|
677
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...allowlistPrefixes],
|
|
678
|
+
{ cwd, stdio: "pipe", timeout: 3e4, reject: false }
|
|
679
|
+
);
|
|
680
|
+
if (status.timedOut || status.failed === true || status.exitCode !== void 0 && status.exitCode !== 0) {
|
|
681
|
+
throw new PlanningSnapshotError(
|
|
682
|
+
`git status failed while snapshotting planning paths (exit ${status.exitCode ?? "none"}): ${String(status.stderr ?? "").trim()}`
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
686
|
+
const fields = status.stdout.split("\0");
|
|
687
|
+
for (let i = 0; i < fields.length; i++) {
|
|
688
|
+
const entry = fields[i];
|
|
689
|
+
if (entry === void 0 || entry.length < 4) continue;
|
|
690
|
+
const x = entry[0];
|
|
691
|
+
const y = entry[1];
|
|
692
|
+
dirty.add(entry.slice(3));
|
|
693
|
+
if (x === "R" || x === "C" || y === "R" || y === "C") {
|
|
694
|
+
const source = fields[i + 1];
|
|
695
|
+
if (source !== void 0 && source.length > 0) {
|
|
696
|
+
dirty.add(source);
|
|
697
|
+
}
|
|
698
|
+
i++;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return dirty;
|
|
702
|
+
}
|
|
703
|
+
async function snapshotStageablePlanningPaths(params) {
|
|
704
|
+
const status = await execGitChecked(
|
|
705
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...params.allowlistPrefixes],
|
|
706
|
+
{ cwd: params.cwd, timeoutMs: 3e4 }
|
|
707
|
+
);
|
|
708
|
+
return new Set(selectStageablePaths(status.stdout));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/automation/flywheel-merge-commit.ts
|
|
712
|
+
var FlywheelCommitError = class extends Error {
|
|
713
|
+
phase;
|
|
714
|
+
constructor(phase, message) {
|
|
715
|
+
super(message);
|
|
716
|
+
this.name = "FlywheelCommitError";
|
|
717
|
+
this.phase = phase;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
var OUTPUT_TAIL_LINES = 40;
|
|
721
|
+
function indentedTail(output) {
|
|
722
|
+
return output.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-OUTPUT_TAIL_LINES).map((line) => ` ${line}`).join("\n");
|
|
723
|
+
}
|
|
724
|
+
function describeFailure(error) {
|
|
725
|
+
const failure = error ?? {};
|
|
726
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
727
|
+
const reason = failure.timedOut ? "timed out; a commit hook may be hanging" : typeof failure.exitCode === "number" ? `exit code ${failure.exitCode}` : (text(failure.message).split("\n")[0] ?? "") || "could not run";
|
|
728
|
+
return { reason, output: [text(failure.stderr), text(failure.stdout)].join("\n").trim() };
|
|
729
|
+
}
|
|
730
|
+
function formatFlywheelCommitRecovery(params) {
|
|
731
|
+
const rerun = `rerun \`build run flywheel merge-result ${params.workflowId} --force\`. --force retries this candidate; it does not skip verification or your commit hooks.`;
|
|
732
|
+
if (!params.staged) return `Resolve the git error above, then ${rerun}`;
|
|
733
|
+
const restore = params.restoreSha ? `restore the checkout to ${params.restoreSha.slice(0, 12)}` : "restore the checkout to its pre-merge state";
|
|
734
|
+
return `The merged changes remain staged at ${params.cwd}. Resolve the git error above, ${restore} (keep any edits you still need), then ${rerun}`;
|
|
735
|
+
}
|
|
736
|
+
async function runFlywheelCommit(params) {
|
|
737
|
+
try {
|
|
738
|
+
await params.run();
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const { reason, output } = describeFailure(error);
|
|
741
|
+
const label = params.phase === "code" ? "merge-result commit" : "task/sprint metadata commit";
|
|
742
|
+
const lines = [`Commit failed: git did not create the ${label} (${reason}).`];
|
|
743
|
+
if (params.quiet && output) lines.push("git output:", indentedTail(output));
|
|
744
|
+
if (params.priorCommit) {
|
|
745
|
+
lines.push(
|
|
746
|
+
`The changes for the ${label} remain staged at ${params.cwd}, and the earlier commit is kept.`
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (params.style !== "conventional") {
|
|
750
|
+
lines.push(
|
|
751
|
+
"If the repository enforces Conventional Commits, set git.messageStyle to conventional (build config set git.messageStyle conventional)."
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
throw new FlywheelCommitError(params.phase, lines.join("\n"));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
var MAX_LISTED_PATHS = 10;
|
|
758
|
+
async function readStagedTree(cwd) {
|
|
759
|
+
const result = await execGit(["write-tree"], { cwd, timeoutMs: 3e4 });
|
|
760
|
+
const tree = result.stdout.trim();
|
|
761
|
+
return result.exitCode === 0 && /^[0-9a-f]{40,64}$/u.test(tree) ? tree : void 0;
|
|
762
|
+
}
|
|
763
|
+
async function describeCommitChangedTree(params) {
|
|
764
|
+
const changed = /* @__PURE__ */ new Set();
|
|
765
|
+
for (const commit of params.commits) {
|
|
766
|
+
const diff = await execGit(
|
|
767
|
+
["diff", "--name-only", "-z", "--no-renames", commit.preCommitTree, `${commit.sha}^{tree}`],
|
|
768
|
+
{ cwd: params.cwd, timeoutMs: 3e4 }
|
|
769
|
+
);
|
|
770
|
+
if (diff.exitCode !== 0) continue;
|
|
771
|
+
for (const entry of diff.stdout.split("\0")) {
|
|
772
|
+
if (entry) changed.add(entry);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const head = params.commits.at(-1);
|
|
776
|
+
if (changed.size === 0 || !head) return void 0;
|
|
777
|
+
const paths = [...changed];
|
|
778
|
+
const listed = paths.slice(0, MAX_LISTED_PATHS).join(", ");
|
|
779
|
+
const more = paths.length > MAX_LISTED_PATHS ? ` and ${paths.length - MAX_LISTED_PATHS} more` : "";
|
|
780
|
+
const onBranch = params.branch ? ` of branch ${params.branch}` : "";
|
|
781
|
+
const discard = params.restoreSha ? `, or restore the branch to ${params.restoreSha.slice(0, 12)} to discard it` : "";
|
|
782
|
+
return `${params.headline ?? "Committed HEAD tree does not match the final verified tree."} These files changed during the commit; a hook may have changed them: ${listed}${more}. Commit ${head.sha} is HEAD${onBranch}. Workflow ${params.workflowId} is not marked applied, and Build did not re-verify the changed tree. Review the commit and keep it${discard}.`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/automation/flywheel-merge-commit-message.ts
|
|
786
|
+
var MERGE_SUBJECT = {
|
|
787
|
+
plain: "Merge Build workflow result",
|
|
788
|
+
conventional: "chore(build): merge workflow result"
|
|
789
|
+
};
|
|
790
|
+
var METADATA_SUBJECT = {
|
|
791
|
+
plain: "Sync task and sprint metadata",
|
|
792
|
+
conventional: "chore(build): sync task and sprint metadata"
|
|
793
|
+
};
|
|
794
|
+
var LEGACY_METADATA_SUBJECT = "chore: sync task/sprint metadata [flywheel]";
|
|
795
|
+
function flywheelMergeCommitSubject(style) {
|
|
796
|
+
return capCommitSubject(MERGE_SUBJECT[style]);
|
|
797
|
+
}
|
|
798
|
+
function flywheelMetadataCommitSubject(style) {
|
|
799
|
+
return capCommitSubject(METADATA_SUBJECT[style]);
|
|
800
|
+
}
|
|
801
|
+
function legacyFlywheelMergeCommitSubject(workflowId) {
|
|
802
|
+
return `[Build Flywheel] Merge workflow result ${workflowId}`;
|
|
803
|
+
}
|
|
804
|
+
function isFlywheelMergeCommitSubject(subject, workflowId) {
|
|
805
|
+
return subject === legacyFlywheelMergeCommitSubject(workflowId) || subject === flywheelMergeCommitSubject("plain") || subject === flywheelMergeCommitSubject("conventional");
|
|
806
|
+
}
|
|
807
|
+
function isFlywheelMetadataCommitSubject(subject) {
|
|
808
|
+
return subject === LEGACY_METADATA_SUBJECT || subject === flywheelMetadataCommitSubject("plain") || subject === flywheelMetadataCommitSubject("conventional");
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/automation/flywheel-merge-failed-run.ts
|
|
812
|
+
import fs from "fs/promises";
|
|
813
|
+
async function readRecordedFailure(resultPath) {
|
|
814
|
+
let parsed;
|
|
815
|
+
try {
|
|
816
|
+
parsed = JSON.parse(await fs.readFile(resultPath, "utf-8"));
|
|
817
|
+
} catch {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
821
|
+
const result = parsed;
|
|
822
|
+
if (result.success === false) return "recorded a failed outcome";
|
|
823
|
+
if (result.materialized === false) return "did not materialize its result";
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
async function assertFailedRunHasCodeToApply(gate, resultPath) {
|
|
827
|
+
const reason = gate?.passed === false && gate.blockingStageIds.length > 0 ? `failed its gate (${gate.reason})` : await readRecordedFailure(resultPath);
|
|
828
|
+
if (!reason) return;
|
|
829
|
+
throw new Error(`Workflow ${reason} and produced no code to commit; nothing was applied.`);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/automation/flywheel-merge-outcomes.ts
|
|
833
|
+
import fs2 from "fs/promises";
|
|
834
|
+
import path2 from "path";
|
|
835
|
+
function isRecord2(value) {
|
|
836
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
837
|
+
}
|
|
838
|
+
function nonEmpty(value) {
|
|
839
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
840
|
+
}
|
|
841
|
+
function listAppliedCompetitionStages(result) {
|
|
842
|
+
if (!isRecord2(result) || !Array.isArray(result.stageResults)) return [];
|
|
843
|
+
const stages = [];
|
|
844
|
+
for (const stage of result.stageResults) {
|
|
845
|
+
if (!isRecord2(stage) || stage.stageType !== "competition") continue;
|
|
846
|
+
if (stage.winnerApplied !== true || stage.winnerNoOp === true) continue;
|
|
847
|
+
const taskId = nonEmpty(stage.competitionTaskId);
|
|
848
|
+
const winner = isRecord2(stage.winner) ? stage.winner : void 0;
|
|
849
|
+
const spec = winner && isRecord2(winner.variant) ? winner.variant : void 0;
|
|
850
|
+
const provider = nonEmpty(spec?.provider);
|
|
851
|
+
if (!taskId || !provider) continue;
|
|
852
|
+
const model = nonEmpty(spec?.model);
|
|
853
|
+
const effort = normalizeReasoningEffortInput(spec?.reasoning);
|
|
854
|
+
const effortSegment = effort ? `:effort-${effort}` : "";
|
|
855
|
+
const winnerId = canonicalizeVariantIdentifier({
|
|
856
|
+
variant: `${provider}:${model ?? "default"}${effortSegment}`,
|
|
857
|
+
provider,
|
|
858
|
+
...model ? { model } : {},
|
|
859
|
+
executionMode: "local"
|
|
860
|
+
});
|
|
861
|
+
stages.push({ stageId: nonEmpty(stage.stageId) ?? "(unknown)", taskId, winnerId });
|
|
862
|
+
}
|
|
863
|
+
return stages;
|
|
864
|
+
}
|
|
865
|
+
function recordAppliedStageOutcomes(db, input) {
|
|
866
|
+
let recorded = 0;
|
|
867
|
+
for (const stage of listAppliedCompetitionStages(input.result)) {
|
|
868
|
+
const evaluationId = resolveLatestEvaluationId(db, input.projectId, stage.taskId);
|
|
869
|
+
if (!evaluationId) continue;
|
|
870
|
+
const event = buildCompetitionOutcomeEvent({
|
|
871
|
+
eventType: "completion_applied",
|
|
872
|
+
competitionId: stage.taskId,
|
|
873
|
+
taskId: stage.taskId,
|
|
874
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
875
|
+
finalJudgeWinner: { canonicalKey: stage.winnerId },
|
|
876
|
+
appliedArtifact: {
|
|
877
|
+
kind: "variant",
|
|
878
|
+
identity: stage.winnerId,
|
|
879
|
+
variantId: stage.winnerId,
|
|
880
|
+
...input.targetBranch ? { branch: input.targetBranch } : {},
|
|
881
|
+
...input.commitSha ? { commitSha: input.commitSha } : {}
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
const inserted = recordCompetitionOutcomeEvent(db, {
|
|
885
|
+
projectId: input.projectId,
|
|
886
|
+
evaluationId,
|
|
887
|
+
outcomeKind: "applied",
|
|
888
|
+
event,
|
|
889
|
+
commitSha: input.commitSha ?? null,
|
|
890
|
+
metadata: {
|
|
891
|
+
mode: "flywheel-merge-result",
|
|
892
|
+
workflowId: input.workflowId,
|
|
893
|
+
stageId: stage.stageId,
|
|
894
|
+
appliedVariant: stage.winnerId,
|
|
895
|
+
...input.targetBranch ? { targetBranch: input.targetBranch } : {}
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
if (inserted) recorded += 1;
|
|
899
|
+
}
|
|
900
|
+
return recorded;
|
|
901
|
+
}
|
|
902
|
+
async function recordWorkflowCompetitionOutcomes(input) {
|
|
903
|
+
try {
|
|
904
|
+
const result = JSON.parse(await fs2.readFile(getResultPath(input.workflowId), "utf-8"));
|
|
905
|
+
if (listAppliedCompetitionStages(result).length === 0) return;
|
|
906
|
+
const projectId = TelemetryPathResolver.getInstance().getProjectId(
|
|
907
|
+
path2.join(getProjectRoot(), ".build-project"),
|
|
908
|
+
{ register: false }
|
|
909
|
+
);
|
|
910
|
+
const { db } = openConfiguredTelemetryDb({ writable: true });
|
|
911
|
+
if (!db) return;
|
|
912
|
+
try {
|
|
913
|
+
recordAppliedStageOutcomes(db, { ...input, projectId, result });
|
|
914
|
+
} finally {
|
|
915
|
+
db.close();
|
|
916
|
+
}
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/automation/flywheel-merge-retry.ts
|
|
922
|
+
async function detectFailedApplyCorrection(params) {
|
|
923
|
+
const { cwd, integrationBranch, applyFailedIntegrationSha } = params;
|
|
924
|
+
if (!applyFailedIntegrationSha) return { corrected: false, commits: [] };
|
|
925
|
+
const ancestry = await execGit(
|
|
926
|
+
["merge-base", "--is-ancestor", applyFailedIntegrationSha, integrationBranch],
|
|
927
|
+
{ cwd, timeoutMs: 3e4 }
|
|
928
|
+
);
|
|
929
|
+
if (ancestry.exitCode !== 0) return { corrected: false, commits: [] };
|
|
930
|
+
const range = `${applyFailedIntegrationSha}..${integrationBranch}`;
|
|
931
|
+
const count = await execGit(["rev-list", "--count", range], { cwd, timeoutMs: 3e4 });
|
|
932
|
+
const commitsPast = count.exitCode === 0 ? Number.parseInt(count.stdout.trim(), 10) : 0;
|
|
933
|
+
if (!Number.isFinite(commitsPast) || commitsPast <= 0) return { corrected: false, commits: [] };
|
|
934
|
+
const log = await execGit(["log", "--reverse", "--format=%h %s", range], {
|
|
935
|
+
cwd,
|
|
936
|
+
timeoutMs: 3e4
|
|
937
|
+
});
|
|
938
|
+
const commits = log.exitCode === 0 ? log.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0) : [];
|
|
939
|
+
return { corrected: true, commits };
|
|
940
|
+
}
|
|
941
|
+
async function resolveIntegrationTipForRecovery(cwd, branch) {
|
|
942
|
+
if (!branch) return void 0;
|
|
943
|
+
try {
|
|
944
|
+
const tip = await execGit(["rev-parse", branch], { cwd, timeoutMs: 1e4 });
|
|
945
|
+
return tip.exitCode === 0 ? tip.stdout.trim() || void 0 : void 0;
|
|
946
|
+
} catch {
|
|
947
|
+
return void 0;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function reconciliationApplyTarget(manifest) {
|
|
951
|
+
return manifest.branchRoleRefused ? void 0 : manifest.applyTargetBranch ?? void 0;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/automation/flywheel-merge-signal.ts
|
|
955
|
+
import { randomUUID } from "crypto";
|
|
956
|
+
function registerMergeSignalFinalizer(workflowId) {
|
|
957
|
+
const controller = new AbortController();
|
|
958
|
+
const cleanupName = `FlywheelMerge:${workflowId}:${randomUUID()}`;
|
|
959
|
+
let settle = () => {
|
|
960
|
+
};
|
|
961
|
+
const settled = new Promise((resolve2) => {
|
|
962
|
+
settle = resolve2;
|
|
963
|
+
});
|
|
964
|
+
signalCoordinator.register(
|
|
965
|
+
cleanupName,
|
|
966
|
+
async ({ reason }) => {
|
|
967
|
+
if (reason !== "SIGINT" && reason !== "SIGTERM") return;
|
|
968
|
+
controller.abort(new Error(`Merge-result cancelled by ${reason}`));
|
|
969
|
+
await settled;
|
|
970
|
+
},
|
|
971
|
+
// Stash restore may need apply + list + drop, each with its existing timeout.
|
|
972
|
+
{ priority: "critical", phase: "rollback", budgetMs: 12e4 }
|
|
973
|
+
);
|
|
974
|
+
return {
|
|
975
|
+
signal: controller.signal,
|
|
976
|
+
finish: () => {
|
|
977
|
+
settle();
|
|
978
|
+
signalCoordinator.unregister(cleanupName);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/automation/flywheel-merge-stash-outcome.ts
|
|
984
|
+
import chalk2 from "chalk";
|
|
985
|
+
async function collectUnmergedPaths(cwd) {
|
|
986
|
+
const unmerged = await execGit(["diff", "--name-only", "--diff-filter=U"], {
|
|
987
|
+
cwd,
|
|
988
|
+
timeoutMs: 3e4
|
|
989
|
+
});
|
|
990
|
+
if (unmerged.exitCode !== 0) return [];
|
|
991
|
+
return unmerged.stdout.split("\n").map((entry) => entry.trim()).filter(Boolean);
|
|
992
|
+
}
|
|
993
|
+
function describeUnrestoredAutoStash(params) {
|
|
994
|
+
const { reason, stashSha, conflictPaths = [] } = params;
|
|
995
|
+
return `Auto-stashed changes were NOT restored: ${reason}. The stash is preserved${stashSha ? ` at ${stashSha}` : ""}` + (conflictPaths.length > 0 ? `, and these paths are left unmerged in the checkout: ${conflictPaths.join(", ")}` : "") + (stashSha ? `. Recover manually with: git stash apply ${stashSha}` : '. Recover manually with: git stash list | grep "Build Flywheel"');
|
|
996
|
+
}
|
|
997
|
+
function describeRetainedAutoStash(stashSha) {
|
|
998
|
+
return describeUnrestoredAutoStash({
|
|
999
|
+
reason: "the staged merge was left in the checkout",
|
|
1000
|
+
stashSha
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
function reportMergeOutcome(warnings) {
|
|
1004
|
+
if (warnings.length === 0) {
|
|
1005
|
+
console.log(chalk2.bold.green("\nMerge complete!\n"));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
console.log(
|
|
1009
|
+
chalk2.bold.yellow(
|
|
1010
|
+
`
|
|
1011
|
+
Merge applied with ${warnings.length} warning(s) - review the warnings above.
|
|
1012
|
+
`
|
|
1013
|
+
)
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/automation/flywheel-post-merge-unavailable.ts
|
|
1018
|
+
function classifyPostMergeVerifyUnavailable(failed, checkoutRoot) {
|
|
1019
|
+
if (failed.exitCode === 0) return void 0;
|
|
1020
|
+
const output = failed.outputTail ?? `${failed.stdoutTail ?? ""}
|
|
1021
|
+
${failed.stderrTail ?? ""}`;
|
|
1022
|
+
const detect = failed.complete === false ? detectUnavailableSignal : detectResolverUnavailableSignal;
|
|
1023
|
+
const signal = detect(failed.command, output, failed.exitCode);
|
|
1024
|
+
if (signal?.unavailableReason === "zero_tests" || failed.unavailableReason === "zero_tests") {
|
|
1025
|
+
return {
|
|
1026
|
+
reason: signal?.message ?? "Verification unavailable: the command ran no tests.",
|
|
1027
|
+
setupCommands: [],
|
|
1028
|
+
emptyRun: true
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
const reason = signal?.message ?? detectMissingInstallForVerification(checkoutRoot, failed.command, output, failed.exitCode) ?? (failed.unavailable ? "Verification tool unavailable: the command could not be launched." : void 0);
|
|
1032
|
+
if (!reason) return void 0;
|
|
1033
|
+
return {
|
|
1034
|
+
reason,
|
|
1035
|
+
setupCommands: deriveDependencySetupCommandsForVerification(checkoutRoot, failed.command)
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function describePostMergeVerifySetup(unavailable) {
|
|
1039
|
+
if (unavailable.emptyRun) {
|
|
1040
|
+
return "The command executed no tests on the merged tree, so it verified nothing. Check what it selects, then retry.";
|
|
1041
|
+
}
|
|
1042
|
+
return unavailable.setupCommands.length > 0 ? `Build does not install into your checkout. Install the dependencies there (${unavailable.setupCommands.join("; ")}), then retry.` : "Build does not install into your checkout, and the project resolver derives no install command for this command. Install its dependencies there, then retry.";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/automation/flywheel-task-transitions.ts
|
|
1046
|
+
import * as fs3 from "fs/promises";
|
|
1047
|
+
import * as path3 from "path";
|
|
1048
|
+
import chalk3 from "chalk";
|
|
1049
|
+
function readArrayField(obj, key) {
|
|
1050
|
+
const value = obj[key];
|
|
1051
|
+
return Array.isArray(value) ? value : void 0;
|
|
1052
|
+
}
|
|
1053
|
+
function readStringField(obj, key) {
|
|
1054
|
+
const value = obj[key];
|
|
1055
|
+
return typeof value === "string" ? value : void 0;
|
|
1056
|
+
}
|
|
1057
|
+
var VALID_TASK_TRANSITION_STATUSES = /* @__PURE__ */ new Set([
|
|
1058
|
+
"todo",
|
|
1059
|
+
"in_progress",
|
|
1060
|
+
"review",
|
|
1061
|
+
"done",
|
|
1062
|
+
"blocked"
|
|
1063
|
+
]);
|
|
1064
|
+
function extractTaskTransitionsFromResult(parsed) {
|
|
1065
|
+
if (!isRecord(parsed)) return null;
|
|
1066
|
+
const stageResults = readArrayField(parsed, "stageResults");
|
|
1067
|
+
const allTransitions = [];
|
|
1068
|
+
const collectTransitions = (transitions) => {
|
|
1069
|
+
if (!Array.isArray(transitions)) return;
|
|
1070
|
+
for (const transition of transitions) {
|
|
1071
|
+
if (!isRecord(transition)) continue;
|
|
1072
|
+
const taskId = readStringField(transition, "taskId");
|
|
1073
|
+
const targetStatus = readStringField(transition, "targetStatus");
|
|
1074
|
+
if (!taskId || !targetStatus) continue;
|
|
1075
|
+
if (!VALID_TASK_TRANSITION_STATUSES.has(targetStatus)) {
|
|
1076
|
+
console.log(
|
|
1077
|
+
chalk3.yellow(
|
|
1078
|
+
` Warning: Skipping task transition for ${taskId}: invalid targetStatus '${targetStatus}'`
|
|
1079
|
+
)
|
|
1080
|
+
);
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const hoursLogged = typeof transition.hoursLogged === "number" ? transition.hoursLogged : void 0;
|
|
1084
|
+
const reason = readStringField(transition, "reason");
|
|
1085
|
+
const resultArtifactPath = readStringField(transition, "resultArtifactPath");
|
|
1086
|
+
const transitionId = readStringField(transition, "transitionId");
|
|
1087
|
+
allTransitions.push({
|
|
1088
|
+
taskId,
|
|
1089
|
+
targetStatus,
|
|
1090
|
+
hoursLogged,
|
|
1091
|
+
reason,
|
|
1092
|
+
...resultArtifactPath ? { resultArtifactPath } : {},
|
|
1093
|
+
...transitionId ? { transitionId } : {}
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
for (const stageResult of stageResults ?? []) {
|
|
1098
|
+
if (!isRecord(stageResult)) continue;
|
|
1099
|
+
collectTransitions(readArrayField(stageResult, "taskTransitions"));
|
|
1100
|
+
}
|
|
1101
|
+
collectTransitions(readArrayField(parsed, "taskTransitions"));
|
|
1102
|
+
return allTransitions.length > 0 ? allTransitions : null;
|
|
1103
|
+
}
|
|
1104
|
+
async function loadRecordedTaskTransitions(workflowId, sprintId) {
|
|
1105
|
+
let workflowResult;
|
|
1106
|
+
try {
|
|
1107
|
+
workflowResult = JSON.parse(await fs3.readFile(getResultPath(workflowId), "utf-8"));
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
console.log(
|
|
1110
|
+
chalk3.yellow(
|
|
1111
|
+
` Warning: Failed to load workflow result for status sync: ${error instanceof Error ? error.message : String(error)}`
|
|
1112
|
+
)
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const transitions = extractTaskTransitionsFromResult(workflowResult);
|
|
1116
|
+
if (transitions) return transitions;
|
|
1117
|
+
emitWarning(
|
|
1118
|
+
"FM-002",
|
|
1119
|
+
{ taskId: sprintId, subsystem: "flywheel-merge", executionPhase: "cleanup" },
|
|
1120
|
+
{
|
|
1121
|
+
severity: "WARN" /* WARN */,
|
|
1122
|
+
metadata: { workflowId, sprintId, operation: "task-transition-artifact-missing" }
|
|
1123
|
+
}
|
|
1124
|
+
);
|
|
1125
|
+
console.log(
|
|
1126
|
+
chalk3.yellow(
|
|
1127
|
+
` Warning: taskTransitions artifact missing for sprint workflow '${sprintId}'. Legacy task-status sync skipped (stageIds are not task IDs for sprint workflows). Ensure sprint-bridge.ts populates taskTransitions on stageResults.`
|
|
1128
|
+
)
|
|
1129
|
+
);
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
async function applyVerifiedTaskCompletions(sprintId, transitions) {
|
|
1133
|
+
return applyTaskTransitions(sprintId, transitions, { promoteToDone: true });
|
|
1134
|
+
}
|
|
1135
|
+
function summarizeTaskSyncFailures(result) {
|
|
1136
|
+
if (result.tasksFailed <= 0) return void 0;
|
|
1137
|
+
return {
|
|
1138
|
+
count: result.tasksFailed,
|
|
1139
|
+
details: (result.details ?? []).filter((detail) => detail.error !== void 0).map((detail) => ({ taskId: detail.taskId, error: detail.error ?? "unknown error" }))
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function reportTaskSyncFailures(params) {
|
|
1143
|
+
const failures = summarizeTaskSyncFailures(params.result);
|
|
1144
|
+
if (!failures) return void 0;
|
|
1145
|
+
params.onWarning(
|
|
1146
|
+
` Warning: ${failures.count} task transition(s) failed (merge still succeeded):
|
|
1147
|
+
` + failures.details.map((d) => ` - ${d.taskId}: ${d.error}`).join("\n")
|
|
1148
|
+
);
|
|
1149
|
+
emitWarning(
|
|
1150
|
+
"FM-001",
|
|
1151
|
+
{ subsystem: "flywheel", taskId: params.workflowId },
|
|
1152
|
+
{
|
|
1153
|
+
severity: "WARN" /* WARN */,
|
|
1154
|
+
metadata: {
|
|
1155
|
+
type: "task_sync_partial_failure",
|
|
1156
|
+
workflowId: params.workflowId,
|
|
1157
|
+
tasksFailed: failures.count,
|
|
1158
|
+
failedTaskIds: failures.details.map((d) => d.taskId)
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
);
|
|
1162
|
+
return failures;
|
|
1163
|
+
}
|
|
1164
|
+
async function adoptStatusWriteTreeDelta(cwd, expectedTreeFingerprint, stagedStatusWritePaths) {
|
|
1165
|
+
const current = await execGit(["write-tree"], { cwd, timeoutMs: 3e4 });
|
|
1166
|
+
const candidate = current.stdout.trim();
|
|
1167
|
+
if (current.exitCode !== 0 || candidate.length === 0) {
|
|
1168
|
+
throw new Error("Could not fingerprint the staged tree after task status transitions.");
|
|
1169
|
+
}
|
|
1170
|
+
if (candidate === expectedTreeFingerprint) return expectedTreeFingerprint;
|
|
1171
|
+
const allowed = new Set([...stagedStatusWritePaths].map(normalizeRepoPath));
|
|
1172
|
+
const changed = await execGit(["diff", "--name-only", expectedTreeFingerprint, candidate], {
|
|
1173
|
+
cwd,
|
|
1174
|
+
timeoutMs: 3e4
|
|
1175
|
+
});
|
|
1176
|
+
const changedPaths = changed.stdout.split("\n").map((entry) => entry.trim()).filter(Boolean);
|
|
1177
|
+
if (changed.exitCode !== 0 || changedPaths.length === 0 || changedPaths.some((pathname) => !allowed.has(normalizeRepoPath(pathname)))) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
"Task status transitions changed content outside this apply's staged task/sprint status-write targets after final verification; refusing to commit or report apply success."
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
return candidate;
|
|
1183
|
+
}
|
|
1184
|
+
function bucketRelative(mergeCwd, bucketPath) {
|
|
1185
|
+
return path3.relative(mergeCwd, bucketPath).replace(/\\/g, "/");
|
|
1186
|
+
}
|
|
1187
|
+
function taskActivePath(mergeCwd, taskId) {
|
|
1188
|
+
const taskPrefix = bucketRelative(mergeCwd, getBucketPathByKind("task", mergeCwd));
|
|
1189
|
+
return `${taskPrefix}/${taskId}.md`;
|
|
1190
|
+
}
|
|
1191
|
+
function dirtyArchivePathsForId(dirtyPaths, archivePrefix, id) {
|
|
1192
|
+
const suffix = `/${id}.md`;
|
|
1193
|
+
return [...dirtyPaths].filter(
|
|
1194
|
+
(pathname) => pathname.startsWith(`${archivePrefix}/`) && pathname.endsWith(suffix)
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
function wroteThisRun(detail) {
|
|
1198
|
+
return detail.updated || detail.fileUpdated === true;
|
|
1199
|
+
}
|
|
1200
|
+
function buildTaskSyncTargetCandidates(params) {
|
|
1201
|
+
const { mergeCwd, sprintId, details, dirtyAfterSync } = params;
|
|
1202
|
+
const sprintPrefix = bucketRelative(mergeCwd, getBucketPathByKind("sprint", mergeCwd));
|
|
1203
|
+
const sprintArchivePrefix = bucketRelative(
|
|
1204
|
+
mergeCwd,
|
|
1205
|
+
getBucketArchivePathByKind("sprint", mergeCwd)
|
|
1206
|
+
);
|
|
1207
|
+
const candidates = /* @__PURE__ */ new Set([
|
|
1208
|
+
`${sprintPrefix}/${sprintId}.md`,
|
|
1209
|
+
...dirtyArchivePathsForId(dirtyAfterSync, sprintArchivePrefix, sprintId)
|
|
1210
|
+
]);
|
|
1211
|
+
for (const detail of details) {
|
|
1212
|
+
if (!wroteThisRun(detail)) continue;
|
|
1213
|
+
candidates.add(taskActivePath(mergeCwd, detail.taskId));
|
|
1214
|
+
if (detail.archivedPath !== void 0 && detail.archivedPath.length > 0) {
|
|
1215
|
+
candidates.add(bucketRelative(mergeCwd, detail.archivedPath));
|
|
1216
|
+
}
|
|
1217
|
+
for (const unblockedId of detail.unblockedTaskIds ?? []) {
|
|
1218
|
+
candidates.add(taskActivePath(mergeCwd, unblockedId));
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return candidates;
|
|
1222
|
+
}
|
|
1223
|
+
function buildResumeClaimCandidates(params) {
|
|
1224
|
+
const { mergeCwd, sprintId, transitions, dirty } = params;
|
|
1225
|
+
const claimed = buildTaskSyncTargetCandidates({
|
|
1226
|
+
mergeCwd,
|
|
1227
|
+
sprintId,
|
|
1228
|
+
details: [],
|
|
1229
|
+
dirtyAfterSync: dirty
|
|
1230
|
+
});
|
|
1231
|
+
const taskArchivePrefix = bucketRelative(mergeCwd, getBucketArchivePathByKind("task", mergeCwd));
|
|
1232
|
+
for (const transition of transitions) {
|
|
1233
|
+
claimed.add(taskActivePath(mergeCwd, transition.taskId));
|
|
1234
|
+
for (const archived of dirtyArchivePathsForId(dirty, taskArchivePrefix, transition.taskId)) {
|
|
1235
|
+
claimed.add(archived);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return claimed;
|
|
1239
|
+
}
|
|
1240
|
+
function describeUnreconciledTaskSync(params) {
|
|
1241
|
+
const { mergeCwd, details, dirtyAfterSync } = params;
|
|
1242
|
+
const taskArchivePrefix = bucketRelative(mergeCwd, getBucketArchivePathByKind("task", mergeCwd));
|
|
1243
|
+
const taskIds = details.filter((detail) => !wroteThisRun(detail)).filter(
|
|
1244
|
+
(detail) => dirtyAfterSync.has(taskActivePath(mergeCwd, detail.taskId)) && dirtyArchivePathsForId(dirtyAfterSync, taskArchivePrefix, detail.taskId).length > 0
|
|
1245
|
+
).map((detail) => detail.taskId);
|
|
1246
|
+
if (taskIds.length === 0) return void 0;
|
|
1247
|
+
return `Task sync incomplete: ${taskIds.join(", ")} - uncommitted content exists in both the active and archive location, and this apply did not write it. Those files were left untouched and are not part of this commit; reconcile them manually before re-running merge.`;
|
|
1248
|
+
}
|
|
1249
|
+
function selectTaskSyncStagePaths(params) {
|
|
1250
|
+
const { dirtyAfterSync, syncTargetCandidates } = params;
|
|
1251
|
+
return [...dirtyAfterSync].filter((pathname) => syncTargetCandidates.has(pathname));
|
|
1252
|
+
}
|
|
1253
|
+
async function reopenTasksArchivedWithoutCommit(details) {
|
|
1254
|
+
const warnings = [];
|
|
1255
|
+
for (const detail of details) {
|
|
1256
|
+
if (!detail.archivedPath) continue;
|
|
1257
|
+
try {
|
|
1258
|
+
await restoreTask(detail.taskId, { status: "review" });
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
warnings.push(
|
|
1261
|
+
`Task ${detail.taskId} was archived as done but the apply commit failed; reopen it manually (${error instanceof Error ? error.message : String(error)}).`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return warnings;
|
|
1266
|
+
}
|
|
1267
|
+
function unclaimedStorageResidue(mergeCwd, dirty, claimed) {
|
|
1268
|
+
const prefixes = [];
|
|
1269
|
+
const localArchives = [];
|
|
1270
|
+
for (const kind of ["task", "sprint"]) {
|
|
1271
|
+
const activePrefix = bucketRelative(mergeCwd, getBucketPathByKind(kind, mergeCwd));
|
|
1272
|
+
const archivePrefix = bucketRelative(mergeCwd, getBucketArchivePathByKind(kind, mergeCwd));
|
|
1273
|
+
prefixes.push(`${activePrefix}/`, `${archivePrefix}/`);
|
|
1274
|
+
if (getBucketByName(kind, mergeCwd)?.archivePublished === false) {
|
|
1275
|
+
localArchives.push(`${archivePrefix}/`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return [...dirty].filter(
|
|
1279
|
+
(pathname) => !claimed.has(pathname) && prefixes.some((prefix) => pathname.startsWith(prefix)) && !localArchives.some((prefix) => pathname.startsWith(prefix))
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
async function publishCompletionToStorage(params, completion) {
|
|
1283
|
+
const { workflowId, sprintId, mergeCwd } = params;
|
|
1284
|
+
const fail = (paths, error) => {
|
|
1285
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1286
|
+
completion.publication = { outcome: "failed", paths, reason };
|
|
1287
|
+
const message = `Task completion not published to planning storage (${reason})` + (paths.length > 0 ? `; these writes remain uncommitted in the storage worktree: ${paths.join(", ")}` : "");
|
|
1288
|
+
params.onWarning(` Warning: ${message}`);
|
|
1289
|
+
completion.warnings.push(message);
|
|
1290
|
+
return void 0;
|
|
1291
|
+
};
|
|
1292
|
+
let ctx;
|
|
1293
|
+
let storageDirty;
|
|
1294
|
+
try {
|
|
1295
|
+
ctx = await beginManagedSync(mergeCwd);
|
|
1296
|
+
storageDirty = await snapshotDirtyPlanningPaths({
|
|
1297
|
+
cwd: ctx.storageRoot,
|
|
1298
|
+
allowlistPrefixes: []
|
|
1299
|
+
});
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
return fail([], error);
|
|
1302
|
+
}
|
|
1303
|
+
const storageRoot = ctx.storageRoot;
|
|
1304
|
+
const dirty = new Set(
|
|
1305
|
+
[...storageDirty].map((pathname) => bucketRelative(mergeCwd, path3.join(storageRoot, pathname)))
|
|
1306
|
+
);
|
|
1307
|
+
const candidates = buildTaskSyncTargetCandidates({
|
|
1308
|
+
mergeCwd,
|
|
1309
|
+
sprintId,
|
|
1310
|
+
details: completion.details,
|
|
1311
|
+
dirtyAfterSync: dirty
|
|
1312
|
+
});
|
|
1313
|
+
if (params.resume) {
|
|
1314
|
+
for (const claimed of buildResumeClaimCandidates({
|
|
1315
|
+
mergeCwd,
|
|
1316
|
+
sprintId,
|
|
1317
|
+
transitions: params.transitions,
|
|
1318
|
+
dirty
|
|
1319
|
+
})) {
|
|
1320
|
+
candidates.add(claimed);
|
|
1321
|
+
}
|
|
1322
|
+
completion.unclaimedStoragePaths = unclaimedStorageResidue(mergeCwd, dirty, candidates);
|
|
1323
|
+
}
|
|
1324
|
+
const owned = selectTaskSyncStagePaths({
|
|
1325
|
+
dirtyAfterSync: dirty,
|
|
1326
|
+
syncTargetCandidates: candidates
|
|
1327
|
+
});
|
|
1328
|
+
for (const detail of completion.details) {
|
|
1329
|
+
if (!wroteThisRun(detail) || !detail.archivedPath) continue;
|
|
1330
|
+
const archived = bucketRelative(mergeCwd, detail.archivedPath);
|
|
1331
|
+
if (!owned.includes(archived)) owned.push(archived);
|
|
1332
|
+
}
|
|
1333
|
+
if (owned.length === 0) return dirty;
|
|
1334
|
+
try {
|
|
1335
|
+
await params.assertApplyLockOwned();
|
|
1336
|
+
const published = await publishManagedSync(ctx, {
|
|
1337
|
+
paths: owned,
|
|
1338
|
+
message: `chore: sync task/sprint metadata [flywheel] (${workflowId})`,
|
|
1339
|
+
// The pending line is rendered once, below, through the merge warnings.
|
|
1340
|
+
reportPending: "caller"
|
|
1341
|
+
});
|
|
1342
|
+
completion.publication = {
|
|
1343
|
+
outcome: published.publishOutcome,
|
|
1344
|
+
paths: owned,
|
|
1345
|
+
...published.reason ? { reason: published.reason } : {},
|
|
1346
|
+
...published.pendingRef ? { pendingRef: published.pendingRef } : {}
|
|
1347
|
+
};
|
|
1348
|
+
if (published.pendingWarning) {
|
|
1349
|
+
params.onWarning(` Warning: Task completion: ${published.pendingWarning}`);
|
|
1350
|
+
completion.warnings.push(`Task completion: ${published.pendingWarning}`);
|
|
1351
|
+
} else if (published.publishOutcome === "published") {
|
|
1352
|
+
params.onInfo(` Task completion published to planning storage (${owned.length} path(s))`);
|
|
1353
|
+
} else {
|
|
1354
|
+
const reason = published.reason ?? "nothing to publish";
|
|
1355
|
+
params.onInfo(` Task completion not published: ${reason}`);
|
|
1356
|
+
}
|
|
1357
|
+
if (published.localByPolicyPaths.length > 0) {
|
|
1358
|
+
params.onInfo(` Kept local by archive policy: ${published.localByPolicyPaths.join(", ")}`);
|
|
1359
|
+
}
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
return fail(owned, error);
|
|
1362
|
+
}
|
|
1363
|
+
return dirty;
|
|
1364
|
+
}
|
|
1365
|
+
async function completeAndStageVerifiedTasks(params) {
|
|
1366
|
+
const { workflowId, sprintId, mergeCwd } = params;
|
|
1367
|
+
const completion = { details: [], stagedPaths: [], warnings: [] };
|
|
1368
|
+
try {
|
|
1369
|
+
await params.assertApplyLockOwned();
|
|
1370
|
+
const syncResult = await applyVerifiedTaskCompletions(sprintId, params.transitions);
|
|
1371
|
+
completion.details = syncResult.details ?? [];
|
|
1372
|
+
params.onInfo(formatSyncSummary(syncResult));
|
|
1373
|
+
completion.failures = reportTaskSyncFailures({
|
|
1374
|
+
workflowId,
|
|
1375
|
+
result: syncResult,
|
|
1376
|
+
onWarning: params.onWarning
|
|
1377
|
+
});
|
|
1378
|
+
let dirtyAfterSync;
|
|
1379
|
+
if (codeCheckoutCarriesSharedBuckets(mergeCwd)) {
|
|
1380
|
+
dirtyAfterSync = await snapshotDirtyPlanningPaths({
|
|
1381
|
+
cwd: mergeCwd,
|
|
1382
|
+
allowlistPrefixes: params.allowlistPrefixes
|
|
1383
|
+
});
|
|
1384
|
+
completion.stagedPaths = selectTaskSyncStagePaths({
|
|
1385
|
+
dirtyAfterSync,
|
|
1386
|
+
syncTargetCandidates: buildTaskSyncTargetCandidates({
|
|
1387
|
+
mergeCwd,
|
|
1388
|
+
sprintId,
|
|
1389
|
+
details: completion.details,
|
|
1390
|
+
dirtyAfterSync
|
|
1391
|
+
})
|
|
1392
|
+
});
|
|
1393
|
+
if (completion.stagedPaths.length > 0) {
|
|
1394
|
+
await execGitChecked(["add", "--", ...completion.stagedPaths], {
|
|
1395
|
+
cwd: mergeCwd,
|
|
1396
|
+
timeoutMs: 3e4
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
} else {
|
|
1400
|
+
dirtyAfterSync = await publishCompletionToStorage(params, completion);
|
|
1401
|
+
}
|
|
1402
|
+
const unreconciled = params.resume || !dirtyAfterSync ? void 0 : describeUnreconciledTaskSync({ mergeCwd, details: completion.details, dirtyAfterSync });
|
|
1403
|
+
if (unreconciled) {
|
|
1404
|
+
params.onWarning(` Warning: ${unreconciled}`);
|
|
1405
|
+
completion.warnings.push(unreconciled);
|
|
1406
|
+
}
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
if (error instanceof PlanningSnapshotError) throw error;
|
|
1409
|
+
params.onWarning(
|
|
1410
|
+
` Warning: Task transition application failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
return completion;
|
|
1414
|
+
}
|
|
1415
|
+
var HELD_METADATA_TRAILER = "Build-Held-Metadata: ";
|
|
1416
|
+
function formatHeldMetadataTrailers(paths) {
|
|
1417
|
+
return paths.map((pathname) => `${HELD_METADATA_TRAILER}${pathname}`).join("\n");
|
|
1418
|
+
}
|
|
1419
|
+
function parseHeldMetadataTrailers(body) {
|
|
1420
|
+
return body.split(/\r?\n/).filter((line) => line.startsWith(HELD_METADATA_TRAILER)).map((line) => line.slice(HELD_METADATA_TRAILER.length)).filter((pathname) => pathname.length > 0);
|
|
1421
|
+
}
|
|
1422
|
+
async function finishCompletionAfterCodeCommit(params) {
|
|
1423
|
+
const { mergeCwd, sprintId } = params;
|
|
1424
|
+
const headBody = await execGit(["log", "-1", "--format=%B", "HEAD"], {
|
|
1425
|
+
cwd: mergeCwd,
|
|
1426
|
+
timeoutMs: 3e4
|
|
1427
|
+
});
|
|
1428
|
+
if (headBody.exitCode !== 0) {
|
|
1429
|
+
return { kind: "refused", reason: "Could not read the code commit message to resume." };
|
|
1430
|
+
}
|
|
1431
|
+
const heldBack = parseHeldMetadataTrailers(headBody.stdout);
|
|
1432
|
+
const completion = sprintId && params.transitions ? await completeAndStageVerifiedTasks({
|
|
1433
|
+
workflowId: params.workflowId,
|
|
1434
|
+
sprintId,
|
|
1435
|
+
transitions: params.transitions,
|
|
1436
|
+
mergeCwd,
|
|
1437
|
+
allowlistPrefixes: params.allowlistPrefixes,
|
|
1438
|
+
assertApplyLockOwned: params.assertApplyLockOwned,
|
|
1439
|
+
onInfo: params.onInfo,
|
|
1440
|
+
onWarning: params.onWarning,
|
|
1441
|
+
resume: true
|
|
1442
|
+
}) : void 0;
|
|
1443
|
+
if (completion?.failures) {
|
|
1444
|
+
return {
|
|
1445
|
+
kind: "refused",
|
|
1446
|
+
reason: `Task transitions failed while finishing ${params.workflowId}: ${completion.failures.details.map((d) => `${d.taskId} (${d.error})`).join("; ")}. Repair those tasks, then rerun merge-result.`
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
if (completion?.publication?.outcome === "failed") {
|
|
1450
|
+
return {
|
|
1451
|
+
kind: "refused",
|
|
1452
|
+
reason: `The task completion for ${params.workflowId} was not published to planning storage (${completion.publication.reason ?? "unknown error"}). Resolve the cause, then rerun merge-result.`
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const dirty = await snapshotDirtyPlanningPaths({ cwd: mergeCwd, allowlistPrefixes: [] });
|
|
1456
|
+
const owned = /* @__PURE__ */ new Set([...heldBack, ...completion?.stagedPaths ?? []]);
|
|
1457
|
+
if (sprintId && params.transitions) {
|
|
1458
|
+
for (const candidate of buildResumeClaimCandidates({
|
|
1459
|
+
mergeCwd,
|
|
1460
|
+
sprintId,
|
|
1461
|
+
transitions: params.transitions,
|
|
1462
|
+
dirty
|
|
1463
|
+
})) {
|
|
1464
|
+
owned.add(candidate);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
const unowned = [
|
|
1468
|
+
...[...dirty].filter(
|
|
1469
|
+
(p) => params.bucketPrefixes.some((prefix) => p.startsWith(prefix)) && !owned.has(p)
|
|
1470
|
+
),
|
|
1471
|
+
...completion?.unclaimedStoragePaths ?? []
|
|
1472
|
+
];
|
|
1473
|
+
if (unowned.length > 0) {
|
|
1474
|
+
return {
|
|
1475
|
+
kind: "refused",
|
|
1476
|
+
reason: `The code commit for ${params.workflowId} landed but its task metadata was not committed, and other uncommitted task/sprint/backlog changes are present: ${unowned.join(", ")}. Set them aside without committing (for example with git stash), rerun merge-result to finish the completion, then restore them.`
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const toStage = [...dirty].filter((p) => owned.has(p));
|
|
1480
|
+
if (toStage.length > 0) {
|
|
1481
|
+
await params.assertApplyLockOwned();
|
|
1482
|
+
await execGitChecked(["add", "--", ...toStage], { cwd: mergeCwd, timeoutMs: 3e4 });
|
|
1483
|
+
}
|
|
1484
|
+
const stagedResult = await execGitChecked(["diff", "--cached", "--name-only", "-z"], {
|
|
1485
|
+
cwd: mergeCwd,
|
|
1486
|
+
timeoutMs: 3e4
|
|
1487
|
+
});
|
|
1488
|
+
const staged = stagedResult.stdout.split("\0").filter(Boolean);
|
|
1489
|
+
const stray = staged.filter((p) => !owned.has(p));
|
|
1490
|
+
if (stray.length > 0) {
|
|
1491
|
+
return {
|
|
1492
|
+
kind: "refused",
|
|
1493
|
+
reason: `Refusing to finish the completion for ${params.workflowId}: changes this apply does not own are staged (${stray.join(", ")}). Unstage them, then rerun merge-result.`
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
const warnings = completion?.warnings ?? [];
|
|
1497
|
+
if (staged.length === 0) {
|
|
1498
|
+
return { kind: "finished", warnings };
|
|
1499
|
+
}
|
|
1500
|
+
const authorizedTree = await execGit(["write-tree"], { cwd: mergeCwd, timeoutMs: 3e4 });
|
|
1501
|
+
if (authorizedTree.exitCode !== 0 || authorizedTree.stdout.trim().length === 0) {
|
|
1502
|
+
return { kind: "refused", reason: "Could not fingerprint the staged metadata to resume." };
|
|
1503
|
+
}
|
|
1504
|
+
await params.assertApplyLockOwned();
|
|
1505
|
+
const codeCommit = await execGit(["rev-parse", "HEAD"], { cwd: mergeCwd, timeoutMs: 3e4 });
|
|
1506
|
+
try {
|
|
1507
|
+
await runFlywheelCommit({
|
|
1508
|
+
cwd: mergeCwd,
|
|
1509
|
+
phase: "metadata",
|
|
1510
|
+
run: () => execGitChecked(["commit", "-m", params.metadataCommitMessage], {
|
|
1511
|
+
cwd: mergeCwd,
|
|
1512
|
+
timeoutMs: GIT_HOOK_COMMIT_TIMEOUT_MS,
|
|
1513
|
+
env: { BUILD_BACKLOG_SYNC: "1" }
|
|
1514
|
+
}),
|
|
1515
|
+
// execGitChecked captures git's output, so the failure message carries it.
|
|
1516
|
+
quiet: true,
|
|
1517
|
+
style: params.commitStyle,
|
|
1518
|
+
priorCommit: true
|
|
1519
|
+
});
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (!(error instanceof FlywheelCommitError)) throw error;
|
|
1522
|
+
return {
|
|
1523
|
+
kind: "refused",
|
|
1524
|
+
reason: `${error.message}
|
|
1525
|
+
Resolve the git error above, then rerun \`build run flywheel merge-result ${params.workflowId}\` to finish the completion.`
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
const [head, headTree] = await Promise.all([
|
|
1529
|
+
execGit(["rev-parse", "HEAD"], { cwd: mergeCwd, timeoutMs: 3e4 }),
|
|
1530
|
+
execGit(["rev-parse", "HEAD^{tree}"], { cwd: mergeCwd, timeoutMs: 3e4 })
|
|
1531
|
+
]);
|
|
1532
|
+
if (headTree.exitCode !== 0 || headTree.stdout.trim() !== authorizedTree.stdout.trim()) {
|
|
1533
|
+
const branch = await execGit(["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
1534
|
+
cwd: mergeCwd,
|
|
1535
|
+
timeoutMs: 3e4
|
|
1536
|
+
});
|
|
1537
|
+
const changed = head.exitCode === 0 ? await describeCommitChangedTree({
|
|
1538
|
+
cwd: mergeCwd,
|
|
1539
|
+
workflowId: params.workflowId,
|
|
1540
|
+
commits: [{ preCommitTree: authorizedTree.stdout.trim(), sha: head.stdout.trim() }],
|
|
1541
|
+
branch: branch.exitCode === 0 ? branch.stdout.trim() : void 0,
|
|
1542
|
+
restoreSha: codeCommit.exitCode === 0 ? codeCommit.stdout.trim() : void 0,
|
|
1543
|
+
headline: "The metadata commit does not carry the staged task metadata."
|
|
1544
|
+
}).catch(() => void 0) : void 0;
|
|
1545
|
+
return {
|
|
1546
|
+
kind: "refused",
|
|
1547
|
+
reason: changed ?? `The metadata commit for ${params.workflowId} does not carry the staged task metadata (a commit hook may have changed it). The commit was preserved for inspection and is not a verified apply.`,
|
|
1548
|
+
unverifiedCommitTree: authorizedTree.stdout.trim()
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
return {
|
|
1552
|
+
kind: "finished",
|
|
1553
|
+
metadataCommitSha: head.exitCode === 0 ? head.stdout.trim() || void 0 : void 0,
|
|
1554
|
+
warnings
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/automation/flywheel-verify-stage-status.ts
|
|
1559
|
+
function isRecord3(value) {
|
|
1560
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1561
|
+
}
|
|
1562
|
+
function getWorkflowVerifyStageStatusFromResult(parsed) {
|
|
1563
|
+
if (!isRecord3(parsed)) return null;
|
|
1564
|
+
const stageResults = Array.isArray(parsed.stageResults) ? parsed.stageResults : void 0;
|
|
1565
|
+
if (!stageResults) return null;
|
|
1566
|
+
const statuses = [];
|
|
1567
|
+
for (const r of stageResults) {
|
|
1568
|
+
if (!isRecord3(r)) continue;
|
|
1569
|
+
const stageType = typeof r.stageType === "string" ? r.stageType : void 0;
|
|
1570
|
+
const verifyCommands = Array.isArray(r.verifyCommands) ? r.verifyCommands : void 0;
|
|
1571
|
+
const isVerifyStage = stageType === "verify" || Boolean(verifyCommands);
|
|
1572
|
+
if (!isVerifyStage) continue;
|
|
1573
|
+
const stageId = typeof r.stageId === "string" ? r.stageId : "(unknown)";
|
|
1574
|
+
const validationPassed = r.validationPassed === true;
|
|
1575
|
+
const durationMs = typeof r.durationMs === "number" && Number.isFinite(r.durationMs) ? r.durationMs : void 0;
|
|
1576
|
+
const commandsCount = Array.isArray(verifyCommands) ? verifyCommands.length : 0;
|
|
1577
|
+
const unverified = validationPassed && readStagesUnverifiedDisposition([r]) !== void 0;
|
|
1578
|
+
statuses.push({
|
|
1579
|
+
stageId,
|
|
1580
|
+
validationPassed,
|
|
1581
|
+
durationMs,
|
|
1582
|
+
commandsCount,
|
|
1583
|
+
...unverified ? { unverified: true } : {}
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
return statuses;
|
|
1587
|
+
}
|
|
1588
|
+
function formatVerifyStatusForCommitMessage(parsed) {
|
|
1589
|
+
const verifyStages = getWorkflowVerifyStageStatusFromResult(parsed);
|
|
1590
|
+
if (!verifyStages || verifyStages.length === 0) return null;
|
|
1591
|
+
const totalStages = verifyStages.length;
|
|
1592
|
+
const unverifiedStages = verifyStages.filter((s) => s.unverified).length;
|
|
1593
|
+
const passedStages = verifyStages.filter((s) => s.validationPassed && !s.unverified).length;
|
|
1594
|
+
const totalCommands = verifyStages.reduce((sum, s) => sum + s.commandsCount, 0);
|
|
1595
|
+
const stageLabel = totalStages === 1 ? ` (${verifyStages[0]?.stageId ?? "(unknown)"})` : ` (${passedStages}/${totalStages} stages)`;
|
|
1596
|
+
const commandsLabel = totalCommands === 1 ? "1 cmd" : `${totalCommands} cmds`;
|
|
1597
|
+
const unverifiedLabel = unverifiedStages > 0 ? `, ${unverifiedStages} unverified` : "";
|
|
1598
|
+
return `Verify: ${passedStages}/${totalStages} passed${unverifiedLabel}, ${commandsLabel}${stageLabel}`;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// src/automation/merge-gate-refusal.ts
|
|
1602
|
+
function mergeResultCommand(input) {
|
|
1603
|
+
const base = input.pr && input.baseBranch ? ` --base ${shellPath(input.baseBranch)}` : "";
|
|
1604
|
+
return `build run flywheel merge-result ${input.workflowId}${input.pr ? " --pr" : ""}${base}`;
|
|
1605
|
+
}
|
|
1606
|
+
function asRecord(value) {
|
|
1607
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1608
|
+
}
|
|
1609
|
+
function shellPath(path5) {
|
|
1610
|
+
return /^[\w./@%+=:,-]+$/.test(path5) ? path5 : escapeShellArg(path5);
|
|
1611
|
+
}
|
|
1612
|
+
function nonEmptyString(value) {
|
|
1613
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1614
|
+
}
|
|
1615
|
+
function preservedStageWork(stageResults) {
|
|
1616
|
+
if (!Array.isArray(stageResults)) return [];
|
|
1617
|
+
return stageResults.flatMap((stage) => {
|
|
1618
|
+
const work = asRecord(asRecord(stage)?.preservedWork);
|
|
1619
|
+
if (!work) return [];
|
|
1620
|
+
const location = {
|
|
1621
|
+
worktreePath: nonEmptyString(work.worktreePath),
|
|
1622
|
+
branch: nonEmptyString(work.branch)
|
|
1623
|
+
};
|
|
1624
|
+
return location.worktreePath || location.branch ? [location] : [];
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
function preservedStageBranches(stageResults) {
|
|
1628
|
+
return preservedStageWork(stageResults).flatMap((work) => work.branch ? [work.branch] : []);
|
|
1629
|
+
}
|
|
1630
|
+
var FAILED_VERIFICATION_PART = /\b\d+ stage\(s\) failed verification\b/;
|
|
1631
|
+
function displayedGateReason(input) {
|
|
1632
|
+
const reason = input.summary?.reason ?? "Workflow gate failed";
|
|
1633
|
+
if (reason.includes("stage(s) verification unavailable")) return reason;
|
|
1634
|
+
if (!FAILED_VERIFICATION_PART.test(reason) || !Array.isArray(input.stageResults)) return reason;
|
|
1635
|
+
const blocking = new Set(input.blockingStageIds);
|
|
1636
|
+
const stages = input.stageResults.filter((stage) => {
|
|
1637
|
+
const record = asRecord(stage);
|
|
1638
|
+
return typeof record?.stageId === "string" && blocking.has(record.stageId) && record.winnerNeedsRepair !== true;
|
|
1639
|
+
});
|
|
1640
|
+
if (stages.length === 0) return reason;
|
|
1641
|
+
return reason.replace(
|
|
1642
|
+
FAILED_VERIFICATION_PART,
|
|
1643
|
+
describeVerificationFailedStages(stages).join(", ")
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
function manualFixSteps(input, needsRepair) {
|
|
1647
|
+
const preserved = preservedStageWork(input.stageResults);
|
|
1648
|
+
if (preserved.length > 0) {
|
|
1649
|
+
const locations = preserved.map(
|
|
1650
|
+
(work) => work.worktreePath ? ` cd ${shellPath(work.worktreePath)}${work.branch ? ` # branch ${work.branch}` : ""}
|
|
1651
|
+
` : ` # branch ${work.branch} (no retained worktree)
|
|
1652
|
+
`
|
|
1653
|
+
).join("");
|
|
1654
|
+
const merges = preserved.flatMap((work) => work.branch ? [` git merge --squash ${work.branch}
|
|
1655
|
+
`] : []).join("");
|
|
1656
|
+
return ` ${input.integrationBranch} does not hold the failed stage work; it is kept
|
|
1657
|
+
on the preserved stage branch, checked out in its retained worktree:
|
|
1658
|
+
|
|
1659
|
+
` + locations + " # Committing a fix there does not clear the recorded gate result, and\n # merge-result applies the integration branch, which does not hold this work.\n" + (merges ? ` # To land it by hand, verify it there and commit any uncommitted changes,
|
|
1660
|
+
# then from your target branch:
|
|
1661
|
+
${merges}` : "") + "\n";
|
|
1662
|
+
}
|
|
1663
|
+
if (needsRepair) {
|
|
1664
|
+
return ` git checkout ${input.integrationBranch}
|
|
1665
|
+
# Fix the findings and commit, then re-run the failed gates; reverify clears
|
|
1666
|
+
# the recorded result only when they pass (<stage-id> is under "Blocking stages"):
|
|
1667
|
+
build run flywheel reverify ${input.workflowId} <stage-id>
|
|
1668
|
+
${mergeResultCommand(input)}
|
|
1669
|
+
|
|
1670
|
+
`;
|
|
1671
|
+
}
|
|
1672
|
+
return ` git checkout ${input.integrationBranch}
|
|
1673
|
+
# Fix and commit. Committing does not clear the recorded gate result:
|
|
1674
|
+
# merge-result stays refused until a new run passes, or until you verify by
|
|
1675
|
+
# hand and use the --skip-verify override below.
|
|
1676
|
+
|
|
1677
|
+
`;
|
|
1678
|
+
}
|
|
1679
|
+
function rerunSteps(input) {
|
|
1680
|
+
if (input.activeSprintExists === false) {
|
|
1681
|
+
return ` No active sprint exists to re-run (a failed planned build rolls its temporary
|
|
1682
|
+
sprint back), so 'build plan sprint run --apply' has nothing to run. Start the
|
|
1683
|
+
work again; on a fix/*, docs/* or test/* branch it applies directly:
|
|
1684
|
+
|
|
1685
|
+
build --planned "<intent>"
|
|
1686
|
+
`;
|
|
1687
|
+
}
|
|
1688
|
+
return " build plan sprint run --apply --verification-policy findings\n # Sprint runs default to --verification-policy blocking, which disqualifies failing\n # candidates; findings records failures so repair can run on the winner\n";
|
|
1689
|
+
}
|
|
1690
|
+
function whySection(input, unavailable) {
|
|
1691
|
+
const needsRepair = (input.summary?.needsRepairCount ?? 0) > 0;
|
|
1692
|
+
const manual = manualFixSteps(input, needsRepair);
|
|
1693
|
+
if (needsRepair) {
|
|
1694
|
+
return "Why did this happen?\n Automatic repair was either disabled (--no-repair-on-findings) or failed to resolve all issues.\n\nRECOMMENDED: Re-run with automatic repair enabled (default behavior)\n This will allow the system to automatically fix verification findings:\n\n" + rerunSteps(input) + "\nALTERNATIVE: Manually fix on the run branch\n If you prefer manual control:\n\n" + manual;
|
|
1695
|
+
}
|
|
1696
|
+
if (unavailable) {
|
|
1697
|
+
return "Why did this happen?\n Verification unavailable: the verify commands could not run (a missing tool or\n no verify command for this project), so the change was never checked. Nothing\n ran and failed, so repair had nothing to fix.\n\nRECOMMENDED: Make verification runnable, then re-run\n Install the missing tool or declare verify commands for this project.\n\n" + (preservedStageWork(input.stageResults).length > 0 ? "ALTERNATIVE: Verify by hand on the preserved stage branch\n" : "ALTERNATIVE: Verify by hand on the run branch\n") + manual;
|
|
1698
|
+
}
|
|
1699
|
+
if (input.verificationPolicy === "blocking") {
|
|
1700
|
+
return "Why did this happen?\n Verification failed in blocking mode (--verification-policy blocking).\n In blocking mode, failing candidates are disqualified and the workflow gate stays closed.\n\nRECOMMENDED: Re-run with non-blocking verification (--verification-policy findings)\n This allows repair to fix issues automatically:\n\n" + rerunSteps(input) + "\nALTERNATIVE: Manually fix verification failures\n" + manual;
|
|
1701
|
+
}
|
|
1702
|
+
return "Why did this happen?\n Verification failed during execution and the workflow was recorded as not mergeable.\n Repair was not sufficient to clear the gate.\n\nRECOMMENDED: Re-run with automatic repair enabled (default behavior)\n This allows the system to automatically fix verification issues:\n\n" + rerunSteps(input) + "\nALTERNATIVE: Manually fix verification failures\n" + manual;
|
|
1703
|
+
}
|
|
1704
|
+
function stageIdsWithIntegrationWork(stageResults) {
|
|
1705
|
+
if (!Array.isArray(stageResults)) return /* @__PURE__ */ new Set();
|
|
1706
|
+
return new Set(
|
|
1707
|
+
stageResults.flatMap((stage) => {
|
|
1708
|
+
const stageId = asRecord(stage)?.stageId;
|
|
1709
|
+
return typeof stageId === "string" && preservedStageWork([stage]).length === 0 ? [stageId] : [];
|
|
1710
|
+
})
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
function overrideSection(input) {
|
|
1714
|
+
const command = ` ${mergeResultCommand(input)} --skip-verify
|
|
1715
|
+
`;
|
|
1716
|
+
if (preservedStageWork(input.stageResults).length === 0) {
|
|
1717
|
+
return "OVERRIDE (not recommended): Skip verification gate\n Only use this if you understand the risks:\n\n" + command + "\n";
|
|
1718
|
+
}
|
|
1719
|
+
const withIntegrationWork = stageIdsWithIntegrationWork(input.stageResults);
|
|
1720
|
+
const stageOnly = !input.blockingStageIds.some((stageId) => withIntegrationWork.has(stageId));
|
|
1721
|
+
const cannotLand = ` --skip-verify applies only ${input.integrationBranch}, which does not hold the
|
|
1722
|
+
preserved stage work, so it cannot land that work; use the steps above for it.
|
|
1723
|
+
`;
|
|
1724
|
+
if (stageOnly) {
|
|
1725
|
+
return `NOTE: Skipping the verification gate does not help here
|
|
1726
|
+
${cannotLand}
|
|
1727
|
+
`;
|
|
1728
|
+
}
|
|
1729
|
+
return "OVERRIDE (not recommended): Skip verification gate\n" + cannotLand + " For the work the integration branch does hold, and only if you understand the risks:\n\n" + command + "\n";
|
|
1730
|
+
}
|
|
1731
|
+
function buildMergeGateRefusalMessage(input) {
|
|
1732
|
+
const { workflowId } = input;
|
|
1733
|
+
const unavailable = failedOnlyOnUnavailableVerification(input.stageResults);
|
|
1734
|
+
const lines = input.blockingStageIds.slice(0, 6).map((stageId) => `- ${stageId}`).join("\n");
|
|
1735
|
+
const preserved = preservedStageBranches(input.stageResults);
|
|
1736
|
+
return "Cannot apply workflow result: " + displayedGateReason(input) + "\n" + (lines ? `
|
|
1737
|
+
Blocking stages:
|
|
1738
|
+
${lines}
|
|
1739
|
+
` : "") + (preserved.length > 0 ? `
|
|
1740
|
+
Preserved stage work: ${preserved.map((b) => `branch ${b}`).join(", ")}
|
|
1741
|
+
` : "") + "\n" + whySection(input, unavailable) + (input.blockingStageIds.some((stageId) => input.stageProgress?.[stageId]?.reviewGate) ? `REVIEWER FINDINGS: Dispose a halted review gate with a recorded decision
|
|
1742
|
+
Reject a false-positive finding or accept a real one; the gate re-runs and
|
|
1743
|
+
merge-result becomes eligible without skipping verification:
|
|
1744
|
+
|
|
1745
|
+
build run flywheel gate ${workflowId} <gate-stage-id> --reject <finding-id> --reason "<why>"
|
|
1746
|
+
build run flywheel gate ${workflowId} <gate-stage-id> --accept <finding-id> --reason "<why>"
|
|
1747
|
+
# <gate-stage-id> is listed under "Blocking stages" above
|
|
1748
|
+
|
|
1749
|
+
` : "") + overrideSection(input) + `Learn more: Run 'build plan sprint run --help' to see all verification and repair options.`;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/automation/flywheel-merge.ts
|
|
1753
|
+
function commandUsesVitest(command) {
|
|
1754
|
+
return /(?:^|\s)(?:(?:npx|pnpm exec|pnpx|yarn|bunx)\s+)?vitest(?:\s|$)/.test(command);
|
|
1755
|
+
}
|
|
1756
|
+
function resolvePostMergeFocusedTestCommand(params) {
|
|
1757
|
+
const { testFile, capabilities, existingCommands } = params;
|
|
1758
|
+
const hasVitest = existingCommands.some(commandUsesVitest) || targetSignalsVitest(capabilities.root);
|
|
1759
|
+
if (hasVitest && isNodeVerificationPath(testFile) && isRunnableProjectTestFile(testFile)) {
|
|
1760
|
+
const run = resolverVitestRun(capabilities, [testFile]);
|
|
1761
|
+
if (run !== "whole-suite" && Object.keys(run?.env ?? {}).length === 0) {
|
|
1762
|
+
const args = run?.args ?? ["--no-install", "vitest", "run", testFile];
|
|
1763
|
+
const operands = args.map((arg) => arg === testFile ? quotePosixShellArg(arg) : arg);
|
|
1764
|
+
return { kind: "focused", command: ["npx", ...operands].join(" ") };
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const guidance = buildProjectVerificationGuidance(capabilities.root, [testFile], {
|
|
1768
|
+
capabilities
|
|
1769
|
+
});
|
|
1770
|
+
const wholeSuite = guidance.commands.filter((entry) => entry.kind === "test").map((entry) => entry.command);
|
|
1771
|
+
if (wholeSuite.length > 0 && verificationCommandsCoverChangedPaths(capabilities, wholeSuite, guidance.relevantLanguages, [
|
|
1772
|
+
testFile
|
|
1773
|
+
])) {
|
|
1774
|
+
return { kind: "whole-suite", commands: wholeSuite };
|
|
1775
|
+
}
|
|
1776
|
+
return { kind: "unavailable" };
|
|
1777
|
+
}
|
|
1778
|
+
async function applyTriageDecisions(workflowId, options = {}) {
|
|
1779
|
+
const triage = await loadTriage(workflowId);
|
|
1780
|
+
if (!triage) return { followUpFailures: [] };
|
|
1781
|
+
const manifest = await loadManifest(workflowId);
|
|
1782
|
+
const cwd = options.cwd ?? getProjectRoot();
|
|
1783
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
1784
|
+
if (options.expectedBranch && currentBranch !== options.expectedBranch) {
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
`Triage apply target branch changed: expected ${options.expectedBranch}, found ${currentBranch}`
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
assertBranchAllowsCodeApply({
|
|
1790
|
+
allowMain: options.allowMain,
|
|
1791
|
+
allowProtected: options.allowProtected,
|
|
1792
|
+
command: `run flywheel merge-result ${workflowId}`,
|
|
1793
|
+
cwd,
|
|
1794
|
+
currentBranch
|
|
1795
|
+
});
|
|
1796
|
+
const sprintId = manifest.sprintId;
|
|
1797
|
+
const followUpFailures = [];
|
|
1798
|
+
for (const [stageId, stageTriage] of Object.entries(triage.stages)) {
|
|
1799
|
+
const decisions = stageTriage.decisions;
|
|
1800
|
+
const toDiscard = [];
|
|
1801
|
+
const followUps = [];
|
|
1802
|
+
for (const decision of decisions) {
|
|
1803
|
+
if (decision.outcome === "accept") {
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
toDiscard.push(decision.file);
|
|
1807
|
+
if (decision.outcome === "follow-up") {
|
|
1808
|
+
followUps.push(decision.file);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (toDiscard.length > 0) {
|
|
1812
|
+
console.log(
|
|
1813
|
+
chalk4.cyan(
|
|
1814
|
+
` Applying triage for stage ${stageId}: discarding ${toDiscard.length} file(s)...`
|
|
1815
|
+
)
|
|
1816
|
+
);
|
|
1817
|
+
await execa2("git", ["reset", "HEAD", "--", ...toDiscard], {
|
|
1818
|
+
cwd,
|
|
1819
|
+
stdio: "pipe",
|
|
1820
|
+
timeout: 3e4
|
|
1821
|
+
});
|
|
1822
|
+
const tracked = [];
|
|
1823
|
+
const untracked = [];
|
|
1824
|
+
for (const f of toDiscard) {
|
|
1825
|
+
const check = await execa2("git", ["ls-files", "--error-unmatch", "--", f], {
|
|
1826
|
+
cwd,
|
|
1827
|
+
stdio: "pipe",
|
|
1828
|
+
timeout: 1e4,
|
|
1829
|
+
reject: false
|
|
1830
|
+
});
|
|
1831
|
+
if (check.exitCode === 0) {
|
|
1832
|
+
tracked.push(f);
|
|
1833
|
+
} else {
|
|
1834
|
+
untracked.push(f);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (tracked.length > 0) {
|
|
1838
|
+
await execa2("git", ["checkout", "--", ...tracked], {
|
|
1839
|
+
cwd,
|
|
1840
|
+
stdio: "pipe",
|
|
1841
|
+
timeout: 3e4
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
if (untracked.length > 0) {
|
|
1845
|
+
await execa2("git", ["clean", "-f", "--", ...untracked], {
|
|
1846
|
+
cwd,
|
|
1847
|
+
stdio: "pipe",
|
|
1848
|
+
timeout: 3e4
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
console.log(chalk4.dim(` Discarded: ${formatPathList(toDiscard)}`));
|
|
1852
|
+
}
|
|
1853
|
+
if (followUps.length > 0 && sprintId) {
|
|
1854
|
+
console.log(chalk4.cyan(` Creating ${followUps.length} follow-up task(s)...`));
|
|
1855
|
+
for (const file of followUps) {
|
|
1856
|
+
try {
|
|
1857
|
+
const task = await createTask(`Follow-up: Refine drift in ${file}`, sprintId, {
|
|
1858
|
+
priority: "P2",
|
|
1859
|
+
estimatedHours: 2
|
|
1860
|
+
});
|
|
1861
|
+
console.log(chalk4.dim(` - Created task ${task.id} for ${file}`));
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1864
|
+
console.log(
|
|
1865
|
+
chalk4.yellow(` Warning: Failed to create follow-up task for ${file}: ${errMsg}`)
|
|
1866
|
+
);
|
|
1867
|
+
followUpFailures.push(`Follow-up: Refine drift in ${file}: ${errMsg}`);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
} else if (followUps.length > 0 && !sprintId) {
|
|
1871
|
+
console.log(
|
|
1872
|
+
chalk4.yellow(` Warning: ${followUps.length} follow-up(s) skipped: no sprintId in manifest`)
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return { followUpFailures };
|
|
1877
|
+
}
|
|
1878
|
+
function parseGitStatusPorcelain(output) {
|
|
1879
|
+
return parseGitStatus(output).map((entry) => ({
|
|
1880
|
+
status: `${entry.indexStatus}${entry.worktreeStatus}`,
|
|
1881
|
+
path: entry.path,
|
|
1882
|
+
...entry.originalPath ? { originalPath: entry.originalPath } : {}
|
|
1883
|
+
}));
|
|
1884
|
+
}
|
|
1885
|
+
function getStatusEntryPathspecs(entry) {
|
|
1886
|
+
return Array.from(new Set([entry.originalPath, entry.path].filter(Boolean)));
|
|
1887
|
+
}
|
|
1888
|
+
function isStatusEntryInAllowlist(entry, allowlistPrefixes) {
|
|
1889
|
+
return getStatusEntryPathspecs(entry).every(
|
|
1890
|
+
(pathname) => isPathInAllowlist(pathname, allowlistPrefixes)
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
function normalizeDirPrefix(prefix) {
|
|
1894
|
+
const normalized = prefix.trim().replace(/\\/g, "/");
|
|
1895
|
+
if (normalized.length === 0) return normalized;
|
|
1896
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
1897
|
+
}
|
|
1898
|
+
function getPlanningBucketPrefixes(repoRoot) {
|
|
1899
|
+
return getBuckets(repoRoot).map((b) => normalizeDirPrefix(b.path));
|
|
1900
|
+
}
|
|
1901
|
+
function getPlanningDirtyAllowlistPrefixes(repoRoot) {
|
|
1902
|
+
return [
|
|
1903
|
+
...getPlanningBucketPrefixes(repoRoot),
|
|
1904
|
+
...[".build-project/", "planning/"].map(normalizeDirPrefix)
|
|
1905
|
+
];
|
|
1906
|
+
}
|
|
1907
|
+
function isPathInAllowlist(pathname, allowlistPrefixes) {
|
|
1908
|
+
const normalized = pathname.replace(/\\/g, "/");
|
|
1909
|
+
return allowlistPrefixes.some((prefix) => normalized.startsWith(prefix));
|
|
1910
|
+
}
|
|
1911
|
+
async function resolveStashShaByMessage(cwd, message) {
|
|
1912
|
+
const listResult = await execa2("git", ["stash", "list", "--format=%H %gs"], {
|
|
1913
|
+
cwd,
|
|
1914
|
+
stdio: "pipe",
|
|
1915
|
+
timeout: 3e4,
|
|
1916
|
+
reject: false
|
|
1917
|
+
});
|
|
1918
|
+
if ((listResult.exitCode ?? 1) !== 0) return void 0;
|
|
1919
|
+
const matches = listResult.stdout.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.length > 0).map((line) => {
|
|
1920
|
+
const sep = line.indexOf(" ");
|
|
1921
|
+
if (sep < 0) return void 0;
|
|
1922
|
+
return { sha: line.slice(0, sep), subject: line.slice(sep + 1) };
|
|
1923
|
+
}).filter((entry) => entry !== void 0).filter((entry) => entry.subject.endsWith(message) || entry.subject.includes(message));
|
|
1924
|
+
if (matches.length === 1) return matches[0]?.sha;
|
|
1925
|
+
return void 0;
|
|
1926
|
+
}
|
|
1927
|
+
function hasUnstagedTrackedChange(status) {
|
|
1928
|
+
const worktreeStatus = status[1] ?? " ";
|
|
1929
|
+
return worktreeStatus !== " " && worktreeStatus !== "?";
|
|
1930
|
+
}
|
|
1931
|
+
function isTrackedStatusChange(status) {
|
|
1932
|
+
const indexStatus = status[0] ?? " ";
|
|
1933
|
+
const worktreeStatus = status[1] ?? " ";
|
|
1934
|
+
if (indexStatus === "?" || worktreeStatus === "?") return false;
|
|
1935
|
+
return indexStatus !== " " || worktreeStatus !== " ";
|
|
1936
|
+
}
|
|
1937
|
+
function relevantPostVerifyMutations(entries, trackedRoots) {
|
|
1938
|
+
return entries.filter(
|
|
1939
|
+
(entry) => entry.status === "??" || isTrackedStatusChange(entry.status) && hasUnstagedTrackedChange(entry.status)
|
|
1940
|
+
).flatMap(
|
|
1941
|
+
(entry) => getStatusEntryPathspecs(entry).filter(
|
|
1942
|
+
(pathname) => classifyRepoPath(pathname) !== "managed-metadata" && !isWorktreeRunSentinelPath(pathname) && !shouldExcludeStatusPath(pathname, entry.status === "??", trackedRoots)
|
|
1943
|
+
)
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
async function captureProspectiveTreeFingerprint(cwd) {
|
|
1947
|
+
const prospectiveTree = await execGit(["write-tree"], { cwd, timeoutMs: 3e4 });
|
|
1948
|
+
const fingerprint = prospectiveTree.stdout.trim();
|
|
1949
|
+
if (prospectiveTree.exitCode !== 0 || fingerprint.length === 0) {
|
|
1950
|
+
throw new Error("Post-merge verify gate could not fingerprint the prospective commit tree.");
|
|
1951
|
+
}
|
|
1952
|
+
return fingerprint;
|
|
1953
|
+
}
|
|
1954
|
+
async function relevantStatusPaths(cwd) {
|
|
1955
|
+
const status = await execGit(["status", "--porcelain=v1", "--untracked-files=all"], {
|
|
1956
|
+
cwd,
|
|
1957
|
+
timeoutMs: 3e4
|
|
1958
|
+
});
|
|
1959
|
+
if (status.exitCode !== 0) {
|
|
1960
|
+
throw new Error("Post-merge verify gate could not inspect the verified worktree state.");
|
|
1961
|
+
}
|
|
1962
|
+
return relevantPostVerifyMutations(
|
|
1963
|
+
parseGitStatusPorcelain(status.stdout),
|
|
1964
|
+
await readTrackedToolOutputRoots(cwd)
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
async function hashWorktreeFiles(cwd, paths) {
|
|
1968
|
+
if (paths.length === 0) return [];
|
|
1969
|
+
const hashed = await execGit(["hash-object", "--", ...paths], { cwd, timeoutMs: 3e4 });
|
|
1970
|
+
return hashed.exitCode === 0 ? hashed.stdout.trim().split("\n") : [];
|
|
1971
|
+
}
|
|
1972
|
+
async function snapshotPreVerifyUntracked(cwd) {
|
|
1973
|
+
const status = await execGit(["status", "--porcelain=v1", "--untracked-files=all"], {
|
|
1974
|
+
cwd,
|
|
1975
|
+
timeoutMs: 3e4
|
|
1976
|
+
});
|
|
1977
|
+
if (status.exitCode !== 0) return /* @__PURE__ */ new Map();
|
|
1978
|
+
const untracked = relevantPostVerifyMutations(
|
|
1979
|
+
parseGitStatusPorcelain(status.stdout).filter((entry) => entry.status === "??"),
|
|
1980
|
+
await readTrackedToolOutputRoots(cwd)
|
|
1981
|
+
);
|
|
1982
|
+
const hashes = await hashWorktreeFiles(cwd, untracked);
|
|
1983
|
+
return new Map(
|
|
1984
|
+
hashes.length === untracked.length ? untracked.map((p, i) => [p, hashes[i] ?? ""]) : []
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
async function finalizeVerifiedTreeFingerprint(cwd, prospectiveFingerprint, preVerifyUntracked = /* @__PURE__ */ new Map()) {
|
|
1988
|
+
const changed = await relevantStatusPaths(cwd);
|
|
1989
|
+
const carried = changed.filter((pathname) => preVerifyUntracked.has(pathname));
|
|
1990
|
+
const carriedHashes = await hashWorktreeFiles(cwd, carried);
|
|
1991
|
+
const unchanged = new Set(
|
|
1992
|
+
carried.filter((pathname, i) => carriedHashes[i] === preVerifyUntracked.get(pathname))
|
|
1993
|
+
);
|
|
1994
|
+
const verifyMutations = changed.filter((pathname) => !unchanged.has(pathname));
|
|
1995
|
+
if (verifyMutations.length > 0) {
|
|
1996
|
+
throw new Error(
|
|
1997
|
+
`Post-merge verify mutated relevant worktree content: ${formatPathList(verifyMutations)}`
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
const tree = await execGit(["write-tree"], { cwd, timeoutMs: 3e4 });
|
|
2001
|
+
const treeSha = tree.stdout.trim();
|
|
2002
|
+
if (tree.exitCode !== 0 || treeSha.length === 0) {
|
|
2003
|
+
throw new Error("Post-merge verify gate could not fingerprint the verified tree.");
|
|
2004
|
+
}
|
|
2005
|
+
if (treeSha !== prospectiveFingerprint) {
|
|
2006
|
+
throw new Error("Post-merge verify changed the prospective commit tree.");
|
|
2007
|
+
}
|
|
2008
|
+
return treeSha;
|
|
2009
|
+
}
|
|
2010
|
+
async function assertCommittedTreeMatchesFingerprint(cwd, verifiedTreeFingerprint) {
|
|
2011
|
+
const finalHead = await execGit(["rev-parse", "HEAD"], { cwd, timeoutMs: 3e4 });
|
|
2012
|
+
const finalTree = await execGit(["rev-parse", "HEAD^{tree}"], { cwd, timeoutMs: 3e4 });
|
|
2013
|
+
if (finalHead.exitCode !== 0 || finalHead.stdout.trim().length === 0 || finalTree.exitCode !== 0) {
|
|
2014
|
+
throw new Error("Committed HEAD tree does not match the final verified tree.");
|
|
2015
|
+
}
|
|
2016
|
+
if (finalTree.stdout.trim() !== verifiedTreeFingerprint) {
|
|
2017
|
+
const changed = await execGit(
|
|
2018
|
+
["diff", "--name-only", verifiedTreeFingerprint, finalTree.stdout.trim()],
|
|
2019
|
+
{ cwd, timeoutMs: 3e4 }
|
|
2020
|
+
);
|
|
2021
|
+
const paths = changed.stdout.split("\n").map((entry) => entry.trim()).filter(Boolean);
|
|
2022
|
+
if (changed.exitCode !== 0 || paths.length === 0 || paths.some((pathname) => classifyRepoPath(pathname) !== "managed-metadata")) {
|
|
2023
|
+
throw new Error("Committed HEAD tree does not match the final verified tree.");
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return finalHead.stdout.trim();
|
|
2027
|
+
}
|
|
2028
|
+
function buildApplyOnlyProofStatus(verifiedTreeFingerprint, stagedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
2029
|
+
return { stagedAt, verifiedTreeFingerprint };
|
|
2030
|
+
}
|
|
2031
|
+
function getAutomergeDeprecationWarning() {
|
|
2032
|
+
const envMode = process.env[BUILD_AUTOMERGE_MODE.name]?.trim();
|
|
2033
|
+
if (envMode === "label" || envMode === "auto") {
|
|
2034
|
+
return "Auto-merge configuration is deprecated and has no effect; Build creates the PR but never labels or merges it.";
|
|
2035
|
+
}
|
|
2036
|
+
return void 0;
|
|
2037
|
+
}
|
|
2038
|
+
function readArrayField2(obj, key) {
|
|
2039
|
+
const value = obj[key];
|
|
2040
|
+
return Array.isArray(value) ? value : void 0;
|
|
2041
|
+
}
|
|
2042
|
+
function readStringField2(obj, key) {
|
|
2043
|
+
const value = obj[key];
|
|
2044
|
+
return typeof value === "string" ? value : void 0;
|
|
2045
|
+
}
|
|
2046
|
+
function readBooleanField(obj, key) {
|
|
2047
|
+
const value = obj[key];
|
|
2048
|
+
return typeof value === "boolean" ? value : void 0;
|
|
2049
|
+
}
|
|
2050
|
+
function readNumberField(obj, key) {
|
|
2051
|
+
const value = obj[key];
|
|
2052
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2053
|
+
}
|
|
2054
|
+
function readStringArrayField(obj, key) {
|
|
2055
|
+
const value = obj[key];
|
|
2056
|
+
if (!Array.isArray(value)) return void 0;
|
|
2057
|
+
const strings = value.filter((v) => typeof v === "string");
|
|
2058
|
+
return strings.length === value.length ? strings : void 0;
|
|
2059
|
+
}
|
|
2060
|
+
function summarizeWorkflowGateFromVerificationStatuses(statuses, stageResults = []) {
|
|
2061
|
+
if (statuses.length === 0) {
|
|
2062
|
+
return {
|
|
2063
|
+
passed: false,
|
|
2064
|
+
source: "execution",
|
|
2065
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2066
|
+
reason: "Workflow verification evidence missing or empty",
|
|
2067
|
+
blockingStageIds: [],
|
|
2068
|
+
needsRepairCount: 0,
|
|
2069
|
+
verificationFailedCount: 0
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
const blockingStages = statuses.filter(
|
|
2073
|
+
(status) => status.winnerNeedsRepair || !status.verificationPassed
|
|
2074
|
+
);
|
|
2075
|
+
const needsRepairCount = blockingStages.filter((status) => status.winnerNeedsRepair).length;
|
|
2076
|
+
const verificationFailedCount = blockingStages.length - needsRepairCount;
|
|
2077
|
+
const passed = blockingStages.length === 0;
|
|
2078
|
+
const verificationFailedStages = statuses.flatMap(
|
|
2079
|
+
(status, index) => !status.winnerNeedsRepair && !status.verificationPassed ? [stageResults[index]] : []
|
|
2080
|
+
);
|
|
2081
|
+
return {
|
|
2082
|
+
passed,
|
|
2083
|
+
source: "execution",
|
|
2084
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2085
|
+
reason: passed ? "All stage gates passed" : [
|
|
2086
|
+
needsRepairCount > 0 ? `${needsRepairCount} stage(s) need repair` : null,
|
|
2087
|
+
...describeVerificationFailedStages(verificationFailedStages)
|
|
2088
|
+
].filter((part) => typeof part === "string" && part.length > 0).join(", ") || "Workflow gate failed",
|
|
2089
|
+
blockingStageIds: blockingStages.map((status) => status.stageId),
|
|
2090
|
+
needsRepairCount,
|
|
2091
|
+
verificationFailedCount
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
function isCoherentStructuralWorkflowGate(params) {
|
|
2095
|
+
const { passed, blockingStageIds, needsRepairCount, verificationFailedCount } = params;
|
|
2096
|
+
if (!Number.isInteger(needsRepairCount) || !Number.isInteger(verificationFailedCount) || needsRepairCount < 0 || verificationFailedCount < 0) {
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
if (needsRepairCount + verificationFailedCount !== blockingStageIds.length) {
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
if (passed) {
|
|
2103
|
+
return blockingStageIds.length === 0 && needsRepairCount === 0 && verificationFailedCount === 0;
|
|
2104
|
+
}
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
function failClosedWorkflowGateSummary(params) {
|
|
2108
|
+
const blockingStageIds = params.blockingStageIds ?? [];
|
|
2109
|
+
return {
|
|
2110
|
+
passed: false,
|
|
2111
|
+
source: "execution",
|
|
2112
|
+
decidedAt: typeof params.decidedAt === "string" && params.decidedAt.trim().length > 0 ? params.decidedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
2113
|
+
reason: params.reason,
|
|
2114
|
+
blockingStageIds,
|
|
2115
|
+
needsRepairCount: 0,
|
|
2116
|
+
verificationFailedCount: blockingStageIds.length
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function getWorkflowGateSummaryFromResult(parsed) {
|
|
2120
|
+
if (!isRecord(parsed)) return null;
|
|
2121
|
+
if ("workflowGate" in parsed) {
|
|
2122
|
+
const workflowGate = isRecord(parsed.workflowGate) ? parsed.workflowGate : null;
|
|
2123
|
+
if (!workflowGate || typeof workflowGate.passed !== "boolean") {
|
|
2124
|
+
return failClosedWorkflowGateSummary({
|
|
2125
|
+
reason: "Workflow gate evidence is malformed or incomplete"
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
const decidedAt = typeof workflowGate.decidedAt === "string" && workflowGate.decidedAt.trim().length > 0 ? workflowGate.decidedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
2129
|
+
const blockingStageIds = readStringArrayField(workflowGate, "blockingStageIds");
|
|
2130
|
+
const needsRepairCount = typeof workflowGate.needsRepairCount === "number" && Number.isFinite(workflowGate.needsRepairCount) ? workflowGate.needsRepairCount : void 0;
|
|
2131
|
+
const verificationFailedCount = typeof workflowGate.verificationFailedCount === "number" && Number.isFinite(workflowGate.verificationFailedCount) ? workflowGate.verificationFailedCount : void 0;
|
|
2132
|
+
if (blockingStageIds === void 0 || needsRepairCount === void 0 || verificationFailedCount === void 0) {
|
|
2133
|
+
return failClosedWorkflowGateSummary({
|
|
2134
|
+
decidedAt,
|
|
2135
|
+
reason: "Workflow gate evidence is incomplete"
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
if (!isCoherentStructuralWorkflowGate({
|
|
2139
|
+
passed: workflowGate.passed,
|
|
2140
|
+
blockingStageIds,
|
|
2141
|
+
needsRepairCount,
|
|
2142
|
+
verificationFailedCount
|
|
2143
|
+
})) {
|
|
2144
|
+
return failClosedWorkflowGateSummary({
|
|
2145
|
+
decidedAt,
|
|
2146
|
+
reason: "Workflow gate evidence is contradictory",
|
|
2147
|
+
// Preserve declared blockers for diagnostics when they look like strings.
|
|
2148
|
+
blockingStageIds
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
return {
|
|
2152
|
+
passed: workflowGate.passed,
|
|
2153
|
+
source: "execution",
|
|
2154
|
+
decidedAt,
|
|
2155
|
+
reason: typeof workflowGate.reason === "string" && workflowGate.reason.trim().length > 0 ? workflowGate.reason : workflowGate.passed ? "All stage gates passed" : "Workflow gate failed",
|
|
2156
|
+
blockingStageIds,
|
|
2157
|
+
needsRepairCount,
|
|
2158
|
+
verificationFailedCount
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
const stageStatuses = getWorkflowVerificationGateStatusFromResult(parsed);
|
|
2162
|
+
return stageStatuses ? summarizeWorkflowGateFromVerificationStatuses(
|
|
2163
|
+
stageStatuses,
|
|
2164
|
+
readArrayField2(parsed, "stageResults")
|
|
2165
|
+
) : null;
|
|
2166
|
+
}
|
|
2167
|
+
function getStagesWithDriftFromWorkflowResult(parsed) {
|
|
2168
|
+
if (!isRecord(parsed)) return null;
|
|
2169
|
+
const stageResults = readArrayField2(parsed, "stageResults");
|
|
2170
|
+
if (!stageResults) return null;
|
|
2171
|
+
const stagesWithDrift = [];
|
|
2172
|
+
for (const stageResult of stageResults) {
|
|
2173
|
+
if (!isRecord(stageResult)) continue;
|
|
2174
|
+
const drift = stageResult.driftContext;
|
|
2175
|
+
if (!isRecord(drift)) continue;
|
|
2176
|
+
const violations = readStringArrayField(drift, "violations");
|
|
2177
|
+
if (!violations || violations.length === 0) continue;
|
|
2178
|
+
stagesWithDrift.push({
|
|
2179
|
+
stageId: readStringField2(stageResult, "stageId") ?? "(unknown)",
|
|
2180
|
+
violations,
|
|
2181
|
+
mode: readStringField2(drift, "mode") ?? "warn"
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
return stagesWithDrift;
|
|
2185
|
+
}
|
|
2186
|
+
async function listStagedFiles(cwd, options = {}) {
|
|
2187
|
+
const { stdout } = await execa2(
|
|
2188
|
+
"git",
|
|
2189
|
+
[
|
|
2190
|
+
"diff",
|
|
2191
|
+
"--cached",
|
|
2192
|
+
"--name-only",
|
|
2193
|
+
"-z",
|
|
2194
|
+
...options.includeDeletions ? [] : ["--diff-filter=d"]
|
|
2195
|
+
],
|
|
2196
|
+
{
|
|
2197
|
+
cwd,
|
|
2198
|
+
stdio: "pipe",
|
|
2199
|
+
timeout: 3e4
|
|
2200
|
+
}
|
|
2201
|
+
);
|
|
2202
|
+
return stdout.split("\0").filter(Boolean);
|
|
2203
|
+
}
|
|
2204
|
+
async function runNormalization(invocation, cwd) {
|
|
2205
|
+
const env = createVerificationCommandEnv(process.env, cwd);
|
|
2206
|
+
const sandboxed = wrapWithSandbox({
|
|
2207
|
+
workingDir: cwd,
|
|
2208
|
+
command: invocation.command,
|
|
2209
|
+
args: invocation.args,
|
|
2210
|
+
providerName: "project-verification",
|
|
2211
|
+
env,
|
|
2212
|
+
allowGitWrites: false,
|
|
2213
|
+
providerId: "project-verification"
|
|
2214
|
+
});
|
|
2215
|
+
const result = await execa2(sandboxed.command, sandboxed.args, {
|
|
2216
|
+
cwd,
|
|
2217
|
+
env,
|
|
2218
|
+
extendEnv: false,
|
|
2219
|
+
stdio: "pipe",
|
|
2220
|
+
timeout: 3e5,
|
|
2221
|
+
reject: false,
|
|
2222
|
+
shell: false
|
|
2223
|
+
});
|
|
2224
|
+
return {
|
|
2225
|
+
ok: result.exitCode === 0,
|
|
2226
|
+
stdout: result.stdout ?? "",
|
|
2227
|
+
stderr: result.stderr ?? ""
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
function formatNormalizationFailureSummary(result) {
|
|
2231
|
+
const combined = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
2232
|
+
const lines = combined.split("\n");
|
|
2233
|
+
const clipped = lines.slice(0, 60).join("\n");
|
|
2234
|
+
const suffix = lines.length > 60 ? `
|
|
2235
|
+
... (${lines.length - 60} more lines)` : "";
|
|
2236
|
+
return `${clipped}${suffix}`.trim();
|
|
2237
|
+
}
|
|
2238
|
+
function getWorkflowVerificationGateStatusFromResult(parsed) {
|
|
2239
|
+
if (!isRecord(parsed)) return null;
|
|
2240
|
+
const stageResults = readArrayField2(parsed, "stageResults");
|
|
2241
|
+
if (!stageResults || stageResults.length === 0) return null;
|
|
2242
|
+
const statuses = [];
|
|
2243
|
+
for (const r of stageResults) {
|
|
2244
|
+
if (!isRecord(r)) {
|
|
2245
|
+
statuses.push({
|
|
2246
|
+
stageId: "(malformed)",
|
|
2247
|
+
winnerNeedsRepair: false,
|
|
2248
|
+
verificationPassed: false
|
|
2249
|
+
});
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
const stageId = readStringField2(r, "stageId") ?? "(unknown)";
|
|
2253
|
+
const winnerNeedsRepair = Boolean(readBooleanField(r, "winnerNeedsRepair"));
|
|
2254
|
+
const mergeGate = isRecord(r.mergeGate) ? r.mergeGate : void 0;
|
|
2255
|
+
const verification = r.verification;
|
|
2256
|
+
const stageType = readStringField2(r, "stageType");
|
|
2257
|
+
const verifyCommands = readArrayField2(r, "verifyCommands");
|
|
2258
|
+
const isVerifyStage = stageType === "verify" || Boolean(verifyCommands);
|
|
2259
|
+
const canonicalMergeGatePassed = isRecord(mergeGate) && typeof mergeGate.passed === "boolean" ? mergeGate.passed : void 0;
|
|
2260
|
+
const effectiveWinnerNeedsRepair = canonicalMergeGatePassed === true ? false : winnerNeedsRepair;
|
|
2261
|
+
const explicitVerificationPassed = isRecord(verification) && typeof verification.passed === "boolean" ? verification.passed : void 0;
|
|
2262
|
+
const validationPassed = readBooleanField(r, "validationPassed");
|
|
2263
|
+
const resolvedVerification = canonicalMergeGatePassed ?? explicitVerificationPassed ?? (isVerifyStage || stageType === "orchestrator" ? validationPassed : void 0);
|
|
2264
|
+
const verificationPassed = resolvedVerification === true;
|
|
2265
|
+
statuses.push({
|
|
2266
|
+
stageId,
|
|
2267
|
+
winnerNeedsRepair: effectiveWinnerNeedsRepair,
|
|
2268
|
+
verificationPassed
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
if (statuses.length === 0) return null;
|
|
2272
|
+
return statuses;
|
|
2273
|
+
}
|
|
2274
|
+
async function loadWorkflowResultForGate(workflowId) {
|
|
2275
|
+
let content;
|
|
2276
|
+
try {
|
|
2277
|
+
const resultPath = getResultPath(workflowId);
|
|
2278
|
+
content = await fs4.readFile(resultPath, "utf-8");
|
|
2279
|
+
} catch {
|
|
2280
|
+
return {
|
|
2281
|
+
kind: "unreadable",
|
|
2282
|
+
summary: failClosedWorkflowGateSummary({
|
|
2283
|
+
reason: "Workflow gate evidence cannot be read"
|
|
2284
|
+
})
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
let parsed;
|
|
2288
|
+
try {
|
|
2289
|
+
parsed = JSON.parse(content);
|
|
2290
|
+
} catch {
|
|
2291
|
+
return {
|
|
2292
|
+
kind: "unreadable",
|
|
2293
|
+
summary: failClosedWorkflowGateSummary({
|
|
2294
|
+
reason: "Workflow gate evidence cannot be parsed"
|
|
2295
|
+
})
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
if (!isRecord(parsed)) {
|
|
2299
|
+
return {
|
|
2300
|
+
kind: "unreadable",
|
|
2301
|
+
summary: failClosedWorkflowGateSummary({
|
|
2302
|
+
reason: "Workflow gate evidence cannot be parsed"
|
|
2303
|
+
})
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
return { kind: "ok", parsed };
|
|
2307
|
+
}
|
|
2308
|
+
async function getWorkflowVerificationGateStatus(workflowId) {
|
|
2309
|
+
const loaded = await loadWorkflowResultForGate(workflowId);
|
|
2310
|
+
if (loaded.kind === "absent") return null;
|
|
2311
|
+
if (loaded.kind === "unreadable") {
|
|
2312
|
+
return [
|
|
2313
|
+
{
|
|
2314
|
+
stageId: "(unreadable)",
|
|
2315
|
+
winnerNeedsRepair: false,
|
|
2316
|
+
verificationPassed: false
|
|
2317
|
+
}
|
|
2318
|
+
];
|
|
2319
|
+
}
|
|
2320
|
+
return getWorkflowVerificationGateStatusFromResult(loaded.parsed);
|
|
2321
|
+
}
|
|
2322
|
+
async function getWorkflowGateSummary(workflowId) {
|
|
2323
|
+
const loaded = await loadWorkflowResultForGate(workflowId);
|
|
2324
|
+
if (loaded.kind === "absent") return null;
|
|
2325
|
+
if (loaded.kind === "unreadable") return loaded.summary;
|
|
2326
|
+
return getWorkflowGateSummaryFromResult(loaded.parsed) ?? failClosedWorkflowGateSummary({
|
|
2327
|
+
reason: "Workflow verification evidence missing or empty"
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
async function getMergeGateRefusal(workflowId, manifest, summary, invocation) {
|
|
2331
|
+
const blockingStageIds = summary !== null ? summary.passed === false ? summary.blockingStageIds : [] : (await getWorkflowVerificationGateStatus(workflowId))?.filter((s) => s.winnerNeedsRepair || !s.verificationPassed).map((s) => s.stageId) ?? [];
|
|
2332
|
+
if (summary?.passed !== false && blockingStageIds.length === 0) return null;
|
|
2333
|
+
const loaded = await loadWorkflowResultForGate(workflowId);
|
|
2334
|
+
const activeSprintExists = await getCurrentSprint().then((sprint) => sprint !== null).catch(() => void 0);
|
|
2335
|
+
return buildMergeGateRefusalMessage({
|
|
2336
|
+
...manifest,
|
|
2337
|
+
workflowId,
|
|
2338
|
+
summary,
|
|
2339
|
+
blockingStageIds,
|
|
2340
|
+
stageResults: loaded.kind === "ok" ? loaded.parsed.stageResults : void 0,
|
|
2341
|
+
pr: invocation.pr,
|
|
2342
|
+
baseBranch: invocation.baseBranch,
|
|
2343
|
+
activeSprintExists
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
function collectHistoricalVerifyCommandResults(parsed) {
|
|
2347
|
+
const results = [];
|
|
2348
|
+
const stack = [parsed];
|
|
2349
|
+
while (stack.length > 0) {
|
|
2350
|
+
const current = stack.pop();
|
|
2351
|
+
if (Array.isArray(current)) {
|
|
2352
|
+
stack.push(...current);
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
if (!isRecord(current)) continue;
|
|
2356
|
+
const verifyCommands = readArrayField2(current, "verifyCommands");
|
|
2357
|
+
if (verifyCommands) {
|
|
2358
|
+
for (const entry of verifyCommands) {
|
|
2359
|
+
if (!isRecord(entry)) continue;
|
|
2360
|
+
const command = readStringField2(entry, "command");
|
|
2361
|
+
const exitCode = readNumberField(entry, "exitCode");
|
|
2362
|
+
const durationMs = readNumberField(entry, "durationMs");
|
|
2363
|
+
if (!command || exitCode === void 0 || durationMs === void 0) continue;
|
|
2364
|
+
results.push({
|
|
2365
|
+
command,
|
|
2366
|
+
exitCode,
|
|
2367
|
+
durationMs,
|
|
2368
|
+
outputTail: readStringField2(entry, "outputTail"),
|
|
2369
|
+
stdoutTail: readStringField2(entry, "stdoutTail"),
|
|
2370
|
+
stderrTail: readStringField2(entry, "stderrTail"),
|
|
2371
|
+
baselineErrorCount: readNumberField(entry, "baselineErrorCount"),
|
|
2372
|
+
currentErrorCount: readNumberField(entry, "currentErrorCount"),
|
|
2373
|
+
newErrorCount: readNumberField(entry, "newErrorCount")
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
for (const value of Object.values(current)) {
|
|
2378
|
+
if (value && typeof value === "object") {
|
|
2379
|
+
stack.push(value);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
return results;
|
|
2384
|
+
}
|
|
2385
|
+
function formatBaselineComparison(value) {
|
|
2386
|
+
return value === void 0 ? "n/a" : String(value);
|
|
2387
|
+
}
|
|
2388
|
+
function buildPostMergeBaselineDivergenceMessage(params) {
|
|
2389
|
+
const { command, stageResult, mergeResult, stageBaseSha, mergeBaseSha } = params;
|
|
2390
|
+
if (!stageBaseSha || !mergeBaseSha || stageBaseSha === mergeBaseSha) return null;
|
|
2391
|
+
return [
|
|
2392
|
+
`Historical stage verify passed for "${command}", but post-merge verify failed against a different baseline.`,
|
|
2393
|
+
`Stage baseline counts (base ${stageBaseSha ?? "unknown"}): baseline=${formatBaselineComparison(stageResult.baselineErrorCount)}, current=${formatBaselineComparison(stageResult.currentErrorCount)}, new=${formatBaselineComparison(stageResult.newErrorCount)}.`,
|
|
2394
|
+
`Post-merge baseline counts (base ${mergeBaseSha ?? "unknown"}): baseline=${formatBaselineComparison(mergeResult.baselineErrorCount)}, current=${formatBaselineComparison(mergeResult.currentErrorCount)}, new=${formatBaselineComparison(mergeResult.newErrorCount)}.`
|
|
2395
|
+
].join(" ");
|
|
2396
|
+
}
|
|
2397
|
+
function buildBaselineWorktreeCleanupWarning(params) {
|
|
2398
|
+
const { baselineCwd, projectRoot, stillPresent, registrationState } = params;
|
|
2399
|
+
if (!stillPresent && registrationState === "removed") return void 0;
|
|
2400
|
+
const recovery = `Check it with: git -C ${projectRoot} worktree list --porcelain. Reclaim it with: git -C ${projectRoot} worktree remove --force ${baselineCwd} && git -C ${projectRoot} worktree prune`;
|
|
2401
|
+
if (!stillPresent && registrationState === "indeterminate") {
|
|
2402
|
+
return `Post-merge verify baseline worktree removal could not be verified because its git registration lookup failed (path: ${baselineCwd}). ${recovery}`;
|
|
2403
|
+
}
|
|
2404
|
+
return `Post-merge verify baseline worktree could not be fully removed (path: ${baselineCwd}). ` + recovery;
|
|
2405
|
+
}
|
|
2406
|
+
async function runPostMergeVerifyGate(params) {
|
|
2407
|
+
const {
|
|
2408
|
+
workflowId,
|
|
2409
|
+
manifest,
|
|
2410
|
+
preMergeHeadSha,
|
|
2411
|
+
projectRoot,
|
|
2412
|
+
mergeCwd,
|
|
2413
|
+
ambientUnstagedAllowlistedPaths = [],
|
|
2414
|
+
onVerificationStarted
|
|
2415
|
+
} = params;
|
|
2416
|
+
let orphanedStashWarning;
|
|
2417
|
+
let verifyPassed = false;
|
|
2418
|
+
let verifiedTreeFingerprint;
|
|
2419
|
+
const commands = await resolvePostMergeVerifyCommands({
|
|
2420
|
+
workflowId,
|
|
2421
|
+
manifest,
|
|
2422
|
+
projectRoot,
|
|
2423
|
+
verificationRoot: mergeCwd,
|
|
2424
|
+
onWarning: (message) => renderWarn(chalk4.yellow(` Warning: ${message}`))
|
|
2425
|
+
});
|
|
2426
|
+
const capabilities = detectProjectCapabilities(projectRoot);
|
|
2427
|
+
const pushCommand = (cmd) => {
|
|
2428
|
+
if (!commands.includes(cmd)) {
|
|
2429
|
+
commands.push(cmd);
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
const stagedFiles = await listStagedFiles(mergeCwd);
|
|
2433
|
+
const stagedTestFiles = stagedFiles.filter(isProjectTestPath);
|
|
2434
|
+
let unverifiableStagedTest = false;
|
|
2435
|
+
for (const testFile of stagedTestFiles) {
|
|
2436
|
+
const augmentation = resolvePostMergeFocusedTestCommand({
|
|
2437
|
+
testFile,
|
|
2438
|
+
capabilities,
|
|
2439
|
+
existingCommands: commands
|
|
2440
|
+
});
|
|
2441
|
+
if (augmentation.kind === "focused") {
|
|
2442
|
+
if (!commands.some((c) => c.includes(testFile))) {
|
|
2443
|
+
pushCommand(augmentation.command);
|
|
2444
|
+
}
|
|
2445
|
+
} else if (augmentation.kind === "whole-suite") {
|
|
2446
|
+
for (const cmd of augmentation.commands) pushCommand(cmd);
|
|
2447
|
+
} else {
|
|
2448
|
+
unverifiableStagedTest = true;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
if (unverifiableStagedTest) {
|
|
2452
|
+
console.log(
|
|
2453
|
+
chalk4.yellow(
|
|
2454
|
+
" Warning: staged test file(s) detected but no resolver test command is available for this target language; augmented post-merge verification is unavailable for them."
|
|
2455
|
+
)
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
if (commands.length === 0) {
|
|
2459
|
+
throw new Error(
|
|
2460
|
+
"Post-merge verify gate cannot prove the final tree: no verify commands found."
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
console.log(
|
|
2464
|
+
chalk4.cyan(` Post-merge verify (${commands.length} cmd${commands.length === 1 ? "" : "s"})...`)
|
|
2465
|
+
);
|
|
2466
|
+
const { runVerifyCommands } = await import("./verify-commands-LOHMOFKC.js");
|
|
2467
|
+
let baselineCwd;
|
|
2468
|
+
let historicalVerifyResults = [];
|
|
2469
|
+
let verifyAllowlistStashSha;
|
|
2470
|
+
if (preMergeHeadSha && commands.some((command) => isBaselineComparableTypeScriptCommand(command))) {
|
|
2471
|
+
const baselinePrefix = path4.join(projectRoot, ".build-project", "tmp", "post-merge-verify-");
|
|
2472
|
+
baselineCwd = await fs4.mkdtemp(baselinePrefix);
|
|
2473
|
+
try {
|
|
2474
|
+
const addResult = await execGit(
|
|
2475
|
+
["worktree", "add", "--detach", baselineCwd, preMergeHeadSha],
|
|
2476
|
+
{
|
|
2477
|
+
cwd: projectRoot,
|
|
2478
|
+
timeoutMs: 6e4
|
|
2479
|
+
}
|
|
2480
|
+
);
|
|
2481
|
+
if (addResult.exitCode !== 0) {
|
|
2482
|
+
throw new Error(addResult.stderr.trim() || "git worktree add failed");
|
|
2483
|
+
}
|
|
2484
|
+
} catch (error) {
|
|
2485
|
+
await fs4.rm(baselineCwd, { recursive: true, force: true });
|
|
2486
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2487
|
+
throw new Error(`Failed to create baseline worktree for post-merge verify: ${message}`);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
try {
|
|
2491
|
+
const resultPath = getResultPath(workflowId);
|
|
2492
|
+
const raw = await fs4.readFile(resultPath, "utf-8");
|
|
2493
|
+
historicalVerifyResults = collectHistoricalVerifyCommandResults(JSON.parse(raw));
|
|
2494
|
+
} catch {
|
|
2495
|
+
historicalVerifyResults = [];
|
|
2496
|
+
}
|
|
2497
|
+
const ambientAllowlistedPaths = Array.from(
|
|
2498
|
+
new Set(
|
|
2499
|
+
ambientUnstagedAllowlistedPaths.map((candidate) => candidate.trim()).filter((candidate) => candidate.length > 0)
|
|
2500
|
+
)
|
|
2501
|
+
);
|
|
2502
|
+
const restoreAllowlistStash = async () => {
|
|
2503
|
+
if (verifyAllowlistStashSha) {
|
|
2504
|
+
const applyResult = await execa2("git", ["stash", "apply", verifyAllowlistStashSha], {
|
|
2505
|
+
cwd: mergeCwd,
|
|
2506
|
+
stdio: "pipe",
|
|
2507
|
+
timeout: 6e4,
|
|
2508
|
+
reject: false
|
|
2509
|
+
});
|
|
2510
|
+
if ((applyResult.exitCode ?? 1) === 0) {
|
|
2511
|
+
const listResult = await execa2("git", ["stash", "list", "--format=%gd %H"], {
|
|
2512
|
+
cwd: mergeCwd,
|
|
2513
|
+
stdio: "pipe",
|
|
2514
|
+
timeout: 3e4,
|
|
2515
|
+
reject: false
|
|
2516
|
+
});
|
|
2517
|
+
const matchingRef = listResult.stdout.split("\n").map((line) => line.trim()).find((line) => line.endsWith(` ${verifyAllowlistStashSha}`))?.split(" ")[0];
|
|
2518
|
+
if (matchingRef) {
|
|
2519
|
+
await execa2("git", ["stash", "drop", matchingRef], {
|
|
2520
|
+
cwd: mergeCwd,
|
|
2521
|
+
stdio: "pipe",
|
|
2522
|
+
timeout: 3e4,
|
|
2523
|
+
reject: false
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
} else {
|
|
2527
|
+
orphanedStashWarning = `Post-merge verify allowlist stash ${verifyAllowlistStashSha} could not be reapplied and was preserved (worktree: ${mergeCwd}). Recover it with: git -C ${mergeCwd} stash apply ${verifyAllowlistStashSha}`;
|
|
2528
|
+
console.log(
|
|
2529
|
+
chalk4.yellow(
|
|
2530
|
+
` Warning: Failed to restore post-merge verify allowlist stash (${verifyAllowlistStashSha.slice(0, 7)}); it is preserved.`
|
|
2531
|
+
)
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
const removeBaselineWorktree = async () => {
|
|
2537
|
+
if (!baselineCwd) return;
|
|
2538
|
+
let worktreeRemoved = false;
|
|
2539
|
+
try {
|
|
2540
|
+
const removeResult = await execGit(["worktree", "remove", "--force", baselineCwd], {
|
|
2541
|
+
cwd: projectRoot,
|
|
2542
|
+
timeoutMs: 6e4
|
|
2543
|
+
});
|
|
2544
|
+
worktreeRemoved = removeResult.exitCode === 0;
|
|
2545
|
+
} catch {
|
|
2546
|
+
}
|
|
2547
|
+
try {
|
|
2548
|
+
await fs4.rm(baselineCwd, { recursive: true, force: true });
|
|
2549
|
+
} catch {
|
|
2550
|
+
}
|
|
2551
|
+
try {
|
|
2552
|
+
await pruneGitWorktreeEntries({ cwd: projectRoot, timeoutMs: 1e4, paths: [baselineCwd] });
|
|
2553
|
+
} catch {
|
|
2554
|
+
}
|
|
2555
|
+
let stillPresent = false;
|
|
2556
|
+
try {
|
|
2557
|
+
stillPresent = existsSync(baselineCwd);
|
|
2558
|
+
} catch {
|
|
2559
|
+
stillPresent = false;
|
|
2560
|
+
}
|
|
2561
|
+
let registrationState = worktreeRemoved ? "removed" : "indeterminate";
|
|
2562
|
+
if (!worktreeRemoved) {
|
|
2563
|
+
try {
|
|
2564
|
+
const listResult = await execGit(["worktree", "list", "--porcelain"], {
|
|
2565
|
+
cwd: projectRoot,
|
|
2566
|
+
timeoutMs: 1e4
|
|
2567
|
+
});
|
|
2568
|
+
if (listResult.exitCode === 0) {
|
|
2569
|
+
const resolvedBaseline = path4.resolve(baselineCwd);
|
|
2570
|
+
registrationState = listResult.stdout.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => path4.resolve(line.slice("worktree ".length).trim())).includes(resolvedBaseline) ? "registered" : "removed";
|
|
2571
|
+
}
|
|
2572
|
+
} catch {
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
const residualWarning = buildBaselineWorktreeCleanupWarning({
|
|
2576
|
+
baselineCwd,
|
|
2577
|
+
projectRoot,
|
|
2578
|
+
stillPresent,
|
|
2579
|
+
registrationState
|
|
2580
|
+
});
|
|
2581
|
+
if (residualWarning) {
|
|
2582
|
+
orphanedStashWarning = orphanedStashWarning ? `${orphanedStashWarning} ${residualWarning}` : residualWarning;
|
|
2583
|
+
console.log(
|
|
2584
|
+
chalk4.yellow(
|
|
2585
|
+
" Warning: post-merge verify baseline worktree may not have been fully removed."
|
|
2586
|
+
)
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
try {
|
|
2591
|
+
const existingAmbientAllowlistedPaths = [];
|
|
2592
|
+
for (const candidate of ambientAllowlistedPaths) {
|
|
2593
|
+
const normalizedCandidate = candidate.replace(/\\/g, "/");
|
|
2594
|
+
if (path4.posix.isAbsolute(normalizedCandidate) || normalizedCandidate === ".." || normalizedCandidate.startsWith("../") || normalizedCandidate.includes("/../")) {
|
|
2595
|
+
throw new Error(
|
|
2596
|
+
`Failed to isolate allowlisted unstaged changes before post-merge verify gate. Unsafe pathspec: ${candidate}`
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
const tracked = await execGit(["ls-files", "--error-unmatch", "--", candidate], {
|
|
2600
|
+
cwd: mergeCwd,
|
|
2601
|
+
timeoutMs: 3e4
|
|
2602
|
+
});
|
|
2603
|
+
if (tracked.exitCode === 0) {
|
|
2604
|
+
existingAmbientAllowlistedPaths.push(candidate);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
if (existingAmbientAllowlistedPaths.length > 0) {
|
|
2608
|
+
const stashMessage = `[Build Flywheel] post-merge verify allowlist stash ${workflowId} ${randomUUID2()}`;
|
|
2609
|
+
const stashResult = await execa2(
|
|
2610
|
+
"git",
|
|
2611
|
+
[
|
|
2612
|
+
"stash",
|
|
2613
|
+
"push",
|
|
2614
|
+
"--keep-index",
|
|
2615
|
+
"-m",
|
|
2616
|
+
stashMessage,
|
|
2617
|
+
"--",
|
|
2618
|
+
...existingAmbientAllowlistedPaths
|
|
2619
|
+
],
|
|
2620
|
+
{
|
|
2621
|
+
cwd: mergeCwd,
|
|
2622
|
+
stdio: "pipe",
|
|
2623
|
+
timeout: 6e4,
|
|
2624
|
+
reject: false
|
|
2625
|
+
}
|
|
2626
|
+
);
|
|
2627
|
+
const stashOutput = `${stashResult.stdout ?? ""}
|
|
2628
|
+
${stashResult.stderr ?? ""}`;
|
|
2629
|
+
if ((stashResult.exitCode ?? 1) !== 0) {
|
|
2630
|
+
const stderr = String(stashResult.stderr ?? "").trim();
|
|
2631
|
+
const offendingPath = existingAmbientAllowlistedPaths.join(", ");
|
|
2632
|
+
const details = [
|
|
2633
|
+
stderr ? `git stash stderr: ${stderr}` : void 0,
|
|
2634
|
+
offendingPath ? `offending path: ${offendingPath}` : void 0
|
|
2635
|
+
].filter((part) => Boolean(part)).join("; ");
|
|
2636
|
+
throw new Error(
|
|
2637
|
+
details ? `Failed to isolate allowlisted unstaged changes before post-merge verify gate. ${details}` : "Failed to isolate allowlisted unstaged changes before post-merge verify gate."
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
if (!/No local changes/i.test(stashOutput)) {
|
|
2641
|
+
try {
|
|
2642
|
+
verifyAllowlistStashSha = await resolveStashShaByMessage(mergeCwd, stashMessage);
|
|
2643
|
+
} catch {
|
|
2644
|
+
verifyAllowlistStashSha = void 0;
|
|
2645
|
+
}
|
|
2646
|
+
if (!verifyAllowlistStashSha) {
|
|
2647
|
+
orphanedStashWarning = `Post-merge verify allowlist stash was created but its commit SHA could not be resolved (worktree: ${mergeCwd}). Your allowlisted changes are preserved on the stash stack. Recover with: git -C ${mergeCwd} stash list | grep -F ${JSON.stringify(stashMessage)}`;
|
|
2648
|
+
console.log(
|
|
2649
|
+
chalk4.yellow(
|
|
2650
|
+
" Warning: post-merge verify allowlist stash created but unresolved; preserved on the stash stack."
|
|
2651
|
+
)
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
const prospectiveTreeFingerprint = await captureProspectiveTreeFingerprint(mergeCwd);
|
|
2657
|
+
const preVerifyUntracked = await snapshotPreVerifyUntracked(mergeCwd);
|
|
2658
|
+
onVerificationStarted?.();
|
|
2659
|
+
const verifyOutput = await runVerifyCommands({
|
|
2660
|
+
cwd: mergeCwd,
|
|
2661
|
+
commands,
|
|
2662
|
+
baselineCwd,
|
|
2663
|
+
signal: params.signal
|
|
2664
|
+
});
|
|
2665
|
+
if (verifyOutput.passed) {
|
|
2666
|
+
const { summarizeVerifyCommandRun } = await import("./verify-commands-LOHMOFKC.js");
|
|
2667
|
+
console.log(
|
|
2668
|
+
chalk4.green(` ok ${summarizeVerifyCommandRun(verifyOutput.results, commands.length)}`)
|
|
2669
|
+
);
|
|
2670
|
+
verifyPassed = true;
|
|
2671
|
+
verifiedTreeFingerprint = await finalizeVerifiedTreeFingerprint(
|
|
2672
|
+
mergeCwd,
|
|
2673
|
+
prospectiveTreeFingerprint,
|
|
2674
|
+
preVerifyUntracked
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
if (!verifyPassed) {
|
|
2678
|
+
const failedCmd = verifyOutput.results.find((r) => r.exitCode !== 0);
|
|
2679
|
+
const failedLabel = failedCmd?.command ?? "(unknown)";
|
|
2680
|
+
const outputSnippet = selectFailureOutputTail(failedCmd ?? {});
|
|
2681
|
+
const matchingStageVerify = failedCmd ? historicalVerifyResults.find(
|
|
2682
|
+
(result) => result.command === failedCmd.command && result.exitCode === 0
|
|
2683
|
+
) : void 0;
|
|
2684
|
+
const unavailable = failedCmd ? classifyPostMergeVerifyUnavailable(failedCmd, mergeCwd) : void 0;
|
|
2685
|
+
const baselineDivergenceMessage = failedCmd && matchingStageVerify && !unavailable ? buildPostMergeBaselineDivergenceMessage({
|
|
2686
|
+
command: failedCmd.command,
|
|
2687
|
+
stageResult: matchingStageVerify,
|
|
2688
|
+
mergeResult: failedCmd,
|
|
2689
|
+
stageBaseSha: manifest.baseSha,
|
|
2690
|
+
mergeBaseSha: preMergeHeadSha
|
|
2691
|
+
}) : null;
|
|
2692
|
+
const driftDetection = detectContractDrift(outputSnippet);
|
|
2693
|
+
const unavailableGuidance = unavailable ? describePostMergeVerifySetup(unavailable) : "";
|
|
2694
|
+
if (unavailable) {
|
|
2695
|
+
console.error(chalk4.red("\n Post-merge verify UNAVAILABLE"));
|
|
2696
|
+
console.error(
|
|
2697
|
+
chalk4.red(
|
|
2698
|
+
` ${failedLabel} could not complete on the merged tree (exit ${failedCmd?.exitCode ?? 1}); the merge stays blocked.`
|
|
2699
|
+
)
|
|
2700
|
+
);
|
|
2701
|
+
console.error(chalk4.yellow(` ${unavailable.reason}`));
|
|
2702
|
+
console.error(chalk4.yellow(` ${unavailableGuidance}`));
|
|
2703
|
+
} else {
|
|
2704
|
+
console.error(chalk4.red("\n Post-merge verify FAILED"));
|
|
2705
|
+
console.error(
|
|
2706
|
+
chalk4.red(" Verify passed on orchestrator-root but failed on squash-merged tree.")
|
|
2707
|
+
);
|
|
2708
|
+
console.error(
|
|
2709
|
+
chalk4.red(` Failed command: ${failedLabel} (exit ${failedCmd?.exitCode ?? 1})`)
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
if (baselineDivergenceMessage) {
|
|
2713
|
+
console.error(chalk4.yellow(` ${baselineDivergenceMessage}`));
|
|
2714
|
+
}
|
|
2715
|
+
if (driftDetection.isDrift) {
|
|
2716
|
+
console.error(
|
|
2717
|
+
chalk4.yellow(
|
|
2718
|
+
`
|
|
2719
|
+
Failure classification: ${CROSS_SUBSTAGE_CONTRACT_DRIFT}` + (driftDetection.symbols.length > 0 ? ` (symbols: ${driftDetection.symbols.slice(0, 5).join(", ")})` : "")
|
|
2720
|
+
)
|
|
2721
|
+
);
|
|
2722
|
+
console.error(
|
|
2723
|
+
chalk4.yellow(
|
|
2724
|
+
" A sibling substage generated tests asserting an incompatible return shape for a shared symbol."
|
|
2725
|
+
)
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
console.error(chalk4.dim(outputSnippet.split("\n").slice(-20).join("\n")));
|
|
2729
|
+
console.error(
|
|
2730
|
+
chalk4.yellow(`
|
|
2731
|
+
Integration branch preserved: ${manifest.integrationBranch}`)
|
|
2732
|
+
);
|
|
2733
|
+
console.error(chalk4.yellow(` Workflow: ${workflowId}`));
|
|
2734
|
+
console.error(
|
|
2735
|
+
chalk4.gray(
|
|
2736
|
+
"\n To retry after fixing: build run flywheel merge-result " + workflowId + "\n To skip this gate: build run flywheel merge-result " + workflowId + " --skip-verify"
|
|
2737
|
+
)
|
|
2738
|
+
);
|
|
2739
|
+
const baseErrorMessage = unavailable ? `Post-merge verify unavailable: ${failedLabel} could not complete (exit ${failedCmd?.exitCode ?? 1}). ${unavailable.reason} ${unavailableGuidance} Integration branch preserved.` : driftDetection.isDrift ? formatContractDriftMessage(driftDetection, failedLabel) : `Post-merge verify failed: ${failedLabel} (exit ${failedCmd?.exitCode ?? 1}). Integration branch preserved for debugging.`;
|
|
2740
|
+
throw new Error(
|
|
2741
|
+
baselineDivergenceMessage ? `${baseErrorMessage} ${baselineDivergenceMessage}` : baseErrorMessage
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
} finally {
|
|
2745
|
+
try {
|
|
2746
|
+
await restoreAllowlistStash();
|
|
2747
|
+
} finally {
|
|
2748
|
+
await removeBaselineWorktree();
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
return { orphanedStashWarning, verifiedTreeFingerprint };
|
|
2752
|
+
}
|
|
2753
|
+
function extractVerifyCommandsFromPlan(planData) {
|
|
2754
|
+
if (!isRecord(planData)) return [];
|
|
2755
|
+
const payload = isRecord(planData.payload) ? planData.payload : planData;
|
|
2756
|
+
const plan = isRecord(payload.plan) ? payload.plan : payload;
|
|
2757
|
+
const subStages = Array.isArray(plan.subStages) ? plan.subStages : [];
|
|
2758
|
+
const commands = [];
|
|
2759
|
+
for (const stage of subStages) {
|
|
2760
|
+
if (!isRecord(stage)) continue;
|
|
2761
|
+
if (stage.type !== "verify") continue;
|
|
2762
|
+
const verify = isRecord(stage.verify) ? stage.verify : null;
|
|
2763
|
+
if (!verify) continue;
|
|
2764
|
+
const cmds = Array.isArray(verify.commands) ? verify.commands : [];
|
|
2765
|
+
for (const cmd of cmds) {
|
|
2766
|
+
if (typeof cmd === "string" && cmd.trim().length > 0 && !commands.includes(cmd)) {
|
|
2767
|
+
commands.push(cmd);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
return commands;
|
|
2772
|
+
}
|
|
2773
|
+
function extractVerifyCommandsFromWorkflow(workflow) {
|
|
2774
|
+
const commands = [];
|
|
2775
|
+
const collect = (stage) => {
|
|
2776
|
+
for (const command of stage.verify?.commands ?? []) {
|
|
2777
|
+
if (command.trim().length > 0 && !commands.includes(command)) {
|
|
2778
|
+
commands.push(command);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
for (const subStage of stage.seededPlan?.subStages ?? []) {
|
|
2782
|
+
collect(subStage);
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
for (const stage of workflow.stages) collect(stage);
|
|
2786
|
+
return commands;
|
|
2787
|
+
}
|
|
2788
|
+
function isExecutionReviewGateCommand(command) {
|
|
2789
|
+
return /\bbuild\s+internal\s+review-gate\b/.test(command);
|
|
2790
|
+
}
|
|
2791
|
+
async function resolvePostMergeVerifyCommands(params) {
|
|
2792
|
+
const { workflowId, manifest, projectRoot, verificationRoot = projectRoot, onWarning } = params;
|
|
2793
|
+
let commands;
|
|
2794
|
+
let generatedWorkflow = false;
|
|
2795
|
+
if (manifest.planArtifactPath) {
|
|
2796
|
+
try {
|
|
2797
|
+
const planPath = resolvePlanArtifactPath(manifest.planArtifactPath, projectRoot);
|
|
2798
|
+
const raw = await fs4.readFile(planPath, "utf-8");
|
|
2799
|
+
commands = extractVerifyCommandsFromPlan(JSON.parse(raw));
|
|
2800
|
+
} catch (error) {
|
|
2801
|
+
throw new Error(
|
|
2802
|
+
`Post-merge verify gate cannot run: unable to read plan artifact ${manifest.planArtifactPath}. Cause: ${error instanceof Error ? error.message : String(error)}`
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
} else {
|
|
2806
|
+
generatedWorkflow = true;
|
|
2807
|
+
const workflowSource = manifest.generatedYamlPath || path4.join(getArtifactDirForRoot(projectRoot, workflowId), "workflow.yaml");
|
|
2808
|
+
try {
|
|
2809
|
+
const workflowPath = path4.resolve(projectRoot, workflowSource);
|
|
2810
|
+
const raw = await fs4.readFile(workflowPath, "utf-8");
|
|
2811
|
+
const parsed = yaml.load(raw);
|
|
2812
|
+
if (isRecord(parsed) && Array.isArray(parsed.stages)) {
|
|
2813
|
+
const explicitCommands = extractVerifyCommandsFromWorkflow(
|
|
2814
|
+
parsed
|
|
2815
|
+
);
|
|
2816
|
+
const rejected = explicitCommands.filter((command) => !isAllowedVerifyCommand(command));
|
|
2817
|
+
if (rejected.length > 0) {
|
|
2818
|
+
throw new Error(
|
|
2819
|
+
"Post-merge verify gate rejected explicit verify command(s): " + rejected.map((command) => JSON.stringify(command)).join(", ")
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
const { WorkflowExecutor } = await import("./flywheel-executor-JWU43KAY.js");
|
|
2824
|
+
const workflow = await new WorkflowExecutor().loadWorkflow(workflowPath);
|
|
2825
|
+
commands = extractVerifyCommandsFromWorkflow(workflow).filter(
|
|
2826
|
+
(command) => !isExecutionReviewGateCommand(command)
|
|
2827
|
+
);
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
throw new Error(
|
|
2830
|
+
`Post-merge verify gate cannot run: unable to load generated workflow ${workflowSource}. Cause: ${error instanceof Error ? error.message : String(error)}`
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
if (!generatedWorkflow) {
|
|
2835
|
+
const partitioned = partitionVerifyCommands(commands);
|
|
2836
|
+
if (partitioned.rejected.length > 0) {
|
|
2837
|
+
onWarning?.(
|
|
2838
|
+
`Post-merge verify gate dropped ${partitioned.rejected.length} plan command(s): ${partitioned.rejected.map(
|
|
2839
|
+
({ command, reason }) => `${JSON.stringify(command)} (${reason.replace(/^Verify command rejected:\s*/i, "")})`
|
|
2840
|
+
).join(", ")}`
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
if (commands.length > 0) return partitioned.accepted;
|
|
2844
|
+
}
|
|
2845
|
+
if (commands.length > 0) {
|
|
2846
|
+
const rejected = commands.filter((command) => !isAllowedVerifyCommand(command));
|
|
2847
|
+
if (rejected.length > 0) {
|
|
2848
|
+
throw new Error(
|
|
2849
|
+
"Post-merge verify gate rejected explicit verify command(s): " + rejected.map((command) => JSON.stringify(command)).join(", ")
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
return commands;
|
|
2853
|
+
}
|
|
2854
|
+
return buildProjectVerificationGuidance(verificationRoot).commands.map(({ command }) => command).filter(isAllowedVerifyCommand);
|
|
2855
|
+
}
|
|
2856
|
+
function flywheelApplyTrailers(workflowId, role) {
|
|
2857
|
+
return `Build-Workflow-Id: ${workflowId}
|
|
2858
|
+
Build-Apply-Role: ${role}`;
|
|
2859
|
+
}
|
|
2860
|
+
function flywheelMetadataCommitMessage(workflowId, style) {
|
|
2861
|
+
return `${flywheelMetadataCommitSubject(style)}
|
|
2862
|
+
|
|
2863
|
+
${flywheelApplyTrailers(workflowId, "metadata")}`;
|
|
2864
|
+
}
|
|
2865
|
+
function flywheelPrTitle(workflowId) {
|
|
2866
|
+
return `[Flywheel] Merge workflow result ${workflowId}`;
|
|
2867
|
+
}
|
|
2868
|
+
async function reconcileAppliedFromGitLog(params) {
|
|
2869
|
+
const { verifiedTreeFingerprint, ...topologyParams } = params;
|
|
2870
|
+
const match = await matchAppliedCommitTopology(topologyParams);
|
|
2871
|
+
if (match.kind !== "adopt" || !verifiedTreeFingerprint) return match;
|
|
2872
|
+
try {
|
|
2873
|
+
await assertCommittedTreeMatchesFingerprint(params.cwd, verifiedTreeFingerprint);
|
|
2874
|
+
} catch (error) {
|
|
2875
|
+
return {
|
|
2876
|
+
kind: "ambiguous",
|
|
2877
|
+
reason: `HEAD ${match.value.sha.slice(0, 12)} carries this workflow's apply trailers but its tree does not match the verified tree fingerprint recorded in the manifest (${error instanceof Error ? error.message : String(error)}). The commit was preserved after a failed committed-tree authority check; inspect it and reset the branch before retrying.`
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
return match;
|
|
2881
|
+
}
|
|
2882
|
+
async function matchAppliedCommitTopology(params) {
|
|
2883
|
+
const { cwd, workflowId, baseSha, integrationBranch, expectedBranch } = params;
|
|
2884
|
+
const legacySubject = legacyFlywheelMergeCommitSubject(workflowId);
|
|
2885
|
+
if (expectedBranch) {
|
|
2886
|
+
const branchResult = await execGit([...CURRENT_BRANCH_REV_PARSE_ARGS], {
|
|
2887
|
+
cwd,
|
|
2888
|
+
timeoutMs: 1e4
|
|
2889
|
+
});
|
|
2890
|
+
if (branchResult.exitCode !== 0) {
|
|
2891
|
+
return {
|
|
2892
|
+
kind: "ambiguous",
|
|
2893
|
+
reason: `Could not resolve current branch while reconciling appliedAt for ${workflowId}`
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
const currentBranch = parseCurrentBranchRef(branchResult.stdout);
|
|
2897
|
+
if (currentBranch !== expectedBranch) {
|
|
2898
|
+
return {
|
|
2899
|
+
kind: "ambiguous",
|
|
2900
|
+
reason: `Applied-commit reconciliation refused: current branch '${currentBranch}' does not match expected apply target '${expectedBranch}'`
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
const logResult = await execGit(["log", "-1", "--format=%H%x1f%s%x1f%cI%x1f%B", "HEAD"], {
|
|
2905
|
+
cwd,
|
|
2906
|
+
timeoutMs: 3e4
|
|
2907
|
+
});
|
|
2908
|
+
if (logResult.exitCode !== 0) {
|
|
2909
|
+
return { kind: "none" };
|
|
2910
|
+
}
|
|
2911
|
+
const [sha, msgSubject, committedAt, ...bodyParts] = logResult.stdout.replace(/\r?\n$/, "").split("");
|
|
2912
|
+
if (!sha || !committedAt?.trim()) return { kind: "none" };
|
|
2913
|
+
const body = bodyParts.join("");
|
|
2914
|
+
const applyRoleFromBody = (value) => {
|
|
2915
|
+
const workflowIds = [];
|
|
2916
|
+
const roles = [];
|
|
2917
|
+
for (const line of value.split(/\r?\n/)) {
|
|
2918
|
+
if (line.startsWith("Build-Workflow-Id: ")) {
|
|
2919
|
+
workflowIds.push(line.slice("Build-Workflow-Id: ".length));
|
|
2920
|
+
} else if (line.startsWith("Build-Apply-Role: ")) {
|
|
2921
|
+
roles.push(line.slice("Build-Apply-Role: ".length));
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
if (workflowIds.length !== 1 || workflowIds[0] !== workflowId || roles.length !== 1) {
|
|
2925
|
+
return void 0;
|
|
2926
|
+
}
|
|
2927
|
+
return roles[0] === "code" || roles[0] === "metadata" ? roles[0] : void 0;
|
|
2928
|
+
};
|
|
2929
|
+
const headRole = applyRoleFromBody(body);
|
|
2930
|
+
const hasApplyTrailerLines = body.split(/\r?\n/).some(
|
|
2931
|
+
(line) => line.startsWith("Build-Workflow-Id: ") || line.startsWith("Build-Apply-Role: ")
|
|
2932
|
+
);
|
|
2933
|
+
const [parentResult, headTreeResult, integrationTreeResult] = await Promise.all([
|
|
2934
|
+
execGit(["rev-parse", "HEAD^"], { cwd, timeoutMs: 1e4 }),
|
|
2935
|
+
execGit(["rev-parse", "HEAD^{tree}"], { cwd, timeoutMs: 1e4 }),
|
|
2936
|
+
execGit(["rev-parse", `${integrationBranch}^{tree}`], { cwd, timeoutMs: 1e4 })
|
|
2937
|
+
]);
|
|
2938
|
+
if (parentResult.exitCode !== 0 || headTreeResult.exitCode !== 0 || integrationTreeResult.exitCode !== 0) {
|
|
2939
|
+
return { kind: "none" };
|
|
2940
|
+
}
|
|
2941
|
+
const parentSha = parentResult.stdout.trim();
|
|
2942
|
+
if (!hasApplyTrailerLines && msgSubject === legacySubject && parentSha === baseSha && headTreeResult.stdout.trim() === integrationTreeResult.stdout.trim()) {
|
|
2943
|
+
return { kind: "adopt", value: { sha, committedAt: committedAt.trim() } };
|
|
2944
|
+
}
|
|
2945
|
+
if (headRole === "code" && isFlywheelMergeCommitSubject(msgSubject ?? "", workflowId) && parentSha === baseSha) {
|
|
2946
|
+
return { kind: "adopt", value: { sha, committedAt: committedAt.trim(), role: "code" } };
|
|
2947
|
+
}
|
|
2948
|
+
if (headRole !== "metadata" || !isFlywheelMetadataCommitSubject(msgSubject ?? ""))
|
|
2949
|
+
return { kind: "none" };
|
|
2950
|
+
if (parentSha === baseSha) {
|
|
2951
|
+
return { kind: "adopt", value: { sha, committedAt: committedAt.trim() } };
|
|
2952
|
+
}
|
|
2953
|
+
const parentDetails = await execGit(["show", "-s", "--format=%P%x1f%s%x1f%B", parentSha], {
|
|
2954
|
+
cwd,
|
|
2955
|
+
timeoutMs: 1e4
|
|
2956
|
+
});
|
|
2957
|
+
if (parentDetails.exitCode !== 0) return { kind: "none" };
|
|
2958
|
+
const [grandparents, parentSubject, ...parentBodyParts] = parentDetails.stdout.replace(/\r?\n$/, "").split("");
|
|
2959
|
+
const parentBody = parentBodyParts.join("");
|
|
2960
|
+
if (grandparents?.trim() !== baseSha || !isFlywheelMergeCommitSubject(parentSubject ?? "", workflowId) || applyRoleFromBody(parentBody) !== "code") {
|
|
2961
|
+
return { kind: "none" };
|
|
2962
|
+
}
|
|
2963
|
+
return { kind: "adopt", value: { sha, committedAt: committedAt.trim() } };
|
|
2964
|
+
}
|
|
2965
|
+
async function reconcileApplyOnlyStagedEvidence(params) {
|
|
2966
|
+
const { cwd, baseSha, integrationBranch } = params;
|
|
2967
|
+
const headResult = await execGit(["rev-parse", "HEAD"], { cwd, timeoutMs: 1e4 });
|
|
2968
|
+
if (headResult.exitCode !== 0 || headResult.stdout.trim() !== baseSha) {
|
|
2969
|
+
return { kind: "none" };
|
|
2970
|
+
}
|
|
2971
|
+
const stagedQuiet = await execGit(["diff", "--cached", "--quiet"], {
|
|
2972
|
+
cwd,
|
|
2973
|
+
timeoutMs: 3e4
|
|
2974
|
+
});
|
|
2975
|
+
if (stagedQuiet.exitCode === 0) {
|
|
2976
|
+
return { kind: "none" };
|
|
2977
|
+
}
|
|
2978
|
+
if (stagedQuiet.exitCode !== 1) {
|
|
2979
|
+
return {
|
|
2980
|
+
kind: "ambiguous",
|
|
2981
|
+
reason: `Could not inspect staged apply-only evidence (git exit ${stagedQuiet.exitCode})`
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
const indexTree = await execGit(["write-tree"], { cwd, timeoutMs: 3e4 });
|
|
2985
|
+
const integTree = await execGit(["rev-parse", `${integrationBranch}^{tree}`], {
|
|
2986
|
+
cwd,
|
|
2987
|
+
timeoutMs: 1e4
|
|
2988
|
+
});
|
|
2989
|
+
if (indexTree.exitCode !== 0 || integTree.exitCode !== 0) {
|
|
2990
|
+
return { kind: "none" };
|
|
2991
|
+
}
|
|
2992
|
+
const indexSha = indexTree.stdout.trim();
|
|
2993
|
+
const integSha = integTree.stdout.trim();
|
|
2994
|
+
if (!indexSha || !integSha) return { kind: "none" };
|
|
2995
|
+
if (indexSha === integSha) {
|
|
2996
|
+
return { kind: "adopt", value: { stagedTree: indexSha } };
|
|
2997
|
+
}
|
|
2998
|
+
return { kind: "none" };
|
|
2999
|
+
}
|
|
3000
|
+
async function reconcilePrFromGh(params) {
|
|
3001
|
+
const { cwd, workflowId, headBranch, baseBranch } = params;
|
|
3002
|
+
const expectedTitle = flywheelPrTitle(workflowId);
|
|
3003
|
+
const localTipResult = await execGit(["rev-parse", headBranch], { cwd, timeoutMs: 1e4 });
|
|
3004
|
+
if (localTipResult.exitCode !== 0 || !localTipResult.stdout.trim()) return { kind: "none" };
|
|
3005
|
+
const localHeadOid = localTipResult.stdout.trim();
|
|
3006
|
+
let stdout = "";
|
|
3007
|
+
try {
|
|
3008
|
+
const listResult = await execa2(
|
|
3009
|
+
"gh",
|
|
3010
|
+
[
|
|
3011
|
+
"pr",
|
|
3012
|
+
"list",
|
|
3013
|
+
"--head",
|
|
3014
|
+
headBranch,
|
|
3015
|
+
"--state",
|
|
3016
|
+
"open",
|
|
3017
|
+
"--json",
|
|
3018
|
+
"url,number,title,headRefName,headRefOid,baseRefName",
|
|
3019
|
+
"--limit",
|
|
3020
|
+
"20"
|
|
3021
|
+
],
|
|
3022
|
+
{
|
|
3023
|
+
cwd,
|
|
3024
|
+
stdio: "pipe",
|
|
3025
|
+
timeout: 6e4,
|
|
3026
|
+
reject: false
|
|
3027
|
+
}
|
|
3028
|
+
);
|
|
3029
|
+
if ((listResult.exitCode ?? 1) !== 0) {
|
|
3030
|
+
return { kind: "none" };
|
|
3031
|
+
}
|
|
3032
|
+
stdout = typeof listResult.stdout === "string" ? listResult.stdout : String(listResult.stdout ?? "");
|
|
3033
|
+
} catch {
|
|
3034
|
+
return { kind: "none" };
|
|
3035
|
+
}
|
|
3036
|
+
let parsed;
|
|
3037
|
+
try {
|
|
3038
|
+
parsed = JSON.parse(stdout);
|
|
3039
|
+
} catch {
|
|
3040
|
+
return { kind: "none" };
|
|
3041
|
+
}
|
|
3042
|
+
if (!Array.isArray(parsed)) return { kind: "none" };
|
|
3043
|
+
const candidates = [];
|
|
3044
|
+
for (const entry of parsed) {
|
|
3045
|
+
if (!isRecord(entry)) continue;
|
|
3046
|
+
const headRef = typeof entry.headRefName === "string" ? entry.headRefName : void 0;
|
|
3047
|
+
const baseRef = typeof entry.baseRefName === "string" ? entry.baseRefName : void 0;
|
|
3048
|
+
const headRefOid = typeof entry.headRefOid === "string" ? entry.headRefOid : void 0;
|
|
3049
|
+
const title = typeof entry.title === "string" ? entry.title : void 0;
|
|
3050
|
+
const url = typeof entry.url === "string" ? entry.url : void 0;
|
|
3051
|
+
const number = typeof entry.number === "number" ? entry.number : void 0;
|
|
3052
|
+
if (!headRef || !headRefOid || !baseRef || !title || !url || number === void 0 || !Number.isFinite(number))
|
|
3053
|
+
continue;
|
|
3054
|
+
if (headRef !== headBranch) continue;
|
|
3055
|
+
if (headRefOid !== localHeadOid) continue;
|
|
3056
|
+
if (baseRef !== baseBranch) continue;
|
|
3057
|
+
if (title !== expectedTitle) continue;
|
|
3058
|
+
candidates.push({
|
|
3059
|
+
prUrl: url,
|
|
3060
|
+
prNumber: number,
|
|
3061
|
+
prHeadSha: headRefOid
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
if (candidates.length === 0) return { kind: "none" };
|
|
3065
|
+
if (candidates.length > 1) {
|
|
3066
|
+
return {
|
|
3067
|
+
kind: "ambiguous",
|
|
3068
|
+
reason: `Found ${candidates.length} exact open PRs for head '${headBranch}' base '${baseBranch}'; refusing to adopt prUrl under ambiguity`
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
const chosen = candidates[0];
|
|
3072
|
+
return { kind: "adopt", value: chosen };
|
|
3073
|
+
}
|
|
3074
|
+
async function readPrHeadAndState(cwd, prUrl) {
|
|
3075
|
+
const result = await execa2("gh", ["pr", "view", prUrl, "--json", "headRefOid,state"], {
|
|
3076
|
+
cwd,
|
|
3077
|
+
stdio: "pipe",
|
|
3078
|
+
timeout: 6e4,
|
|
3079
|
+
reject: false
|
|
3080
|
+
});
|
|
3081
|
+
if ((result.exitCode ?? 0) !== 0) {
|
|
3082
|
+
throw new Error("Failed to read PR head OID after PR creation");
|
|
3083
|
+
}
|
|
3084
|
+
const parsed = JSON.parse(
|
|
3085
|
+
typeof result.stdout === "string" ? result.stdout : String(result.stdout ?? "")
|
|
3086
|
+
);
|
|
3087
|
+
if (typeof parsed.headRefOid !== "string" || parsed.headRefOid.length === 0) {
|
|
3088
|
+
throw new Error("PR head OID is missing");
|
|
3089
|
+
}
|
|
3090
|
+
const state = typeof parsed.state === "string" ? parsed.state.toUpperCase() : "OPEN";
|
|
3091
|
+
return { headSha: parsed.headRefOid, state };
|
|
3092
|
+
}
|
|
3093
|
+
async function readPrHeadSha(cwd, prUrl) {
|
|
3094
|
+
return (await readPrHeadAndState(cwd, prUrl)).headSha;
|
|
3095
|
+
}
|
|
3096
|
+
async function mergeWorkflowResult(options) {
|
|
3097
|
+
const { workflowId } = options;
|
|
3098
|
+
const mergeCwd = getProjectRoot();
|
|
3099
|
+
const mergeLockPath = path4.join(getArtifactDir(workflowId), ".merge.lock");
|
|
3100
|
+
const legacyLockPath = path4.join(getArtifactDir(workflowId), ".integrate.lock");
|
|
3101
|
+
const thisPid = String(process.pid);
|
|
3102
|
+
function buildLockPayload() {
|
|
3103
|
+
const payload = {
|
|
3104
|
+
pid: process.pid,
|
|
3105
|
+
host: os.hostname(),
|
|
3106
|
+
lockedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3107
|
+
};
|
|
3108
|
+
return JSON.stringify(payload);
|
|
3109
|
+
}
|
|
3110
|
+
async function readLockPid(lockFilePath) {
|
|
3111
|
+
try {
|
|
3112
|
+
const raw = (await fs4.readFile(lockFilePath, "utf-8")).trim();
|
|
3113
|
+
try {
|
|
3114
|
+
const parsed = JSON.parse(raw);
|
|
3115
|
+
if (parsed !== null && typeof parsed === "object" && "pid" in parsed && typeof parsed.pid === "number") {
|
|
3116
|
+
return String(parsed.pid);
|
|
3117
|
+
}
|
|
3118
|
+
} catch {
|
|
3119
|
+
}
|
|
3120
|
+
return raw;
|
|
3121
|
+
} catch {
|
|
3122
|
+
return "";
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
async function breakLocks() {
|
|
3126
|
+
await fs4.unlink(mergeLockPath).catch(() => {
|
|
3127
|
+
});
|
|
3128
|
+
await fs4.unlink(legacyLockPath).catch(() => {
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3131
|
+
let integrationBranchForRecovery;
|
|
3132
|
+
let preMergeHeadSha;
|
|
3133
|
+
let didCommit = false;
|
|
3134
|
+
let mergeMutationStarted = false;
|
|
3135
|
+
let applyMaterialized = false;
|
|
3136
|
+
let currentBranchForRecovery;
|
|
3137
|
+
let didAutoStash = false;
|
|
3138
|
+
let autoStashSha;
|
|
3139
|
+
let autoStashIncludesStagedPlanning = false;
|
|
3140
|
+
let dirtyPlanningUnstagedPaths = [];
|
|
3141
|
+
let taskSyncFailures;
|
|
3142
|
+
let completionSyncDetails = [];
|
|
3143
|
+
let followUpFailures;
|
|
3144
|
+
const postMergeWarnings = [];
|
|
3145
|
+
let verifiedTreeFingerprint;
|
|
3146
|
+
let adoptedApplyTreeFingerprint;
|
|
3147
|
+
const applyProofTreeFingerprint = () => adoptedApplyTreeFingerprint ?? verifiedTreeFingerprint;
|
|
3148
|
+
let postMergeVerificationStarted = false;
|
|
3149
|
+
let normalizationStarted = false;
|
|
3150
|
+
let appliedCommitSha;
|
|
3151
|
+
let committedTreeAuthorityFailed = false;
|
|
3152
|
+
let uncommittedCommitFailure = false;
|
|
3153
|
+
let codeCommitAwaitingCompletion = false;
|
|
3154
|
+
let codeCommitOnBase = false;
|
|
3155
|
+
const recordCommitMaterialized = async () => {
|
|
3156
|
+
didCommit = true;
|
|
3157
|
+
applyMaterialized = true;
|
|
3158
|
+
try {
|
|
3159
|
+
const currentHead = await execGit(["rev-parse", "HEAD"], {
|
|
3160
|
+
cwd: mergeCwd,
|
|
3161
|
+
timeoutMs: 3e4
|
|
3162
|
+
});
|
|
3163
|
+
if (currentHead.exitCode === 0 && currentHead.stdout.trim().length > 0) {
|
|
3164
|
+
appliedCommitSha = currentHead.stdout.trim();
|
|
3165
|
+
}
|
|
3166
|
+
} catch {
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
const recordedCommits = [];
|
|
3170
|
+
let committedTreeChangeReported = false;
|
|
3171
|
+
const committedTreeFailure = async (fallback) => {
|
|
3172
|
+
committedTreeAuthorityFailed = true;
|
|
3173
|
+
const detail = await describeCommitChangedTree({
|
|
3174
|
+
cwd: mergeCwd,
|
|
3175
|
+
workflowId,
|
|
3176
|
+
commits: recordedCommits,
|
|
3177
|
+
branch: currentBranchForRecovery,
|
|
3178
|
+
restoreSha: preMergeHeadSha
|
|
3179
|
+
}).catch(() => void 0);
|
|
3180
|
+
if (!detail) return fallback;
|
|
3181
|
+
committedTreeChangeReported = true;
|
|
3182
|
+
return new Error(detail);
|
|
3183
|
+
};
|
|
3184
|
+
const assertCommittedTreeAuthority = async (fingerprint) => {
|
|
3185
|
+
try {
|
|
3186
|
+
return await assertCommittedTreeMatchesFingerprint(mergeCwd, fingerprint);
|
|
3187
|
+
} catch (authorityError) {
|
|
3188
|
+
throw await committedTreeFailure(
|
|
3189
|
+
authorityError instanceof Error ? authorityError : new Error(String(authorityError))
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
};
|
|
3193
|
+
let hadPreExistingTrackedPlanningEdits = false;
|
|
3194
|
+
let branchRoleApplyTarget;
|
|
3195
|
+
let mergeLockAcquired = false;
|
|
3196
|
+
let releaseApplyLock;
|
|
3197
|
+
let signalFinalizer;
|
|
3198
|
+
const assertApplyLockOwned = async (allowCancelled = false) => {
|
|
3199
|
+
if (!allowCancelled) signalFinalizer?.signal.throwIfAborted();
|
|
3200
|
+
if (releaseApplyLock) {
|
|
3201
|
+
await releaseApplyLock.assertOwned();
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
const acquireMergeLock = async () => {
|
|
3205
|
+
if (options.dryRun || mergeLockAcquired) return;
|
|
3206
|
+
await fs4.mkdir(getArtifactDir(workflowId), { recursive: true });
|
|
3207
|
+
try {
|
|
3208
|
+
await fs4.access(legacyLockPath);
|
|
3209
|
+
if (!options.force) {
|
|
3210
|
+
const existingPid = await readLockPid(legacyLockPath);
|
|
3211
|
+
throw new Error(
|
|
3212
|
+
`Another merge is already in progress (lock file exists).
|
|
3213
|
+
If you're sure it's stale, retry with --force.
|
|
3214
|
+
Lock: ${legacyLockPath}${existingPid ? ` (pid: ${existingPid})` : ""}`
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
await breakLocks();
|
|
3218
|
+
} catch (error) {
|
|
3219
|
+
const code = error.code;
|
|
3220
|
+
if (code && code !== "ENOENT") throw error;
|
|
3221
|
+
}
|
|
3222
|
+
try {
|
|
3223
|
+
await fs4.writeFile(mergeLockPath, buildLockPayload(), { flag: "wx" });
|
|
3224
|
+
mergeLockAcquired = true;
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
if (error.code !== "EEXIST") throw error;
|
|
3227
|
+
if (!options.force) {
|
|
3228
|
+
const existingPid = await readLockPid(mergeLockPath);
|
|
3229
|
+
throw new Error(
|
|
3230
|
+
`Another merge is already in progress (lock file exists).
|
|
3231
|
+
If you're sure it's stale, retry with --force.
|
|
3232
|
+
Lock: ${mergeLockPath}${existingPid ? ` (pid: ${existingPid})` : ""}`
|
|
3233
|
+
);
|
|
3234
|
+
}
|
|
3235
|
+
await breakLocks();
|
|
3236
|
+
await fs4.writeFile(mergeLockPath, buildLockPayload(), { flag: "wx" });
|
|
3237
|
+
mergeLockAcquired = true;
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
const restoreAutoStash = async () => {
|
|
3241
|
+
if (!didAutoStash) return;
|
|
3242
|
+
didAutoStash = false;
|
|
3243
|
+
const quiet = isQuietOutputRequested();
|
|
3244
|
+
if (!quiet) {
|
|
3245
|
+
console.log(chalk4.cyan(" Restoring auto-stashed changes..."));
|
|
3246
|
+
}
|
|
3247
|
+
if (releaseApplyLock) {
|
|
3248
|
+
try {
|
|
3249
|
+
await releaseApplyLock.assertOwned();
|
|
3250
|
+
} catch (ownershipError) {
|
|
3251
|
+
const detail = ownershipError instanceof Error ? ownershipError.message : String(ownershipError);
|
|
3252
|
+
const warning = describeUnrestoredAutoStash({
|
|
3253
|
+
reason: `checkout apply-lock ownership was lost (${detail})`,
|
|
3254
|
+
stashSha: autoStashSha
|
|
3255
|
+
});
|
|
3256
|
+
console.error(chalk4.yellow(` Warning: ${warning}`));
|
|
3257
|
+
postMergeWarnings.push(warning);
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
if (!autoStashSha) {
|
|
3262
|
+
const warning = describeUnrestoredAutoStash({
|
|
3263
|
+
reason: "the auto-stash commit could not be identified, so it was left stashed rather than popped positionally"
|
|
3264
|
+
});
|
|
3265
|
+
console.error(chalk4.yellow(` Warning: ${warning}`));
|
|
3266
|
+
postMergeWarnings.push(warning);
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
try {
|
|
3270
|
+
const applyResult = await execa2(
|
|
3271
|
+
"git",
|
|
3272
|
+
["stash", "apply", ...autoStashIncludesStagedPlanning ? ["--index"] : [], autoStashSha],
|
|
3273
|
+
{
|
|
3274
|
+
cwd: mergeCwd,
|
|
3275
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
3276
|
+
timeout: 6e4,
|
|
3277
|
+
reject: false
|
|
3278
|
+
}
|
|
3279
|
+
);
|
|
3280
|
+
if ((applyResult.exitCode ?? 1) !== 0) {
|
|
3281
|
+
const warning = describeUnrestoredAutoStash({
|
|
3282
|
+
reason: `git stash apply ${autoStashSha.slice(0, 7)} exited ${applyResult.exitCode ?? "unknown"}`,
|
|
3283
|
+
stashSha: autoStashSha,
|
|
3284
|
+
conflictPaths: await collectUnmergedPaths(mergeCwd)
|
|
3285
|
+
});
|
|
3286
|
+
console.error(chalk4.yellow(` Warning: ${warning}`));
|
|
3287
|
+
postMergeWarnings.push(warning);
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
const listResult = await execa2("git", ["stash", "list", "--format=%gd %H"], {
|
|
3291
|
+
cwd: mergeCwd,
|
|
3292
|
+
stdio: "pipe",
|
|
3293
|
+
timeout: 3e4,
|
|
3294
|
+
reject: false
|
|
3295
|
+
});
|
|
3296
|
+
const matchingRef = listResult.stdout.split("\n").map((line) => line.trim()).find((line) => line.endsWith(` ${autoStashSha}`))?.split(" ")[0];
|
|
3297
|
+
if (matchingRef) {
|
|
3298
|
+
await execa2("git", ["stash", "drop", matchingRef], {
|
|
3299
|
+
cwd: mergeCwd,
|
|
3300
|
+
stdio: "pipe",
|
|
3301
|
+
timeout: 3e4,
|
|
3302
|
+
reject: false
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
} catch (restoreError) {
|
|
3306
|
+
const detail = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
3307
|
+
const warning = describeUnrestoredAutoStash({ reason: detail, stashSha: autoStashSha });
|
|
3308
|
+
console.error(chalk4.yellow(` Warning: ${warning}`));
|
|
3309
|
+
postMergeWarnings.push(warning);
|
|
3310
|
+
}
|
|
3311
|
+
};
|
|
3312
|
+
try {
|
|
3313
|
+
console.log(chalk4.bold(`
|
|
3314
|
+
Merging Workflow Result: ${workflowId}
|
|
3315
|
+
`));
|
|
3316
|
+
const manifest = await loadManifest(workflowId);
|
|
3317
|
+
integrationBranchForRecovery = manifest.integrationBranch;
|
|
3318
|
+
const status = getManifestStatus(manifest);
|
|
3319
|
+
const branchRoleApplyStatus = (action) => {
|
|
3320
|
+
const sourceBranch = integrationBranchForRecovery ?? null;
|
|
3321
|
+
const sourceBranchRole = sourceBranch ? classifyBranchRole(sourceBranch) : null;
|
|
3322
|
+
const applyTargetBranch = branchRoleApplyTarget;
|
|
3323
|
+
const applyTargetBranchRole = applyTargetBranch ? classifyBranchRole(applyTargetBranch, void 0, mergeCwd) : void 0;
|
|
3324
|
+
const branchRoleOverride = applyTargetBranchRole === "protected" ? options.allowProtected ? "--allow-protected" : options.allowMain ? "--allow-main" : null : null;
|
|
3325
|
+
return {
|
|
3326
|
+
sourceBranch,
|
|
3327
|
+
sourceBranchRole,
|
|
3328
|
+
applyTargetBranch,
|
|
3329
|
+
applyTargetBranchRole,
|
|
3330
|
+
branchRoleAction: action ?? (branchRoleOverride ? "overridden" : "product-fix"),
|
|
3331
|
+
branchRoleOverride,
|
|
3332
|
+
branchRoleRefused: false
|
|
3333
|
+
};
|
|
3334
|
+
};
|
|
3335
|
+
const markApplied = async () => {
|
|
3336
|
+
await updateManifestApplyStatus(workflowId, {
|
|
3337
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3338
|
+
appliedBy: await getGitUsername(),
|
|
3339
|
+
verifiedTreeFingerprint: applyProofTreeFingerprint(),
|
|
3340
|
+
appliedCommitSha,
|
|
3341
|
+
...branchRoleApplyStatus()
|
|
3342
|
+
});
|
|
3343
|
+
console.log(chalk4.green(" Manifest updated"));
|
|
3344
|
+
const target = { commitSha: appliedCommitSha, targetBranch: branchRoleApplyTarget };
|
|
3345
|
+
await recordWorkflowCompetitionOutcomes({ workflowId, ...target });
|
|
3346
|
+
};
|
|
3347
|
+
const refuseWithBranchRoleProvenance = async (message) => {
|
|
3348
|
+
console.error(chalk4.red(message));
|
|
3349
|
+
if (!options.dryRun) {
|
|
3350
|
+
try {
|
|
3351
|
+
await updateManifestApplyStatus(workflowId, {
|
|
3352
|
+
sourceBranch: manifest.integrationBranch ?? null,
|
|
3353
|
+
sourceBranchRole: manifest.integrationBranch ? classifyBranchRole(manifest.integrationBranch) : null,
|
|
3354
|
+
applyTargetBranch: branchRoleApplyTarget,
|
|
3355
|
+
applyTargetBranchRole: branchRoleApplyTarget ? classifyBranchRole(branchRoleApplyTarget, void 0, mergeCwd) : void 0,
|
|
3356
|
+
branchRoleAction: "refused",
|
|
3357
|
+
branchRoleRefused: true
|
|
3358
|
+
});
|
|
3359
|
+
} catch {
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
return { success: false, workflowId, error: message };
|
|
3363
|
+
};
|
|
3364
|
+
const assertCurrentBranchStillMatchesApplyTarget = async () => {
|
|
3365
|
+
if (!branchRoleApplyTarget) return;
|
|
3366
|
+
const liveBranch = await getCurrentBranch(mergeCwd);
|
|
3367
|
+
if (liveBranch !== branchRoleApplyTarget) {
|
|
3368
|
+
throw new Error(
|
|
3369
|
+
`Apply target branch changed during merge-result: expected ${branchRoleApplyTarget}, found ${liveBranch}`
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
assertBranchAllowsCodeApply({
|
|
3373
|
+
allowMain: options.allowMain,
|
|
3374
|
+
allowProtected: options.allowProtected,
|
|
3375
|
+
command: `run flywheel merge-result ${workflowId}`,
|
|
3376
|
+
cwd: mergeCwd,
|
|
3377
|
+
currentBranch: liveBranch
|
|
3378
|
+
});
|
|
3379
|
+
};
|
|
3380
|
+
const integrationBranchRole = classifyBranchRole(manifest.integrationBranch);
|
|
3381
|
+
if (integrationBranchRole !== "generated-artifact") {
|
|
3382
|
+
return await refuseWithBranchRoleProvenance(
|
|
3383
|
+
`Refusing to merge workflow result from non-generated branch ${manifest.integrationBranch} (role: ${integrationBranchRole})`
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
await acquireMergeLock();
|
|
3387
|
+
if (status === "applied" && !options.force) {
|
|
3388
|
+
console.log(chalk4.yellow(` Already merged on ${manifest.appliedAt}`));
|
|
3389
|
+
console.log(chalk4.yellow(" Use --force to re-merge"));
|
|
3390
|
+
if (!options.dryRun) {
|
|
3391
|
+
return { success: true, workflowId, alreadyMerged: true };
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
if (!options.dryRun && !options.pr && !manifest.appliedAt) {
|
|
3395
|
+
const appliedReconcile = await reconcileAppliedFromGitLog({
|
|
3396
|
+
cwd: mergeCwd,
|
|
3397
|
+
workflowId,
|
|
3398
|
+
baseSha: manifest.baseSha,
|
|
3399
|
+
integrationBranch: manifest.integrationBranch,
|
|
3400
|
+
expectedBranch: reconciliationApplyTarget(manifest),
|
|
3401
|
+
verifiedTreeFingerprint: manifest.verifiedTreeFingerprint
|
|
3402
|
+
});
|
|
3403
|
+
if (appliedReconcile.kind === "ambiguous") {
|
|
3404
|
+
return {
|
|
3405
|
+
success: false,
|
|
3406
|
+
workflowId,
|
|
3407
|
+
error: `Cannot reconcile applied state: ${appliedReconcile.reason}`
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
if (appliedReconcile.kind === "adopt") {
|
|
3411
|
+
let adopted = appliedReconcile.value;
|
|
3412
|
+
let resumeWarnings = [];
|
|
3413
|
+
if (adopted.role === "code") {
|
|
3414
|
+
if (options.applyOnly) {
|
|
3415
|
+
return {
|
|
3416
|
+
success: false,
|
|
3417
|
+
workflowId,
|
|
3418
|
+
applied: true,
|
|
3419
|
+
error: `The code commit ${adopted.sha.slice(0, 12)} for ${workflowId} already landed; rerun merge-result without --apply-only to finish its task completion.`
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
releaseApplyLock = await acquireFlywheelApplyLock(getProjectRoot());
|
|
3423
|
+
signalFinalizer = registerMergeSignalFinalizer(workflowId);
|
|
3424
|
+
const sprintId = manifest.sourceType === "sprint" ? manifest.sprintId : void 0;
|
|
3425
|
+
const resumeCommitStyle = resolveGitMessageStyle(mergeCwd);
|
|
3426
|
+
const resumed = await finishCompletionAfterCodeCommit({
|
|
3427
|
+
mergeCwd,
|
|
3428
|
+
workflowId,
|
|
3429
|
+
sprintId,
|
|
3430
|
+
transitions: sprintId && (options.taskStatus ?? "apply") === "apply" ? await loadRecordedTaskTransitions(workflowId, sprintId) : null,
|
|
3431
|
+
allowlistPrefixes: getPlanningDirtyAllowlistPrefixes(mergeCwd),
|
|
3432
|
+
bucketPrefixes: getPlanningBucketPrefixes(mergeCwd),
|
|
3433
|
+
metadataCommitMessage: flywheelMetadataCommitMessage(workflowId, resumeCommitStyle),
|
|
3434
|
+
commitStyle: resumeCommitStyle,
|
|
3435
|
+
assertApplyLockOwned,
|
|
3436
|
+
onInfo: (message) => console.log(chalk4.dim(message)),
|
|
3437
|
+
onWarning: (message) => console.log(chalk4.yellow(message))
|
|
3438
|
+
});
|
|
3439
|
+
if (resumed.kind === "refused") {
|
|
3440
|
+
if (resumed.unverifiedCommitTree) {
|
|
3441
|
+
await updateManifestApplyStatus(workflowId, {
|
|
3442
|
+
applyError: resumed.reason,
|
|
3443
|
+
verifiedTreeFingerprint: resumed.unverifiedCommitTree
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
return { success: false, workflowId, applied: true, error: resumed.reason };
|
|
3447
|
+
}
|
|
3448
|
+
if (resumed.metadataCommitSha) {
|
|
3449
|
+
console.log(chalk4.green(" Finished task completion for the existing code commit"));
|
|
3450
|
+
adopted = { sha: resumed.metadataCommitSha, committedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3451
|
+
}
|
|
3452
|
+
resumeWarnings = resumed.warnings;
|
|
3453
|
+
}
|
|
3454
|
+
console.log(
|
|
3455
|
+
chalk4.yellow(
|
|
3456
|
+
` Reconciled appliedAt from existing merge commit ${adopted.sha.slice(0, 7)}`
|
|
3457
|
+
)
|
|
3458
|
+
);
|
|
3459
|
+
try {
|
|
3460
|
+
await updateManifestApplyStatus(workflowId, {
|
|
3461
|
+
appliedAt: adopted.committedAt,
|
|
3462
|
+
appliedBy: await getGitUsername(),
|
|
3463
|
+
appliedCommitSha: adopted.sha,
|
|
3464
|
+
...branchRoleApplyStatus()
|
|
3465
|
+
});
|
|
3466
|
+
} catch {
|
|
3467
|
+
}
|
|
3468
|
+
return {
|
|
3469
|
+
success: true,
|
|
3470
|
+
workflowId,
|
|
3471
|
+
alreadyMerged: true,
|
|
3472
|
+
applied: true,
|
|
3473
|
+
...resumeWarnings.length > 0 ? { degraded: true, warnings: resumeWarnings } : {}
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
if (!options.dryRun && !options.force && options.applyOnly && status === "staged" && manifest.stagedAt) {
|
|
3478
|
+
console.log(chalk4.yellow(` Already staged on ${manifest.stagedAt}`));
|
|
3479
|
+
console.log(chalk4.yellow(" Use --force to re-stage"));
|
|
3480
|
+
return { success: true, workflowId, alreadyStaged: true };
|
|
3481
|
+
}
|
|
3482
|
+
if (!options.dryRun && options.applyOnly && !manifest.stagedAt && !manifest.applyError) {
|
|
3483
|
+
const stagedReconcile = await reconcileApplyOnlyStagedEvidence({
|
|
3484
|
+
cwd: mergeCwd,
|
|
3485
|
+
baseSha: manifest.baseSha,
|
|
3486
|
+
integrationBranch: manifest.integrationBranch
|
|
3487
|
+
});
|
|
3488
|
+
if (stagedReconcile.kind === "adopt") {
|
|
3489
|
+
console.log(
|
|
3490
|
+
chalk4.yellow(
|
|
3491
|
+
` Reconciled stagedAt from exact apply-only index residue (tree ${stagedReconcile.value.stagedTree.slice(0, 7)})`
|
|
3492
|
+
)
|
|
3493
|
+
);
|
|
3494
|
+
try {
|
|
3495
|
+
await updateManifestApplyStatus(workflowId, {
|
|
3496
|
+
...buildApplyOnlyProofStatus(stagedReconcile.value.stagedTree),
|
|
3497
|
+
...branchRoleApplyStatus()
|
|
3498
|
+
});
|
|
3499
|
+
} catch {
|
|
3500
|
+
}
|
|
3501
|
+
return { success: true, workflowId, alreadyStaged: true };
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
if (status === "failed" && manifest.applyError) {
|
|
3505
|
+
console.log(chalk4.red(" Previous merge failed:"));
|
|
3506
|
+
console.log(chalk4.red(` ${manifest.applyError}`));
|
|
3507
|
+
if (!options.dryRun && !options.force) {
|
|
3508
|
+
const correction = await detectFailedApplyCorrection({
|
|
3509
|
+
cwd: mergeCwd,
|
|
3510
|
+
integrationBranch: manifest.integrationBranch,
|
|
3511
|
+
applyFailedIntegrationSha: manifest.applyFailedIntegrationSha
|
|
3512
|
+
});
|
|
3513
|
+
if (!correction.corrected) {
|
|
3514
|
+
console.log(
|
|
3515
|
+
chalk4.yellow(
|
|
3516
|
+
" Use --force to retry the same candidate, or commit a correction on the integration branch and rerun"
|
|
3517
|
+
)
|
|
3518
|
+
);
|
|
3519
|
+
return { success: false, workflowId, error: manifest.applyError };
|
|
3520
|
+
}
|
|
3521
|
+
console.log(
|
|
3522
|
+
chalk4.yellow(
|
|
3523
|
+
` Integration branch moved past the failed candidate; retrying on the corrected candidate (${correction.commits.length} commit(s)):`
|
|
3524
|
+
)
|
|
3525
|
+
);
|
|
3526
|
+
for (const line of correction.commits) {
|
|
3527
|
+
console.log(chalk4.yellow(` ${line}`));
|
|
3528
|
+
}
|
|
3529
|
+
} else if (!options.dryRun) {
|
|
3530
|
+
console.log(chalk4.yellow(" Retrying the same candidate with --force"));
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
if (!options.dryRun && !options.pr) {
|
|
3534
|
+
const { stdout: branchStdout } = await execa2(
|
|
3535
|
+
"git",
|
|
3536
|
+
["rev-parse", "--symbolic-full-name", "HEAD"],
|
|
3537
|
+
{
|
|
3538
|
+
cwd: mergeCwd,
|
|
3539
|
+
stdio: "pipe",
|
|
3540
|
+
timeout: 1e4
|
|
3541
|
+
}
|
|
3542
|
+
);
|
|
3543
|
+
const currentBranch2 = parseCurrentBranchRef(branchStdout);
|
|
3544
|
+
branchRoleApplyTarget = currentBranch2;
|
|
3545
|
+
try {
|
|
3546
|
+
assertBranchAllowsCodeApply({
|
|
3547
|
+
allowMain: options.allowMain,
|
|
3548
|
+
allowProtected: options.allowProtected,
|
|
3549
|
+
command: `run flywheel merge-result ${workflowId}`,
|
|
3550
|
+
cwd: mergeCwd,
|
|
3551
|
+
currentBranch: currentBranch2
|
|
3552
|
+
});
|
|
3553
|
+
} catch (error) {
|
|
3554
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3555
|
+
const validationDryRunHint = message.includes("validation branch") ? `
|
|
3556
|
+
|
|
3557
|
+
Preview safely from this validation branch with:
|
|
3558
|
+
build run flywheel merge-result ${workflowId} --dry-run` : "";
|
|
3559
|
+
return await refuseWithBranchRoleProvenance(`${message}${validationDryRunHint}`);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
const branches = await listFlywheelBranches();
|
|
3563
|
+
const branchExists = branches.some((b) => b.name === manifest.integrationBranch);
|
|
3564
|
+
if (!branchExists) {
|
|
3565
|
+
if (options.dryRun && !options.force) {
|
|
3566
|
+
if (status === "applied") {
|
|
3567
|
+
console.log(chalk4.dim(" Run branch already cleaned up; no merge preview available."));
|
|
3568
|
+
return { success: true, workflowId, alreadyMerged: true };
|
|
3569
|
+
}
|
|
3570
|
+
if (status === "failed" && manifest.applyError) {
|
|
3571
|
+
console.log(chalk4.dim(" Run branch already cleaned up; no failure preview available."));
|
|
3572
|
+
return { success: true, workflowId };
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
throw new Error(
|
|
3576
|
+
`Run branch not found: ${manifest.integrationBranch}
|
|
3577
|
+
The branch may have been deleted. Cannot merge.`
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
const workflowGateSummary = await getWorkflowGateSummary(workflowId);
|
|
3581
|
+
const gateRefusal = options.skipVerify ? null : await getMergeGateRefusal(workflowId, manifest, workflowGateSummary, options);
|
|
3582
|
+
if (options.pr) {
|
|
3583
|
+
if (gateRefusal) return { success: false, workflowId, error: gateRefusal };
|
|
3584
|
+
const warnings2 = [getAutomergeDeprecationWarning()].filter(
|
|
3585
|
+
(warning) => Boolean(warning)
|
|
3586
|
+
);
|
|
3587
|
+
if (options.dryRun) {
|
|
3588
|
+
console.log(chalk4.cyan(" Would create PR for workflow result\n"));
|
|
3589
|
+
console.log(chalk4.dim(" No changes made (--dry-run)"));
|
|
3590
|
+
return { success: true, workflowId };
|
|
3591
|
+
}
|
|
3592
|
+
if (manifest.prUrl && !options.force) {
|
|
3593
|
+
const localHeadResult = await execGit(["rev-parse", manifest.integrationBranch], {
|
|
3594
|
+
cwd: mergeCwd,
|
|
3595
|
+
timeoutMs: 1e4
|
|
3596
|
+
});
|
|
3597
|
+
const localHeadSha = localHeadResult.stdout.trim();
|
|
3598
|
+
if (localHeadResult.exitCode !== 0 || localHeadSha.length === 0) {
|
|
3599
|
+
return {
|
|
3600
|
+
success: false,
|
|
3601
|
+
workflowId,
|
|
3602
|
+
error: "Failed to capture source branch HEAD for recorded PR verification"
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
let prHeadSha;
|
|
3606
|
+
let prState;
|
|
3607
|
+
try {
|
|
3608
|
+
({ headSha: prHeadSha, state: prState } = await readPrHeadAndState(
|
|
3609
|
+
mergeCwd,
|
|
3610
|
+
manifest.prUrl
|
|
3611
|
+
));
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
return {
|
|
3614
|
+
success: false,
|
|
3615
|
+
workflowId,
|
|
3616
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
if (prState === "MERGED") {
|
|
3620
|
+
console.log(chalk4.yellow(` PR already merged: ${manifest.prUrl}`));
|
|
3621
|
+
return {
|
|
3622
|
+
success: true,
|
|
3623
|
+
workflowId,
|
|
3624
|
+
alreadyMerged: true,
|
|
3625
|
+
prUrl: manifest.prUrl,
|
|
3626
|
+
prNumber: manifest.prNumber
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
if (prState === "CLOSED") {
|
|
3630
|
+
return {
|
|
3631
|
+
success: false,
|
|
3632
|
+
workflowId,
|
|
3633
|
+
error: `Recorded PR ${manifest.prUrl} is closed without merging; reopen it or clear the recorded PR (--force creates a new one)`
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
if (prHeadSha !== localHeadSha) {
|
|
3637
|
+
return {
|
|
3638
|
+
success: false,
|
|
3639
|
+
workflowId,
|
|
3640
|
+
error: `PR head mismatch: expected ${localHeadSha}, found ${prHeadSha}`
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
await updateManifestApplyStatus(workflowId, { prHeadSha });
|
|
3644
|
+
console.log(chalk4.yellow(` PR already recorded: ${manifest.prUrl}`));
|
|
3645
|
+
return {
|
|
3646
|
+
success: true,
|
|
3647
|
+
workflowId,
|
|
3648
|
+
prUrl: manifest.prUrl,
|
|
3649
|
+
prNumber: manifest.prNumber,
|
|
3650
|
+
prHeadSha,
|
|
3651
|
+
degraded: warnings2.length > 0,
|
|
3652
|
+
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3655
|
+
console.log(chalk4.cyan(" Creating PR for workflow result..."));
|
|
3656
|
+
const prBaseBranch = options.baseBranch ?? await getCurrentBranch(mergeCwd);
|
|
3657
|
+
branchRoleApplyTarget = prBaseBranch;
|
|
3658
|
+
const prBaseRole = classifyBranchRole(prBaseBranch, void 0, mergeCwd);
|
|
3659
|
+
if (prBaseRole === "detached" || prBaseRole === "local-scratch" || isDisposableBranchRole(prBaseRole)) {
|
|
3660
|
+
return await refuseWithBranchRoleProvenance(
|
|
3661
|
+
`Refusing to create PR targeting ${prBaseRole} branch '${prBaseBranch}'. Choose a product or protected base branch with --base.`
|
|
3662
|
+
);
|
|
3663
|
+
}
|
|
3664
|
+
const sourceHeadResult = await execGit(["rev-parse", manifest.integrationBranch], {
|
|
3665
|
+
cwd: mergeCwd,
|
|
3666
|
+
timeoutMs: 1e4
|
|
3667
|
+
});
|
|
3668
|
+
const sourceHeadSha = sourceHeadResult.stdout.trim();
|
|
3669
|
+
if (sourceHeadResult.exitCode !== 0 || sourceHeadSha.length === 0) {
|
|
3670
|
+
return {
|
|
3671
|
+
success: false,
|
|
3672
|
+
workflowId,
|
|
3673
|
+
error: `Failed to capture source branch HEAD before push: ${(sourceHeadResult.stderr ?? "").trim() || "unknown git error"}`
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
try {
|
|
3677
|
+
const pushResult = await execGit(["push", "origin", manifest.integrationBranch], {
|
|
3678
|
+
cwd: mergeCwd,
|
|
3679
|
+
timeoutMs: 6e4
|
|
3680
|
+
});
|
|
3681
|
+
if (pushResult.exitCode !== 0) {
|
|
3682
|
+
throw new Error((pushResult.stderr ?? "").trim() || "git push failed");
|
|
3683
|
+
}
|
|
3684
|
+
} catch (error) {
|
|
3685
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3686
|
+
return {
|
|
3687
|
+
success: false,
|
|
3688
|
+
workflowId,
|
|
3689
|
+
error: `Failed to push run branch to origin: ${errorMsg}`
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
{
|
|
3693
|
+
const existingPr = await reconcilePrFromGh({
|
|
3694
|
+
cwd: mergeCwd,
|
|
3695
|
+
workflowId,
|
|
3696
|
+
headBranch: manifest.integrationBranch,
|
|
3697
|
+
baseBranch: prBaseBranch
|
|
3698
|
+
});
|
|
3699
|
+
if (existingPr.kind === "ambiguous") {
|
|
3700
|
+
return {
|
|
3701
|
+
success: false,
|
|
3702
|
+
workflowId,
|
|
3703
|
+
error: `Cannot reconcile PR state: ${existingPr.reason}`
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
if (existingPr.kind === "adopt") {
|
|
3707
|
+
console.log(chalk4.yellow(` Reconciled existing open PR: ${existingPr.value.prUrl}`));
|
|
3708
|
+
if (existingPr.value.prHeadSha !== sourceHeadSha) {
|
|
3709
|
+
return {
|
|
3710
|
+
success: false,
|
|
3711
|
+
workflowId,
|
|
3712
|
+
error: `PR head mismatch: expected ${sourceHeadSha}, found ${existingPr.value.prHeadSha}`
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3715
|
+
await updateManifestApplyStatus(workflowId, {
|
|
3716
|
+
prUrl: existingPr.value.prUrl,
|
|
3717
|
+
prNumber: existingPr.value.prNumber,
|
|
3718
|
+
prHeadSha: existingPr.value.prHeadSha,
|
|
3719
|
+
applyError: null,
|
|
3720
|
+
...branchRoleApplyStatus("pr-created")
|
|
3721
|
+
});
|
|
3722
|
+
return {
|
|
3723
|
+
success: true,
|
|
3724
|
+
degraded: warnings2.length > 0,
|
|
3725
|
+
workflowId,
|
|
3726
|
+
prUrl: existingPr.value.prUrl,
|
|
3727
|
+
prNumber: existingPr.value.prNumber,
|
|
3728
|
+
prHeadSha: existingPr.value.prHeadSha,
|
|
3729
|
+
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
const prTitle = flywheelPrTitle(workflowId);
|
|
3734
|
+
const observabilityLines = [];
|
|
3735
|
+
const stageCount = (manifest.stageIds ?? []).length;
|
|
3736
|
+
if (stageCount > 0) {
|
|
3737
|
+
observabilityLines.push(`| Stages | ${stageCount} |`);
|
|
3738
|
+
}
|
|
3739
|
+
if (workflowGateSummary) {
|
|
3740
|
+
observabilityLines.push(
|
|
3741
|
+
`| Workflow gate | ${workflowGateSummary.passed ? "pass" : "fail"} |`
|
|
3742
|
+
);
|
|
3743
|
+
}
|
|
3744
|
+
const observabilitySection = observabilityLines.length > 0 ? `
|
|
3745
|
+
|
|
3746
|
+
### Observability
|
|
3747
|
+
|
|
3748
|
+
| Field | Value |
|
|
3749
|
+
|---|---|
|
|
3750
|
+
${observabilityLines.join("\n")}` : "";
|
|
3751
|
+
const prBody = `## Workflow Result
|
|
3752
|
+
|
|
3753
|
+
**Source**: ${manifest.sourceType}${manifest.sprintId ? ` (${manifest.sprintId})` : ""}
|
|
3754
|
+
**Branch**: ${manifest.integrationBranch}
|
|
3755
|
+
**Base**: ${manifest.baseSha.slice(0, 7)}
|
|
3756
|
+
**Executed**: ${manifest.executedAt}
|
|
3757
|
+
**Executed by**: ${manifest.executedBy}${observabilitySection}
|
|
3758
|
+
|
|
3759
|
+
This PR contains the result of a Flywheel workflow execution.
|
|
3760
|
+
Review it and satisfy the repository's required checks before merging.
|
|
3761
|
+
|
|
3762
|
+
---
|
|
3763
|
+
*Generated by Build Flywheel*`;
|
|
3764
|
+
try {
|
|
3765
|
+
const prResult = await execa2(
|
|
3766
|
+
"gh",
|
|
3767
|
+
[
|
|
3768
|
+
"pr",
|
|
3769
|
+
"create",
|
|
3770
|
+
"--title",
|
|
3771
|
+
prTitle,
|
|
3772
|
+
"--body",
|
|
3773
|
+
prBody,
|
|
3774
|
+
"--head",
|
|
3775
|
+
manifest.integrationBranch,
|
|
3776
|
+
"--base",
|
|
3777
|
+
prBaseBranch
|
|
3778
|
+
],
|
|
3779
|
+
{
|
|
3780
|
+
stdio: "pipe",
|
|
3781
|
+
timeout: 6e4
|
|
3782
|
+
}
|
|
3783
|
+
);
|
|
3784
|
+
const stdout = typeof prResult.stdout === "string" ? prResult.stdout : String(prResult.stdout);
|
|
3785
|
+
const prUrl = stdout.match(/https:\/\/github\.com[^\s]+/)?.[0]?.trim() ?? "";
|
|
3786
|
+
if (!prUrl) {
|
|
3787
|
+
throw new Error("Failed to extract PR URL from gh CLI output");
|
|
3788
|
+
}
|
|
3789
|
+
const prNumber = parseInt(prUrl.split("/").pop() || "0", 10);
|
|
3790
|
+
console.log(chalk4.green(` PR created: ${prUrl}`));
|
|
3791
|
+
const prHeadSha = await readPrHeadSha(mergeCwd, prUrl);
|
|
3792
|
+
if (prHeadSha !== sourceHeadSha) {
|
|
3793
|
+
return {
|
|
3794
|
+
success: false,
|
|
3795
|
+
workflowId,
|
|
3796
|
+
error: `PR head mismatch: expected ${sourceHeadSha}, found ${prHeadSha}`
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
await updateManifestApplyStatus(workflowId, {
|
|
3800
|
+
prUrl,
|
|
3801
|
+
prNumber: Number.isFinite(prNumber) && prNumber > 0 ? prNumber : void 0,
|
|
3802
|
+
prHeadSha,
|
|
3803
|
+
applyError: null,
|
|
3804
|
+
...branchRoleApplyStatus("pr-created")
|
|
3805
|
+
});
|
|
3806
|
+
return {
|
|
3807
|
+
success: true,
|
|
3808
|
+
degraded: warnings2.length > 0,
|
|
3809
|
+
workflowId,
|
|
3810
|
+
prUrl,
|
|
3811
|
+
prNumber: prNumber > 0 ? prNumber : void 0,
|
|
3812
|
+
prHeadSha,
|
|
3813
|
+
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
3814
|
+
};
|
|
3815
|
+
} catch (error) {
|
|
3816
|
+
const existingAfterCreate = await reconcilePrFromGh({
|
|
3817
|
+
cwd: mergeCwd,
|
|
3818
|
+
workflowId,
|
|
3819
|
+
headBranch: manifest.integrationBranch,
|
|
3820
|
+
baseBranch: prBaseBranch
|
|
3821
|
+
});
|
|
3822
|
+
if (existingAfterCreate.kind === "adopt") {
|
|
3823
|
+
console.log(
|
|
3824
|
+
chalk4.yellow(
|
|
3825
|
+
` PR create failed but exact open PR exists; adopting ${existingAfterCreate.value.prUrl}`
|
|
3826
|
+
)
|
|
3827
|
+
);
|
|
3828
|
+
if (existingAfterCreate.value.prHeadSha !== sourceHeadSha) {
|
|
3829
|
+
return {
|
|
3830
|
+
success: false,
|
|
3831
|
+
workflowId,
|
|
3832
|
+
error: `PR head mismatch: expected ${sourceHeadSha}, found ${existingAfterCreate.value.prHeadSha}`
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
await updateManifestApplyStatus(workflowId, {
|
|
3836
|
+
prUrl: existingAfterCreate.value.prUrl,
|
|
3837
|
+
prNumber: existingAfterCreate.value.prNumber,
|
|
3838
|
+
prHeadSha: existingAfterCreate.value.prHeadSha,
|
|
3839
|
+
applyError: null,
|
|
3840
|
+
...branchRoleApplyStatus("pr-created")
|
|
3841
|
+
});
|
|
3842
|
+
return {
|
|
3843
|
+
success: true,
|
|
3844
|
+
degraded: warnings2.length > 0,
|
|
3845
|
+
workflowId,
|
|
3846
|
+
prUrl: existingAfterCreate.value.prUrl,
|
|
3847
|
+
prNumber: existingAfterCreate.value.prNumber,
|
|
3848
|
+
prHeadSha: existingAfterCreate.value.prHeadSha,
|
|
3849
|
+
warnings: warnings2.length > 0 ? warnings2 : void 0
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
if (existingAfterCreate.kind === "ambiguous") {
|
|
3853
|
+
return {
|
|
3854
|
+
success: false,
|
|
3855
|
+
workflowId,
|
|
3856
|
+
error: `Cannot reconcile PR state: ${existingAfterCreate.reason}`
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3860
|
+
let hint;
|
|
3861
|
+
if (isNotGithubHostMessage(errorMsg)) {
|
|
3862
|
+
hint = "The git remote is not a GitHub host, so no PR can be opened. For a backlog item rerun with --no-open-pr";
|
|
3863
|
+
} else if (errorMsg.includes("command not found") || errorMsg.includes("ENOENT")) {
|
|
3864
|
+
hint = "Install gh CLI: https://cli.github.com/";
|
|
3865
|
+
} else if (errorMsg.includes("unable to access") || errorMsg.includes("Could not resolve host") || errorMsg.includes("rate limit") || errorMsg.includes("timed out")) {
|
|
3866
|
+
hint = "Network or rate-limit error; check connectivity and retry";
|
|
3867
|
+
} else {
|
|
3868
|
+
hint = "Ensure gh CLI is authenticated: gh auth login";
|
|
3869
|
+
}
|
|
3870
|
+
const err = `Failed to create PR: ${errorMsg}
|
|
3871
|
+
${hint}`;
|
|
3872
|
+
return { success: false, workflowId, error: err };
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
if (options.dryRun) {
|
|
3876
|
+
if (gateRefusal) return { success: false, workflowId, error: gateRefusal };
|
|
3877
|
+
console.log(chalk4.cyan(" Merge Preview:\n"));
|
|
3878
|
+
const { stdout: diffStat } = await execa2(
|
|
3879
|
+
"git",
|
|
3880
|
+
["diff", "--stat", `${manifest.baseSha}..${manifest.integrationBranch}`],
|
|
3881
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
3882
|
+
);
|
|
3883
|
+
console.log(diffStat);
|
|
3884
|
+
console.log("");
|
|
3885
|
+
console.log(chalk4.dim(" No changes made (--dry-run)"));
|
|
3886
|
+
return { success: true, workflowId };
|
|
3887
|
+
}
|
|
3888
|
+
const allowlistPrefixes = getPlanningDirtyAllowlistPrefixes(mergeCwd);
|
|
3889
|
+
const { stdout: currentBranchStdout } = await execa2(
|
|
3890
|
+
"git",
|
|
3891
|
+
["rev-parse", "--symbolic-full-name", "HEAD"],
|
|
3892
|
+
{
|
|
3893
|
+
cwd: mergeCwd,
|
|
3894
|
+
stdio: "pipe",
|
|
3895
|
+
timeout: 3e4
|
|
3896
|
+
}
|
|
3897
|
+
);
|
|
3898
|
+
const currentBranch = parseCurrentBranchRef(currentBranchStdout);
|
|
3899
|
+
currentBranchForRecovery = currentBranch;
|
|
3900
|
+
if (currentBranch === "HEAD") {
|
|
3901
|
+
throw new Error("Cannot merge in detached HEAD state. Checkout a branch and retry.");
|
|
3902
|
+
}
|
|
3903
|
+
if (currentBranch === manifest.integrationBranch) {
|
|
3904
|
+
throw new Error(
|
|
3905
|
+
`You are currently on the run branch (${manifest.integrationBranch}).
|
|
3906
|
+
Checkout the target branch and re-run merge.`
|
|
3907
|
+
);
|
|
3908
|
+
}
|
|
3909
|
+
const { stdout: headShaStdout } = await execa2("git", ["rev-parse", "HEAD"], {
|
|
3910
|
+
cwd: mergeCwd,
|
|
3911
|
+
stdio: "pipe",
|
|
3912
|
+
timeout: 3e4
|
|
3913
|
+
});
|
|
3914
|
+
const headSha = headShaStdout.trim();
|
|
3915
|
+
preMergeHeadSha = headSha;
|
|
3916
|
+
if (headSha !== manifest.baseSha) {
|
|
3917
|
+
const msg = `Current HEAD (${headSha.slice(0, 7)}) does not match workflow base (${manifest.baseSha.slice(0, 7)}).`;
|
|
3918
|
+
if (!options.force) {
|
|
3919
|
+
throw new Error(
|
|
3920
|
+
`${msg}
|
|
3921
|
+
For safest merge, checkout the base commit/branch and retry.
|
|
3922
|
+
Use --force to merge anyway.`
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
console.log(chalk4.yellow(` Warning: ${msg}`));
|
|
3926
|
+
}
|
|
3927
|
+
if (gateRefusal) return { success: false, workflowId, error: gateRefusal };
|
|
3928
|
+
if (!options.dryRun) {
|
|
3929
|
+
releaseApplyLock = await acquireFlywheelApplyLock(getProjectRoot());
|
|
3930
|
+
signalFinalizer = registerMergeSignalFinalizer(workflowId);
|
|
3931
|
+
await assertApplyLockOwned();
|
|
3932
|
+
}
|
|
3933
|
+
if (!options.dryRun) {
|
|
3934
|
+
const { stdout: lockedHeadStdout } = await execa2("git", ["rev-parse", "HEAD"], {
|
|
3935
|
+
cwd: mergeCwd,
|
|
3936
|
+
stdio: "pipe",
|
|
3937
|
+
timeout: 3e4
|
|
3938
|
+
});
|
|
3939
|
+
const lockedHeadSha = lockedHeadStdout.trim();
|
|
3940
|
+
if (lockedHeadSha !== preMergeHeadSha) {
|
|
3941
|
+
if (!options.force && lockedHeadSha !== manifest.baseSha) {
|
|
3942
|
+
throw new Error(
|
|
3943
|
+
`Apply target advanced while waiting for the merge lock (now ${lockedHeadSha.slice(0, 7)}, workflow base ${manifest.baseSha.slice(0, 7)}).
|
|
3944
|
+
Another merge landed first. Re-run merge from the current base, or use --force.`
|
|
3945
|
+
);
|
|
3946
|
+
}
|
|
3947
|
+
preMergeHeadSha = lockedHeadSha;
|
|
3948
|
+
}
|
|
3949
|
+
await assertCurrentBranchStillMatchesApplyTarget();
|
|
3950
|
+
}
|
|
3951
|
+
{
|
|
3952
|
+
const { stdout: statusOutput } = await execa2("git", ["status", "--porcelain=v1"], {
|
|
3953
|
+
cwd: mergeCwd,
|
|
3954
|
+
stdio: "pipe",
|
|
3955
|
+
timeout: 3e4
|
|
3956
|
+
});
|
|
3957
|
+
const entries = parseGitStatusPorcelain(statusOutput);
|
|
3958
|
+
const dirtyEntries = entries.filter(
|
|
3959
|
+
(e) => !getStatusEntryPathspecs(e).every((pathname) => isWorktreeRunSentinelPath(pathname))
|
|
3960
|
+
);
|
|
3961
|
+
const dirtyPlanning = dirtyEntries.filter(
|
|
3962
|
+
(e) => isStatusEntryInAllowlist(e, allowlistPrefixes) && (e.status[0] === " " || e.status[0] === "?")
|
|
3963
|
+
);
|
|
3964
|
+
const dirtyOther = dirtyEntries.filter((e) => !dirtyPlanning.includes(e));
|
|
3965
|
+
dirtyPlanningUnstagedPaths = dirtyPlanning.filter((entry) => hasUnstagedTrackedChange(entry.status)).flatMap((entry) => getStatusEntryPathspecs(entry));
|
|
3966
|
+
hadPreExistingTrackedPlanningEdits = dirtyPlanning.some(
|
|
3967
|
+
(entry) => isTrackedStatusChange(entry.status)
|
|
3968
|
+
);
|
|
3969
|
+
const stagedOther = dirtyOther.filter((entry) => {
|
|
3970
|
+
const indexStatus = entry.status[0] ?? " ";
|
|
3971
|
+
return indexStatus !== " " && indexStatus !== "?";
|
|
3972
|
+
});
|
|
3973
|
+
if (manifest.applyError && stagedOther.length > 0) {
|
|
3974
|
+
throw new Error(
|
|
3975
|
+
`A previous merge-result attempt left staged changes in ${mergeCwd} (${formatPathList(stagedOther.flatMap(getStatusEntryPathspecs))}). Auto-stash would re-apply them on top of this merge. Review them, keep anything you need elsewhere, clear them (for example: git restore --staged --worktree -- <paths>), and rerun merge-result.`
|
|
3976
|
+
);
|
|
3977
|
+
}
|
|
3978
|
+
if (dirtyOther.length > 0) {
|
|
3979
|
+
autoStashIncludesStagedPlanning = dirtyOther.some(
|
|
3980
|
+
(entry) => isStatusEntryInAllowlist(entry, allowlistPrefixes)
|
|
3981
|
+
);
|
|
3982
|
+
console.log(
|
|
3983
|
+
chalk4.yellow(
|
|
3984
|
+
` Isolating ${dirtyOther.length} uncommitted file(s), including pre-staged planning/config edits, for merge.`
|
|
3985
|
+
)
|
|
3986
|
+
);
|
|
3987
|
+
console.log(
|
|
3988
|
+
chalk4.dim(` ${formatPathList(dirtyOther.flatMap(getStatusEntryPathspecs))}`)
|
|
3989
|
+
);
|
|
3990
|
+
const stashMessage = `[Build Flywheel] auto-stash before merge-result ${workflowId} ${randomUUID2()}`;
|
|
3991
|
+
const dirtyOtherPaths = Array.from(new Set(dirtyOther.flatMap(getStatusEntryPathspecs)));
|
|
3992
|
+
const stashResult = await execa2(
|
|
3993
|
+
"git",
|
|
3994
|
+
["stash", "push", "-u", "-m", stashMessage, "--", ...dirtyOtherPaths],
|
|
3995
|
+
{ cwd: mergeCwd, stdio: "pipe", timeout: 6e4, reject: false }
|
|
3996
|
+
);
|
|
3997
|
+
const stashOutput = `${stashResult.stdout ?? ""}
|
|
3998
|
+
${stashResult.stderr ?? ""}`;
|
|
3999
|
+
didAutoStash = stashResult.exitCode === 0 && !/No local changes/i.test(stashOutput);
|
|
4000
|
+
if (!didAutoStash) {
|
|
4001
|
+
throw new Error(
|
|
4002
|
+
"Working tree is not clean and auto-stash did not succeed. Commit/stash changes and retry."
|
|
4003
|
+
);
|
|
4004
|
+
}
|
|
4005
|
+
autoStashSha = await resolveStashShaByMessage(mergeCwd, stashMessage);
|
|
4006
|
+
const { stdout: postStashStatusOutput } = await execa2("git", ["status", "--porcelain=v1"], {
|
|
4007
|
+
cwd: mergeCwd,
|
|
4008
|
+
stdio: "pipe",
|
|
4009
|
+
timeout: 3e4
|
|
4010
|
+
});
|
|
4011
|
+
const postStashDirtyOther = parseGitStatusPorcelain(postStashStatusOutput).filter(
|
|
4012
|
+
(e) => !getStatusEntryPathspecs(e).every((pathname) => isWorktreeRunSentinelPath(pathname))
|
|
4013
|
+
).filter(
|
|
4014
|
+
(e) => !isStatusEntryInAllowlist(e, allowlistPrefixes) || e.status[0] !== " " && e.status[0] !== "?"
|
|
4015
|
+
);
|
|
4016
|
+
if (postStashDirtyOther.length > 0) {
|
|
4017
|
+
throw new Error(
|
|
4018
|
+
"Working tree is not clean after auto-stash. Remaining non-planning changes: " + formatPathList(postStashDirtyOther.flatMap(getStatusEntryPathspecs))
|
|
4019
|
+
);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
if (dirtyPlanning.length > 0) {
|
|
4023
|
+
console.log(
|
|
4024
|
+
chalk4.dim(
|
|
4025
|
+
` Ignoring ${dirtyPlanning.length} dirty planning file(s) for merge-result (${formatPathList(
|
|
4026
|
+
dirtyPlanning.map((d) => d.path)
|
|
4027
|
+
)})`
|
|
4028
|
+
)
|
|
4029
|
+
);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
const dirtyPlanningBaseline = options.dryRun ? /* @__PURE__ */ new Set() : await snapshotDirtyPlanningPaths({ cwd: mergeCwd, allowlistPrefixes });
|
|
4033
|
+
await assertApplyLockOwned();
|
|
4034
|
+
mergeMutationStarted = true;
|
|
4035
|
+
await squashMergeRunBranch({
|
|
4036
|
+
cwd: mergeCwd,
|
|
4037
|
+
integrationBranch: manifest.integrationBranch
|
|
4038
|
+
});
|
|
4039
|
+
await assertApplyLockOwned();
|
|
4040
|
+
async function runPostSquashValidationPipeline() {
|
|
4041
|
+
const triageResult = await applyTriageDecisions(workflowId, {
|
|
4042
|
+
cwd: mergeCwd,
|
|
4043
|
+
expectedBranch: branchRoleApplyTarget ?? void 0,
|
|
4044
|
+
allowMain: options.allowMain,
|
|
4045
|
+
allowProtected: options.allowProtected
|
|
4046
|
+
});
|
|
4047
|
+
if (triageResult.followUpFailures.length > 0) {
|
|
4048
|
+
followUpFailures = [...followUpFailures ?? [], ...triageResult.followUpFailures];
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
await runPostSquashValidationPipeline();
|
|
4052
|
+
await assertCurrentBranchStillMatchesApplyTarget();
|
|
4053
|
+
async function runNormalizationMergeGate() {
|
|
4054
|
+
const stagedFiles = await listStagedFiles(mergeCwd);
|
|
4055
|
+
const plan = resolveMergeNormalization(mergeCwd, stagedFiles);
|
|
4056
|
+
if (plan.kind === "unavailable") {
|
|
4057
|
+
console.log(chalk4.dim(plan.message));
|
|
4058
|
+
return { ok: true };
|
|
4059
|
+
}
|
|
4060
|
+
if (plan.kind === "not-required") return { ok: true };
|
|
4061
|
+
console.log(chalk4.cyan(` ${plan.label} check (merged files): ${plan.files.length}`));
|
|
4062
|
+
await assertApplyLockOwned();
|
|
4063
|
+
normalizationStarted = true;
|
|
4064
|
+
await runNormalization(plan.safeWrite, mergeCwd);
|
|
4065
|
+
await assertApplyLockOwned();
|
|
4066
|
+
await execa2("git", ["add", "--", ...plan.files], {
|
|
4067
|
+
cwd: mergeCwd,
|
|
4068
|
+
stdio: "pipe",
|
|
4069
|
+
timeout: 3e4
|
|
4070
|
+
});
|
|
4071
|
+
const checkResult = await runNormalization(plan.check, mergeCwd);
|
|
4072
|
+
if (!checkResult.ok) {
|
|
4073
|
+
return {
|
|
4074
|
+
ok: false,
|
|
4075
|
+
summary: formatNormalizationFailureSummary(checkResult),
|
|
4076
|
+
plan
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
return { ok: true };
|
|
4080
|
+
}
|
|
4081
|
+
{
|
|
4082
|
+
const gate = await runNormalizationMergeGate();
|
|
4083
|
+
if (!gate.ok) {
|
|
4084
|
+
throw new Error(
|
|
4085
|
+
`${gate.plan.label} check failed for merged files; merge stopped to avoid landing new lint/format errors.
|
|
4086
|
+
|
|
4087
|
+
The staged merge, including normalization edits, was left in the checkout at ${mergeCwd}. Inspect it with:
|
|
4088
|
+
git -C ${mergeCwd} status
|
|
4089
|
+
git -C ${mergeCwd} diff --cached
|
|
4090
|
+
Fix the reported files there and commit, or discard the merge with:
|
|
4091
|
+
git -C ${mergeCwd} reset --hard ${preMergeHeadSha ?? "HEAD"}
|
|
4092
|
+
Suggested fix: ${gate.plan.manualFixCommand}
|
|
4093
|
+
|
|
4094
|
+
${gate.plan.label} output:
|
|
4095
|
+
${gate.summary}`
|
|
4096
|
+
);
|
|
4097
|
+
}
|
|
4098
|
+
normalizationStarted = false;
|
|
4099
|
+
}
|
|
4100
|
+
await assertCurrentBranchStillMatchesApplyTarget();
|
|
4101
|
+
{
|
|
4102
|
+
let workflowResultForDrift;
|
|
4103
|
+
try {
|
|
4104
|
+
const resultPath = getResultPath(workflowId);
|
|
4105
|
+
const content = await fs4.readFile(resultPath, "utf-8");
|
|
4106
|
+
workflowResultForDrift = JSON.parse(content);
|
|
4107
|
+
} catch {
|
|
4108
|
+
}
|
|
4109
|
+
const stagesWithDrift = getStagesWithDriftFromWorkflowResult(workflowResultForDrift);
|
|
4110
|
+
if (stagesWithDrift && stagesWithDrift.length > 0) {
|
|
4111
|
+
const totalViolations = stagesWithDrift.reduce((sum, s) => sum + s.violations.length, 0);
|
|
4112
|
+
console.log(
|
|
4113
|
+
chalk4.yellow(
|
|
4114
|
+
`
|
|
4115
|
+
Drift detected: ${totalViolations} file(s) outside allowlist across ${stagesWithDrift.length} stage(s)`
|
|
4116
|
+
)
|
|
4117
|
+
);
|
|
4118
|
+
for (const stage of stagesWithDrift.slice(0, 3)) {
|
|
4119
|
+
console.log(chalk4.dim(` - ${stage.stageId}: ${stage.violations.length} file(s)`));
|
|
4120
|
+
}
|
|
4121
|
+
if (stagesWithDrift.length > 3) {
|
|
4122
|
+
console.log(chalk4.dim(` ... and ${stagesWithDrift.length - 3} more stage(s)`));
|
|
4123
|
+
}
|
|
4124
|
+
console.log(chalk4.dim(" (Drift was committed in warn mode - review changes if needed)"));
|
|
4125
|
+
console.log("");
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
if (!options.dryRun) {
|
|
4129
|
+
await assertApplyLockOwned();
|
|
4130
|
+
const dirtyPlanningNow = await snapshotDirtyPlanningPaths({
|
|
4131
|
+
cwd: mergeCwd,
|
|
4132
|
+
allowlistPrefixes
|
|
4133
|
+
});
|
|
4134
|
+
const stageable = await snapshotStageablePlanningPaths({
|
|
4135
|
+
cwd: mergeCwd,
|
|
4136
|
+
allowlistPrefixes
|
|
4137
|
+
});
|
|
4138
|
+
const mergeDirtiedPlanningPaths = [...dirtyPlanningNow].filter(
|
|
4139
|
+
(p) => !dirtyPlanningBaseline.has(p) && isPathInAllowlist(p, allowlistPrefixes) && stageable.has(p)
|
|
4140
|
+
);
|
|
4141
|
+
if (mergeDirtiedPlanningPaths.length > 0) {
|
|
4142
|
+
await execa2("git", ["add", "--", ...mergeDirtiedPlanningPaths], {
|
|
4143
|
+
cwd: mergeCwd,
|
|
4144
|
+
stdio: "pipe",
|
|
4145
|
+
timeout: 3e4
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
if (!options.skipVerify) {
|
|
4150
|
+
let gateResult;
|
|
4151
|
+
gateResult = await runPostMergeVerifyGate({
|
|
4152
|
+
workflowId,
|
|
4153
|
+
manifest,
|
|
4154
|
+
preMergeHeadSha,
|
|
4155
|
+
projectRoot: getProjectRoot(),
|
|
4156
|
+
mergeCwd,
|
|
4157
|
+
ambientUnstagedAllowlistedPaths: dirtyPlanningUnstagedPaths,
|
|
4158
|
+
signal: signalFinalizer?.signal,
|
|
4159
|
+
onVerificationStarted: () => {
|
|
4160
|
+
postMergeVerificationStarted = true;
|
|
4161
|
+
}
|
|
4162
|
+
});
|
|
4163
|
+
verifiedTreeFingerprint = gateResult.verifiedTreeFingerprint;
|
|
4164
|
+
if (gateResult.orphanedStashWarning) {
|
|
4165
|
+
postMergeWarnings.push(gateResult.orphanedStashWarning);
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
adoptedApplyTreeFingerprint = verifiedTreeFingerprint;
|
|
4169
|
+
let stagedStatusWritePaths = /* @__PURE__ */ new Set();
|
|
4170
|
+
const adoptStatusWriteStagedDelta = async () => {
|
|
4171
|
+
if (!adoptedApplyTreeFingerprint) return;
|
|
4172
|
+
adoptedApplyTreeFingerprint = await adoptStatusWriteTreeDelta(
|
|
4173
|
+
mergeCwd,
|
|
4174
|
+
adoptedApplyTreeFingerprint,
|
|
4175
|
+
stagedStatusWritePaths
|
|
4176
|
+
);
|
|
4177
|
+
};
|
|
4178
|
+
const taskStatus = options.taskStatus ?? "apply";
|
|
4179
|
+
const completionSprintId = manifest.sourceType === "sprint" ? manifest.sprintId : void 0;
|
|
4180
|
+
const runTaskCompletion = async () => {
|
|
4181
|
+
if (!completionSprintId) return;
|
|
4182
|
+
if (taskStatus !== "apply" && taskStatus !== "preview") {
|
|
4183
|
+
console.log(chalk4.dim(" Task status sync skipped (--task-status=skip)"));
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
const taskTransitions = await loadRecordedTaskTransitions(workflowId, completionSprintId);
|
|
4187
|
+
if (!taskTransitions) return;
|
|
4188
|
+
if (taskStatus === "preview") {
|
|
4189
|
+
console.log(chalk4.cyan(" Task transition preview (artifact-driven):"));
|
|
4190
|
+
for (const t of taskTransitions) {
|
|
4191
|
+
console.log(
|
|
4192
|
+
chalk4.dim(
|
|
4193
|
+
` - ${t.taskId}: ${t.targetStatus}${t.hoursLogged ? ` (${t.hoursLogged.toFixed(2)}h)` : ""}`
|
|
4194
|
+
)
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4197
|
+
return;
|
|
4198
|
+
}
|
|
4199
|
+
console.log(chalk4.cyan(" Applying task transitions (artifact-driven)..."));
|
|
4200
|
+
const completion = await completeAndStageVerifiedTasks({
|
|
4201
|
+
workflowId,
|
|
4202
|
+
sprintId: completionSprintId,
|
|
4203
|
+
transitions: taskTransitions,
|
|
4204
|
+
mergeCwd,
|
|
4205
|
+
allowlistPrefixes,
|
|
4206
|
+
assertApplyLockOwned,
|
|
4207
|
+
onInfo: (message) => console.log(chalk4.dim(message)),
|
|
4208
|
+
onWarning: (message) => console.log(chalk4.yellow(message))
|
|
4209
|
+
});
|
|
4210
|
+
completionSyncDetails = completion.details;
|
|
4211
|
+
if (completion.failures) taskSyncFailures = completion.failures;
|
|
4212
|
+
stagedStatusWritePaths = new Set(completion.stagedPaths);
|
|
4213
|
+
postMergeWarnings.push(...completion.warnings);
|
|
4214
|
+
await adoptStatusWriteStagedDelta();
|
|
4215
|
+
};
|
|
4216
|
+
const assertVerifiedTreeStillCurrent = async () => {
|
|
4217
|
+
if (!adoptedApplyTreeFingerprint) return;
|
|
4218
|
+
const current = await execGit(["write-tree"], { cwd: mergeCwd, timeoutMs: 3e4 });
|
|
4219
|
+
if (current.exitCode !== 0 || current.stdout.trim() !== adoptedApplyTreeFingerprint) {
|
|
4220
|
+
throw new Error(
|
|
4221
|
+
"The staged tree changed after final verification; refusing to commit or report apply success."
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4224
|
+
};
|
|
4225
|
+
await assertVerifiedTreeStillCurrent();
|
|
4226
|
+
await assertApplyLockOwned();
|
|
4227
|
+
if (options.applyOnly) {
|
|
4228
|
+
console.log(chalk4.yellow(" Changes staged (--apply-only)"));
|
|
4229
|
+
console.log(chalk4.yellow(" Review and commit manually"));
|
|
4230
|
+
if (completionSprintId && taskStatus === "apply") {
|
|
4231
|
+
console.log(
|
|
4232
|
+
chalk4.yellow(
|
|
4233
|
+
" Tasks were left open; after committing, complete each with `build plan task complete <task-id>`"
|
|
4234
|
+
)
|
|
4235
|
+
);
|
|
4236
|
+
}
|
|
4237
|
+
if (!options.dryRun) {
|
|
4238
|
+
try {
|
|
4239
|
+
const stagedProofTree = applyProofTreeFingerprint();
|
|
4240
|
+
await updateManifestApplyStatus(workflowId, {
|
|
4241
|
+
...stagedProofTree ? buildApplyOnlyProofStatus(stagedProofTree) : { stagedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4242
|
+
...branchRoleApplyStatus()
|
|
4243
|
+
});
|
|
4244
|
+
} catch {
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
return { success: true, workflowId };
|
|
4248
|
+
}
|
|
4249
|
+
const partitionSystemWrites = codeCheckoutCarriesSharedBuckets(mergeCwd);
|
|
4250
|
+
const stagedBeforeCompletion = await listStagedFiles(mergeCwd, { includeDeletions: true });
|
|
4251
|
+
const codeFirst = stagedBeforeCompletion.some((f) => !isSystemWritePath(f));
|
|
4252
|
+
if (!codeFirst) {
|
|
4253
|
+
await assertFailedRunHasCodeToApply(workflowGateSummary, getResultPath(workflowId));
|
|
4254
|
+
await runTaskCompletion();
|
|
4255
|
+
await assertVerifiedTreeStillCurrent();
|
|
4256
|
+
}
|
|
4257
|
+
const diffCheck = await execa2("git", ["diff", "--cached", "--quiet"], {
|
|
4258
|
+
cwd: mergeCwd,
|
|
4259
|
+
stdio: "pipe",
|
|
4260
|
+
timeout: 3e4,
|
|
4261
|
+
reject: false
|
|
4262
|
+
});
|
|
4263
|
+
if (diffCheck.exitCode === 0) {
|
|
4264
|
+
console.log(chalk4.dim(" No changes to commit (workflow produced no diff)."));
|
|
4265
|
+
applyMaterialized = true;
|
|
4266
|
+
const currentHead = await execGit(["rev-parse", "HEAD"], {
|
|
4267
|
+
cwd: mergeCwd,
|
|
4268
|
+
timeoutMs: 3e4
|
|
4269
|
+
});
|
|
4270
|
+
if (!options.skipVerify) {
|
|
4271
|
+
if (!verifiedTreeFingerprint) {
|
|
4272
|
+
throw new Error("Final verification did not produce a tree fingerprint.");
|
|
4273
|
+
}
|
|
4274
|
+
appliedCommitSha = await assertCommittedTreeAuthority(
|
|
4275
|
+
applyProofTreeFingerprint() ?? verifiedTreeFingerprint
|
|
4276
|
+
);
|
|
4277
|
+
} else {
|
|
4278
|
+
appliedCommitSha = currentHead.exitCode === 0 && currentHead.stdout.trim().length > 0 ? currentHead.stdout.trim() : void 0;
|
|
4279
|
+
}
|
|
4280
|
+
await assertApplyLockOwned();
|
|
4281
|
+
await restoreAutoStash();
|
|
4282
|
+
await assertApplyLockOwned();
|
|
4283
|
+
await markApplied();
|
|
4284
|
+
if (options.cleanup && !options.keepArtifacts) {
|
|
4285
|
+
const trackedCleanupTargets = getTrackedCleanupTargets(manifest);
|
|
4286
|
+
await cleanupArtifacts({
|
|
4287
|
+
integrationBranch: manifest.integrationBranch,
|
|
4288
|
+
workflowId,
|
|
4289
|
+
relatedTaskIds: manifest.relatedTaskIds ?? [],
|
|
4290
|
+
trackedBranches: trackedCleanupTargets.trackedBranches,
|
|
4291
|
+
trackedWorktrees: trackedCleanupTargets.trackedWorktrees
|
|
4292
|
+
});
|
|
4293
|
+
} else if (!options.keepArtifacts) {
|
|
4294
|
+
console.log(chalk4.cyan(" Cleaning up run branch/worktree..."));
|
|
4295
|
+
await cleanupBranchAndWorktree(manifest.integrationBranch, workflowId);
|
|
4296
|
+
const trackedCleanupTargets = getTrackedCleanupTargets(manifest);
|
|
4297
|
+
await cleanupRelatedBranchesAndWorktrees({
|
|
4298
|
+
integrationBranch: manifest.integrationBranch,
|
|
4299
|
+
relatedTaskIds: manifest.relatedTaskIds ?? [],
|
|
4300
|
+
trackedBranches: trackedCleanupTargets.trackedBranches,
|
|
4301
|
+
trackedWorktrees: trackedCleanupTargets.trackedWorktrees
|
|
4302
|
+
});
|
|
4303
|
+
} else {
|
|
4304
|
+
await logPreservedRunArtifacts(manifest.integrationBranch);
|
|
4305
|
+
}
|
|
4306
|
+
const warnings2 = [
|
|
4307
|
+
...taskSyncFailures ? [`Task sync completed with ${taskSyncFailures.count} failure(s)`] : [],
|
|
4308
|
+
...followUpFailures ?? [],
|
|
4309
|
+
...postMergeWarnings
|
|
4310
|
+
];
|
|
4311
|
+
reportMergeOutcome(warnings2);
|
|
4312
|
+
return {
|
|
4313
|
+
success: true,
|
|
4314
|
+
degraded: warnings2.length > 0,
|
|
4315
|
+
workflowId,
|
|
4316
|
+
applied: true,
|
|
4317
|
+
...taskSyncFailures ? { taskSyncFailures } : {},
|
|
4318
|
+
...followUpFailures ? { followUpFailures } : {},
|
|
4319
|
+
...warnings2.length > 0 ? { warnings: warnings2 } : {}
|
|
4320
|
+
};
|
|
4321
|
+
}
|
|
4322
|
+
const messageStyle = resolveGitMessageStyle(mergeCwd);
|
|
4323
|
+
const commitMsg = `${flywheelMergeCommitSubject(messageStyle)}
|
|
4324
|
+
|
|
4325
|
+
Source: ${manifest.sourceType}${manifest.sprintId ? ` (${manifest.sprintId})` : ""}
|
|
4326
|
+
Branch: ${manifest.integrationBranch}
|
|
4327
|
+
Base: ${manifest.baseSha.slice(0, 7)}`;
|
|
4328
|
+
let finalCommitMsg = commitMsg;
|
|
4329
|
+
try {
|
|
4330
|
+
const parsed = JSON.parse(await fs4.readFile(getResultPath(workflowId), "utf-8"));
|
|
4331
|
+
const verifySummary = formatVerifyStatusForCommitMessage(parsed);
|
|
4332
|
+
if (verifySummary) finalCommitMsg = `${commitMsg}
|
|
4333
|
+
${verifySummary}`;
|
|
4334
|
+
} catch {
|
|
4335
|
+
}
|
|
4336
|
+
finalCommitMsg = `${finalCommitMsg}
|
|
4337
|
+
|
|
4338
|
+
${flywheelApplyTrailers(workflowId, "code")}`;
|
|
4339
|
+
const metadataCommitMsg = flywheelMetadataCommitMessage(workflowId, messageStyle);
|
|
4340
|
+
const quiet = isQuietOutputRequested();
|
|
4341
|
+
const commitStaged = async (message, metadata) => {
|
|
4342
|
+
await assertApplyLockOwned();
|
|
4343
|
+
const preCommitTree = await readStagedTree(mergeCwd).catch(() => void 0);
|
|
4344
|
+
try {
|
|
4345
|
+
await runFlywheelCommit({
|
|
4346
|
+
cwd: mergeCwd,
|
|
4347
|
+
phase: metadata ? "metadata" : "code",
|
|
4348
|
+
run: () => execa2("git", ["commit", "-m", message], {
|
|
4349
|
+
cwd: mergeCwd,
|
|
4350
|
+
stdio: quiet ? "pipe" : "inherit",
|
|
4351
|
+
timeout: GIT_HOOK_COMMIT_TIMEOUT_MS,
|
|
4352
|
+
...metadata ? { env: { ...process.env, BUILD_BACKLOG_SYNC: "1" } } : {}
|
|
4353
|
+
}),
|
|
4354
|
+
quiet,
|
|
4355
|
+
style: messageStyle,
|
|
4356
|
+
priorCommit: didCommit
|
|
4357
|
+
});
|
|
4358
|
+
} catch (error) {
|
|
4359
|
+
if (error instanceof FlywheelCommitError && !didCommit) uncommittedCommitFailure = true;
|
|
4360
|
+
throw error;
|
|
4361
|
+
}
|
|
4362
|
+
await recordCommitMaterialized();
|
|
4363
|
+
if (preCommitTree && appliedCommitSha) {
|
|
4364
|
+
recordedCommits.push({ preCommitTree, sha: appliedCommitSha });
|
|
4365
|
+
}
|
|
4366
|
+
};
|
|
4367
|
+
if (codeFirst) {
|
|
4368
|
+
const heldBackFiles = partitionSystemWrites ? stagedBeforeCompletion.filter(isSystemWritePath) : [];
|
|
4369
|
+
await assertVerifiedTreeStillCurrent();
|
|
4370
|
+
if (heldBackFiles.length > 0) {
|
|
4371
|
+
await execGitChecked(["restore", "--staged", ...heldBackFiles], {
|
|
4372
|
+
cwd: mergeCwd,
|
|
4373
|
+
timeoutMs: 3e4
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4376
|
+
await commitStaged(
|
|
4377
|
+
heldBackFiles.length > 0 ? `${finalCommitMsg}
|
|
4378
|
+
${formatHeldMetadataTrailers(heldBackFiles)}` : finalCommitMsg,
|
|
4379
|
+
false
|
|
4380
|
+
);
|
|
4381
|
+
const codeTree = await execGit(["rev-parse", "HEAD^{tree}"], {
|
|
4382
|
+
cwd: mergeCwd,
|
|
4383
|
+
timeoutMs: 3e4
|
|
4384
|
+
});
|
|
4385
|
+
const codeCommitTree = codeTree.exitCode === 0 ? codeTree.stdout.trim() : void 0;
|
|
4386
|
+
if (!options.skipVerify && verifiedTreeFingerprint) {
|
|
4387
|
+
const moved = codeCommitTree === verifiedTreeFingerprint ? { exitCode: 0, stdout: "" } : codeCommitTree ? await execGit(
|
|
4388
|
+
["diff", "--name-only", "--no-renames", verifiedTreeFingerprint, codeCommitTree],
|
|
4389
|
+
{ cwd: mergeCwd, timeoutMs: 3e4 }
|
|
4390
|
+
) : { exitCode: 1, stdout: "" };
|
|
4391
|
+
const heldBack = new Set(heldBackFiles);
|
|
4392
|
+
if (moved.exitCode !== 0 || moved.stdout.split("\n").map((entry) => entry.trim()).some((pathname) => pathname.length > 0 && !heldBack.has(pathname))) {
|
|
4393
|
+
throw await committedTreeFailure(
|
|
4394
|
+
new Error("Committed HEAD tree does not match the final verified tree.")
|
|
4395
|
+
);
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
codeCommitOnBase = preMergeHeadSha === manifest.baseSha;
|
|
4399
|
+
codeCommitAwaitingCompletion = true;
|
|
4400
|
+
if (!quiet) console.log(chalk4.green(" Code changes committed"));
|
|
4401
|
+
if (heldBackFiles.length > 0) {
|
|
4402
|
+
await execa2("git", ["add", ...heldBackFiles], {
|
|
4403
|
+
cwd: mergeCwd,
|
|
4404
|
+
stdio: "pipe",
|
|
4405
|
+
timeout: 3e4
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
await runTaskCompletion();
|
|
4409
|
+
await assertVerifiedTreeStillCurrent();
|
|
4410
|
+
if (heldBackFiles.length > 0 || stagedStatusWritePaths.size > 0) {
|
|
4411
|
+
await commitStaged(metadataCommitMsg, true);
|
|
4412
|
+
if (!quiet) console.log(chalk4.green(" Task/sprint metadata committed separately"));
|
|
4413
|
+
}
|
|
4414
|
+
codeCommitAwaitingCompletion = false;
|
|
4415
|
+
} else {
|
|
4416
|
+
const allStagedFiles = await listStagedFiles(mergeCwd);
|
|
4417
|
+
const systemWriteFiles = partitionSystemWrites ? allStagedFiles.filter(isSystemWritePath) : [];
|
|
4418
|
+
const otherFiles = allStagedFiles.filter((f) => !isSystemWritePath(f));
|
|
4419
|
+
await assertVerifiedTreeStillCurrent();
|
|
4420
|
+
if (systemWriteFiles.length > 0 && otherFiles.length > 0) {
|
|
4421
|
+
await execa2("git", ["restore", "--staged", ...systemWriteFiles], {
|
|
4422
|
+
cwd: mergeCwd,
|
|
4423
|
+
stdio: "pipe",
|
|
4424
|
+
timeout: 3e4
|
|
4425
|
+
});
|
|
4426
|
+
await commitStaged(finalCommitMsg, false);
|
|
4427
|
+
await execa2("git", ["add", ...systemWriteFiles], {
|
|
4428
|
+
cwd: mergeCwd,
|
|
4429
|
+
stdio: "pipe",
|
|
4430
|
+
timeout: 3e4
|
|
4431
|
+
});
|
|
4432
|
+
await commitStaged(metadataCommitMsg, true);
|
|
4433
|
+
if (!quiet) console.log(chalk4.green(" Changes and task/sprint metadata committed"));
|
|
4434
|
+
} else if (systemWriteFiles.length > 0) {
|
|
4435
|
+
await commitStaged(metadataCommitMsg, true);
|
|
4436
|
+
if (!quiet) console.log(chalk4.green(" Task/sprint metadata committed"));
|
|
4437
|
+
} else {
|
|
4438
|
+
await commitStaged(finalCommitMsg, false);
|
|
4439
|
+
if (!quiet) console.log(chalk4.green(" Changes committed"));
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
await assertApplyLockOwned();
|
|
4443
|
+
const finalHead = await execGit(["rev-parse", "HEAD"], { cwd: mergeCwd, timeoutMs: 3e4 });
|
|
4444
|
+
if (finalHead.exitCode !== 0 || finalHead.stdout.trim().length === 0) {
|
|
4445
|
+
throw new Error("Could not resolve the applied commit SHA.");
|
|
4446
|
+
}
|
|
4447
|
+
if (!options.skipVerify) {
|
|
4448
|
+
if (!verifiedTreeFingerprint) {
|
|
4449
|
+
throw new Error("Final verification did not produce a tree fingerprint.");
|
|
4450
|
+
}
|
|
4451
|
+
await assertCommittedTreeAuthority(applyProofTreeFingerprint() ?? verifiedTreeFingerprint);
|
|
4452
|
+
}
|
|
4453
|
+
appliedCommitSha = finalHead.stdout.trim();
|
|
4454
|
+
await assertApplyLockOwned();
|
|
4455
|
+
await restoreAutoStash();
|
|
4456
|
+
await assertApplyLockOwned();
|
|
4457
|
+
if (releaseApplyLock) {
|
|
4458
|
+
try {
|
|
4459
|
+
await releaseApplyLock();
|
|
4460
|
+
releaseApplyLock = void 0;
|
|
4461
|
+
} catch {
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
await markApplied();
|
|
4465
|
+
if (options.cleanup && !options.keepArtifacts) {
|
|
4466
|
+
const trackedCleanupTargets = getTrackedCleanupTargets(manifest);
|
|
4467
|
+
await cleanupArtifacts({
|
|
4468
|
+
integrationBranch: manifest.integrationBranch,
|
|
4469
|
+
workflowId,
|
|
4470
|
+
relatedTaskIds: manifest.relatedTaskIds ?? [],
|
|
4471
|
+
trackedBranches: trackedCleanupTargets.trackedBranches,
|
|
4472
|
+
trackedWorktrees: trackedCleanupTargets.trackedWorktrees
|
|
4473
|
+
});
|
|
4474
|
+
} else if (!options.keepArtifacts) {
|
|
4475
|
+
console.log(chalk4.cyan(" Cleaning up run branch/worktree..."));
|
|
4476
|
+
await cleanupBranchAndWorktree(manifest.integrationBranch, workflowId);
|
|
4477
|
+
const trackedCleanupTargets = getTrackedCleanupTargets(manifest);
|
|
4478
|
+
await cleanupRelatedBranchesAndWorktrees({
|
|
4479
|
+
integrationBranch: manifest.integrationBranch,
|
|
4480
|
+
relatedTaskIds: manifest.relatedTaskIds ?? [],
|
|
4481
|
+
trackedBranches: trackedCleanupTargets.trackedBranches,
|
|
4482
|
+
trackedWorktrees: trackedCleanupTargets.trackedWorktrees
|
|
4483
|
+
});
|
|
4484
|
+
} else {
|
|
4485
|
+
await logPreservedRunArtifacts(manifest.integrationBranch);
|
|
4486
|
+
}
|
|
4487
|
+
const warnings = [
|
|
4488
|
+
...taskSyncFailures ? [`Task sync completed with ${taskSyncFailures.count} failure(s)`] : [],
|
|
4489
|
+
...followUpFailures ?? [],
|
|
4490
|
+
...postMergeWarnings
|
|
4491
|
+
];
|
|
4492
|
+
reportMergeOutcome(warnings);
|
|
4493
|
+
return {
|
|
4494
|
+
success: true,
|
|
4495
|
+
degraded: warnings.length > 0,
|
|
4496
|
+
workflowId,
|
|
4497
|
+
applied: true,
|
|
4498
|
+
...taskSyncFailures ? { taskSyncFailures } : {},
|
|
4499
|
+
...followUpFailures ? { followUpFailures } : {},
|
|
4500
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
4501
|
+
};
|
|
4502
|
+
} catch (error) {
|
|
4503
|
+
let errorMsg = error instanceof Error ? error.message : String(error);
|
|
4504
|
+
let leftMidMergeWarning;
|
|
4505
|
+
let commitFailureLeftStaged = false;
|
|
4506
|
+
let cleanupRan = false;
|
|
4507
|
+
try {
|
|
4508
|
+
if (!didCommit) {
|
|
4509
|
+
if (uncommittedCommitFailure && postMergeVerificationStarted) {
|
|
4510
|
+
commitFailureLeftStaged = true;
|
|
4511
|
+
if (didAutoStash) {
|
|
4512
|
+
leftMidMergeWarning = "Rollback was skipped; the staged merge was left in place.";
|
|
4513
|
+
}
|
|
4514
|
+
} else if (postMergeVerificationStarted) {
|
|
4515
|
+
leftMidMergeWarning = `Post-merge verification failed; staged merge state was preserved at ${mergeCwd}. Destructive rollback was skipped because checkout ownership cannot be proven against external actors.`;
|
|
4516
|
+
} else if (normalizationStarted) {
|
|
4517
|
+
leftMidMergeWarning = `Merge normalization failed; staged merge state was preserved at ${mergeCwd}. Destructive rollback was skipped because normalization may have edited tracked files.`;
|
|
4518
|
+
} else if (applyMaterialized) {
|
|
4519
|
+
leftMidMergeWarning = `Merge applied with no new commit, then failed afterwards; the checkout was preserved at ${mergeCwd}. Destructive rollback was skipped because the apply had already materialized and restored auto-stashed changes.`;
|
|
4520
|
+
} else if (mergeMutationStarted && hadPreExistingTrackedPlanningEdits) {
|
|
4521
|
+
leftMidMergeWarning = `Merge failed after checkout mutation began; staged merge state was preserved at ${mergeCwd}. Destructive rollback was skipped because pre-existing tracked planning edits were present.`;
|
|
4522
|
+
} else if (mergeMutationStarted) {
|
|
4523
|
+
const stillOwned = await assertApplyLockOwned(true).then(
|
|
4524
|
+
() => true,
|
|
4525
|
+
() => false
|
|
4526
|
+
);
|
|
4527
|
+
if (stillOwned) {
|
|
4528
|
+
cleanupRan = true;
|
|
4529
|
+
await abortPendingSquashMerge(mergeCwd, preMergeHeadSha);
|
|
4530
|
+
} else {
|
|
4531
|
+
leftMidMergeWarning = `Merge failed after the apply lock was lost; the checkout at ${mergeCwd} was preserved. Rollback was skipped because another merge may now own it.`;
|
|
4532
|
+
console.warn(chalk4.yellow(`
|
|
4533
|
+
Warning: ${leftMidMergeWarning}
|
|
4534
|
+
`));
|
|
4535
|
+
postMergeWarnings.push(leftMidMergeWarning);
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
} else {
|
|
4539
|
+
cleanupRan = true;
|
|
4540
|
+
await execa2("git", ["merge", "--abort"], {
|
|
4541
|
+
cwd: mergeCwd,
|
|
4542
|
+
stdio: "pipe",
|
|
4543
|
+
timeout: 3e4,
|
|
4544
|
+
reject: false
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
if (cleanupRan && !postMergeVerificationStarted) {
|
|
4548
|
+
const { stdout: postAbortStatus } = await execa2("git", ["status", "--porcelain=v1"], {
|
|
4549
|
+
cwd: mergeCwd,
|
|
4550
|
+
stdio: "pipe",
|
|
4551
|
+
timeout: 3e4,
|
|
4552
|
+
reject: false
|
|
4553
|
+
});
|
|
4554
|
+
const residue = parseGitStatusPorcelain(postAbortStatus).filter(
|
|
4555
|
+
(e) => !getStatusEntryPathspecs(e).every((p) => isWorktreeRunSentinelPath(p)) && !isStatusEntryInAllowlist(e, getPlanningDirtyAllowlistPrefixes(mergeCwd))
|
|
4556
|
+
);
|
|
4557
|
+
if (residue.length > 0) {
|
|
4558
|
+
leftMidMergeWarning = `Worktree left mid-merge after rollback; manual cleanup needed at ${mergeCwd}. Unexpected residue: ${formatPathList(residue.flatMap(getStatusEntryPathspecs))}`;
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
} catch (cleanupError) {
|
|
4562
|
+
leftMidMergeWarning = `Worktree cleanup after merge failure did not complete; manual cleanup may be needed at ${mergeCwd}. Cause: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`;
|
|
4563
|
+
}
|
|
4564
|
+
if (uncommittedCommitFailure) {
|
|
4565
|
+
errorMsg = `${errorMsg}
|
|
4566
|
+
${formatFlywheelCommitRecovery({
|
|
4567
|
+
cwd: mergeCwd,
|
|
4568
|
+
workflowId,
|
|
4569
|
+
restoreSha: preMergeHeadSha,
|
|
4570
|
+
staged: commitFailureLeftStaged
|
|
4571
|
+
})}`;
|
|
4572
|
+
}
|
|
4573
|
+
if (leftMidMergeWarning && didAutoStash) {
|
|
4574
|
+
didAutoStash = false;
|
|
4575
|
+
leftMidMergeWarning += ` ${describeRetainedAutoStash(autoStashSha)}`;
|
|
4576
|
+
}
|
|
4577
|
+
if (leftMidMergeWarning) {
|
|
4578
|
+
console.warn(chalk4.yellow(`
|
|
4579
|
+
Warning: ${leftMidMergeWarning}
|
|
4580
|
+
`));
|
|
4581
|
+
postMergeWarnings.push(leftMidMergeWarning);
|
|
4582
|
+
}
|
|
4583
|
+
if (!didCommit && !applyMaterialized && completionSyncDetails.length > 0) {
|
|
4584
|
+
for (const warning of await reopenTasksArchivedWithoutCommit(completionSyncDetails)) {
|
|
4585
|
+
console.warn(chalk4.yellow(` Warning: ${warning}`));
|
|
4586
|
+
postMergeWarnings.push(warning);
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
if (codeCommitAwaitingCompletion) {
|
|
4590
|
+
const pendingError = `Code committed as ${appliedCommitSha?.slice(0, 12) ?? "(unknown SHA)"}, but task completion did not finish: ${errorMsg} ` + (codeCommitOnBase ? `Rerun \`build run flywheel merge-result ${workflowId}\` to finish it.` : "The code commit is not on the workflow base, so a rerun cannot resume it; commit the staged task metadata manually.");
|
|
4591
|
+
if (!options.dryRun && mergeLockAcquired) {
|
|
4592
|
+
try {
|
|
4593
|
+
await updateManifestApplyStatus(workflowId, { applyError: pendingError });
|
|
4594
|
+
} catch {
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
console.error(chalk4.red(`
|
|
4598
|
+
Merge incomplete: ${pendingError}
|
|
4599
|
+
`));
|
|
4600
|
+
return {
|
|
4601
|
+
success: false,
|
|
4602
|
+
workflowId,
|
|
4603
|
+
applied: true,
|
|
4604
|
+
error: pendingError,
|
|
4605
|
+
...leftMidMergeWarning ? { warnings: [leftMidMergeWarning] } : {}
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
const unverifiedCommitError = applyMaterialized && committedTreeAuthorityFailed ? committedTreeChangeReported ? errorMsg : `${errorMsg} The materialized commit ${appliedCommitSha ?? "(unknown SHA)"} was preserved for recovery and is not a verified apply.` : void 0;
|
|
4609
|
+
if (!options.dryRun && mergeLockAcquired && (mergeMutationStarted || applyMaterialized)) {
|
|
4610
|
+
try {
|
|
4611
|
+
const sourceBranch = integrationBranchForRecovery ?? null;
|
|
4612
|
+
const sourceBranchRole = sourceBranch ? classifyBranchRole(sourceBranch) : null;
|
|
4613
|
+
const applyTargetBranch = branchRoleApplyTarget;
|
|
4614
|
+
const applyTargetBranchRole = applyTargetBranch ? classifyBranchRole(applyTargetBranch, void 0, mergeCwd) : void 0;
|
|
4615
|
+
const branchRoleOverride = applyTargetBranchRole === "protected" ? options.allowProtected ? "--allow-protected" : options.allowMain ? "--allow-main" : null : null;
|
|
4616
|
+
const branchRoleErrorStatus = {
|
|
4617
|
+
sourceBranch,
|
|
4618
|
+
sourceBranchRole,
|
|
4619
|
+
applyTargetBranch,
|
|
4620
|
+
applyTargetBranchRole,
|
|
4621
|
+
branchRoleAction: branchRoleOverride ? "overridden" : "product-fix",
|
|
4622
|
+
branchRoleOverride,
|
|
4623
|
+
branchRoleRefused: false
|
|
4624
|
+
};
|
|
4625
|
+
const applyFailedIntegrationSha = await resolveIntegrationTipForRecovery(
|
|
4626
|
+
mergeCwd,
|
|
4627
|
+
integrationBranchForRecovery
|
|
4628
|
+
);
|
|
4629
|
+
await updateManifestApplyStatus(
|
|
4630
|
+
workflowId,
|
|
4631
|
+
applyMaterialized && !unverifiedCommitError ? {
|
|
4632
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4633
|
+
postApplyError: errorMsg,
|
|
4634
|
+
verifiedTreeFingerprint: applyProofTreeFingerprint(),
|
|
4635
|
+
appliedCommitSha,
|
|
4636
|
+
...branchRoleErrorStatus
|
|
4637
|
+
} : {
|
|
4638
|
+
applyError: unverifiedCommitError ?? errorMsg,
|
|
4639
|
+
...applyFailedIntegrationSha ? { applyFailedIntegrationSha } : {},
|
|
4640
|
+
// Same proof as the success path: the tree this apply was
|
|
4641
|
+
// authorized to commit. Recording it keeps reentry's refusal of
|
|
4642
|
+
// the preserved, authority-failed commit exact.
|
|
4643
|
+
...unverifiedCommitError ? { verifiedTreeFingerprint: applyProofTreeFingerprint() } : {},
|
|
4644
|
+
...branchRoleErrorStatus
|
|
4645
|
+
}
|
|
4646
|
+
);
|
|
4647
|
+
} catch {
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
if (unverifiedCommitError) {
|
|
4651
|
+
console.error(chalk4.red(`
|
|
4652
|
+
Merge failed: ${unverifiedCommitError}
|
|
4653
|
+
`));
|
|
4654
|
+
return {
|
|
4655
|
+
success: false,
|
|
4656
|
+
workflowId,
|
|
4657
|
+
applied: true,
|
|
4658
|
+
error: unverifiedCommitError,
|
|
4659
|
+
...leftMidMergeWarning ? { warnings: [leftMidMergeWarning] } : {}
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
if (applyMaterialized) {
|
|
4663
|
+
console.warn(chalk4.yellow(`
|
|
4664
|
+
Post-apply cleanup failed: ${errorMsg}
|
|
4665
|
+
`));
|
|
4666
|
+
console.warn(
|
|
4667
|
+
chalk4.yellow(" Applied changes were preserved; post-apply cleanup needs follow-up.")
|
|
4668
|
+
);
|
|
4669
|
+
return {
|
|
4670
|
+
success: true,
|
|
4671
|
+
degraded: true,
|
|
4672
|
+
workflowId,
|
|
4673
|
+
applied: true,
|
|
4674
|
+
cleanupFailed: true,
|
|
4675
|
+
postApplyError: errorMsg,
|
|
4676
|
+
warnings: [errorMsg, ...leftMidMergeWarning ? [leftMidMergeWarning] : []]
|
|
4677
|
+
};
|
|
4678
|
+
}
|
|
4679
|
+
console.error(chalk4.red(`
|
|
4680
|
+
Merge failed: ${errorMsg}
|
|
4681
|
+
`));
|
|
4682
|
+
if (error instanceof SquashMergeConflictError && integrationBranchForRecovery && currentBranchForRecovery) {
|
|
4683
|
+
try {
|
|
4684
|
+
const conflictBranch = buildConflictBranchName(workflowId);
|
|
4685
|
+
const worktreePath = path4.join(
|
|
4686
|
+
mergeCwd,
|
|
4687
|
+
".build-project",
|
|
4688
|
+
"tmp",
|
|
4689
|
+
"merge-conflicts",
|
|
4690
|
+
workflowId.replace(/[^a-zA-Z0-9._-]/g, "_")
|
|
4691
|
+
);
|
|
4692
|
+
await fs4.mkdir(worktreePath, { recursive: true });
|
|
4693
|
+
const conflictWorktreeResult = await execGit(
|
|
4694
|
+
["worktree", "add", "-b", conflictBranch, worktreePath, currentBranchForRecovery],
|
|
4695
|
+
{
|
|
4696
|
+
cwd: mergeCwd,
|
|
4697
|
+
timeoutMs: 6e4
|
|
4698
|
+
}
|
|
4699
|
+
);
|
|
4700
|
+
if (conflictWorktreeResult.exitCode !== 0) {
|
|
4701
|
+
throw new Error(
|
|
4702
|
+
conflictWorktreeResult.stderr.trim() || "git worktree add for conflict recovery failed"
|
|
4703
|
+
);
|
|
4704
|
+
}
|
|
4705
|
+
await execa2("git", ["merge", "--squash", integrationBranchForRecovery], {
|
|
4706
|
+
cwd: worktreePath,
|
|
4707
|
+
stdio: isQuietOutputRequested() ? "pipe" : "inherit",
|
|
4708
|
+
timeout: 6e4,
|
|
4709
|
+
reject: false
|
|
4710
|
+
});
|
|
4711
|
+
console.error(chalk4.yellow(" Conflict-resolution worktree created:"));
|
|
4712
|
+
console.error(chalk4.yellow(` ${worktreePath}`));
|
|
4713
|
+
console.error(
|
|
4714
|
+
chalk4.yellow(
|
|
4715
|
+
` Resolve conflicts there, commit, then cherry-pick onto ${currentBranchForRecovery} (or open a PR).`
|
|
4716
|
+
)
|
|
4717
|
+
);
|
|
4718
|
+
} catch {
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
console.error(chalk4.yellow(" Run branch preserved for manual recovery:"));
|
|
4722
|
+
console.error(
|
|
4723
|
+
chalk4.yellow(` ${integrationBranchForRecovery ?? "(unknown - manifest unavailable)"}`)
|
|
4724
|
+
);
|
|
4725
|
+
return {
|
|
4726
|
+
success: false,
|
|
4727
|
+
workflowId,
|
|
4728
|
+
error: errorMsg,
|
|
4729
|
+
...leftMidMergeWarning ? { degraded: true, warnings: [leftMidMergeWarning] } : {}
|
|
4730
|
+
};
|
|
4731
|
+
} finally {
|
|
4732
|
+
try {
|
|
4733
|
+
await restoreAutoStash();
|
|
4734
|
+
if (releaseApplyLock) {
|
|
4735
|
+
try {
|
|
4736
|
+
await releaseApplyLock();
|
|
4737
|
+
} catch {
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
if (mergeLockAcquired) {
|
|
4741
|
+
try {
|
|
4742
|
+
const currentLockPid = await readLockPid(mergeLockPath);
|
|
4743
|
+
if (currentLockPid === thisPid) {
|
|
4744
|
+
await fs4.unlink(mergeLockPath);
|
|
4745
|
+
}
|
|
4746
|
+
} catch {
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
} finally {
|
|
4750
|
+
signalFinalizer?.finish();
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
async function cleanupBranchAndWorktree(integrationBranch, workflowId) {
|
|
4755
|
+
const preserveWorktrees = readEnv(BUILD_PRESERVE_WORKTREES).value || isLabModeFromEnv();
|
|
4756
|
+
if (classifyBranchRole(integrationBranch) !== "generated-artifact") {
|
|
4757
|
+
console.log(
|
|
4758
|
+
chalk4.yellow(` Warning: Refusing to delete non-generated branch ${integrationBranch}`)
|
|
4759
|
+
);
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
const worktreeManager = WorktreeManagerPresets.orchestrator();
|
|
4763
|
+
try {
|
|
4764
|
+
const worktreePath = await findWorktreePathForBranch(integrationBranch);
|
|
4765
|
+
if (worktreePath) {
|
|
4766
|
+
await worktreeManager.cleanup(worktreePath, {
|
|
4767
|
+
preserve: preserveWorktrees
|
|
4768
|
+
});
|
|
4769
|
+
if (workflowId) {
|
|
4770
|
+
await trackWorktreeCleaned(workflowId, worktreePath);
|
|
4771
|
+
}
|
|
4772
|
+
console.log(chalk4.dim(" Worktree cleaned up"));
|
|
4773
|
+
}
|
|
4774
|
+
} catch (error) {
|
|
4775
|
+
console.log(
|
|
4776
|
+
chalk4.yellow(
|
|
4777
|
+
` Warning: Failed to cleanup worktree: ${error instanceof Error ? error.message : String(error)}`
|
|
4778
|
+
)
|
|
4779
|
+
);
|
|
4780
|
+
}
|
|
4781
|
+
try {
|
|
4782
|
+
const deleteResult = await execGit(["branch", "-D", integrationBranch], {
|
|
4783
|
+
cwd: getProjectRoot(),
|
|
4784
|
+
timeoutMs: 1e4
|
|
4785
|
+
});
|
|
4786
|
+
if (deleteResult.exitCode !== 0) {
|
|
4787
|
+
throw new Error(deleteResult.stderr.trim() || "git branch -D failed");
|
|
4788
|
+
}
|
|
4789
|
+
console.log(chalk4.dim(` Integration branch deleted: ${integrationBranch}`));
|
|
4790
|
+
} catch (error) {
|
|
4791
|
+
console.log(
|
|
4792
|
+
chalk4.yellow(
|
|
4793
|
+
` Warning: Failed to delete branch: ${error instanceof Error ? error.message : String(error)}`
|
|
4794
|
+
)
|
|
4795
|
+
);
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
async function cleanupRelatedBranchesAndWorktrees(opts) {
|
|
4799
|
+
const preserveWorktrees = readEnv(BUILD_PRESERVE_WORKTREES).value || isLabModeFromEnv();
|
|
4800
|
+
if (preserveWorktrees) return;
|
|
4801
|
+
await cleanupWorkflowRelatedBranchesAndWorktrees({
|
|
4802
|
+
integrationBranch: opts.integrationBranch,
|
|
4803
|
+
relatedTaskIds: opts.relatedTaskIds,
|
|
4804
|
+
trackedBranches: opts.trackedBranches,
|
|
4805
|
+
trackedWorktrees: opts.trackedWorktrees,
|
|
4806
|
+
dryRun: false
|
|
4807
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
async function cleanupArtifacts(opts) {
|
|
4810
|
+
console.log(chalk4.cyan(" Cleaning up run branch/worktree and artifacts..."));
|
|
4811
|
+
await cleanupBranchAndWorktree(opts.integrationBranch, opts.workflowId);
|
|
4812
|
+
await cleanupRelatedBranchesAndWorktrees({
|
|
4813
|
+
integrationBranch: opts.integrationBranch,
|
|
4814
|
+
relatedTaskIds: opts.relatedTaskIds,
|
|
4815
|
+
trackedBranches: opts.trackedBranches,
|
|
4816
|
+
trackedWorktrees: opts.trackedWorktrees
|
|
4817
|
+
});
|
|
4818
|
+
try {
|
|
4819
|
+
const artifactDir = getArtifactDir(opts.workflowId);
|
|
4820
|
+
await fs4.rm(artifactDir, { recursive: true, force: true });
|
|
4821
|
+
console.log(chalk4.dim(" Artifact directory removed"));
|
|
4822
|
+
} catch (error) {
|
|
4823
|
+
console.log(
|
|
4824
|
+
chalk4.yellow(
|
|
4825
|
+
` Warning: Failed to remove artifacts: ${error instanceof Error ? error.message : String(error)}`
|
|
4826
|
+
)
|
|
4827
|
+
);
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
async function logPreservedRunArtifacts(integrationBranch) {
|
|
4831
|
+
const worktreePath = await findWorktreePathForBranch(integrationBranch);
|
|
4832
|
+
console.log(chalk4.dim(" Run branch/worktree preserved (--keep-artifacts)"));
|
|
4833
|
+
console.log(chalk4.dim(` Branch: ${integrationBranch}`));
|
|
4834
|
+
if (worktreePath) {
|
|
4835
|
+
console.log(chalk4.dim(` Worktree: ${worktreePath}`));
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
async function findWorktreePathForBranch(branchName, repoCwd = getProjectRoot()) {
|
|
4839
|
+
const { stdout } = await execa2("git", ["worktree", "list", "--porcelain"], {
|
|
4840
|
+
cwd: repoCwd,
|
|
4841
|
+
stdio: "pipe",
|
|
4842
|
+
timeout: 1e4
|
|
4843
|
+
});
|
|
4844
|
+
const targetRef = `refs/heads/${branchName}`;
|
|
4845
|
+
let currentWorktreePath;
|
|
4846
|
+
for (const line of stdout.split("\n")) {
|
|
4847
|
+
if (line.startsWith("worktree ")) {
|
|
4848
|
+
currentWorktreePath = line.slice("worktree ".length).trim();
|
|
4849
|
+
continue;
|
|
4850
|
+
}
|
|
4851
|
+
if (line.startsWith("branch ") && currentWorktreePath) {
|
|
4852
|
+
const ref = line.slice("branch ".length).trim();
|
|
4853
|
+
if (ref === targetRef) {
|
|
4854
|
+
return currentWorktreePath;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
return void 0;
|
|
4859
|
+
}
|
|
4860
|
+
async function getGitUsername() {
|
|
4861
|
+
try {
|
|
4862
|
+
const { stdout } = await execa2("git", ["config", "user.name"], {
|
|
4863
|
+
timeout: 5e3
|
|
4864
|
+
});
|
|
4865
|
+
return stdout.trim();
|
|
4866
|
+
} catch {
|
|
4867
|
+
return "unknown";
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
async function getCurrentBranch(cwd = getProjectRoot()) {
|
|
4871
|
+
const { stdout } = await execa2("git", ["rev-parse", "--symbolic-full-name", "HEAD"], {
|
|
4872
|
+
cwd,
|
|
4873
|
+
stdio: "pipe",
|
|
4874
|
+
timeout: 5e3
|
|
4875
|
+
});
|
|
4876
|
+
return parseCurrentBranchRef(stdout);
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
export {
|
|
4880
|
+
probeGithubHost,
|
|
4881
|
+
applyTaskTransitions,
|
|
4882
|
+
preservedStageBranches,
|
|
4883
|
+
mergeWorkflowResult
|
|
4884
|
+
};
|
|
4885
|
+
//# sourceMappingURL=chunk-6KZKRTG3.js.map
|