@poetic-ai/poetic 1.42.0
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/.github/SECURITY.md +47 -0
- package/.nvmrc +1 -0
- package/.poetic/README.md +37 -0
- package/.poetic/providers/catalog.json +12051 -0
- package/.poetic/providers/pricing.json +1272 -0
- package/.poetic/providers/registry.json +3369 -0
- package/CHANGELOG.md +552 -0
- package/CODE_OF_CONDUCT.md +40 -0
- package/CONTRIBUTING.md +23 -0
- package/INSTALL.md +454 -0
- package/LICENSE +21 -0
- package/README.md +474 -0
- package/dist/BasicOptimizationCompetitionRunner-5H5TBYO4.js +153 -0
- package/dist/ExecutionTracker-UVONC4C6.js +16 -0
- package/dist/OptimizationConfig-7DFY2TST.js +17 -0
- package/dist/OptimizationEngine-JIMGP3LY.js +1597 -0
- package/dist/PromptStore-U6ZD3ZMQ.js +18 -0
- package/dist/actions-LUJPINLJ.js +322 -0
- package/dist/agent-ingest-3DJUJ7FU.js +53 -0
- package/dist/aggregator-DNCBINFO.js +12 -0
- package/dist/ai-judge-5QT5QJVU.js +156 -0
- package/dist/allowlist-grounding-RO6HKUFI.js +16 -0
- package/dist/allowlist-utils-23V7TJ3G.js +28 -0
- package/dist/anchored-turn-service-JULUCUIK.js +489 -0
- package/dist/api-transport-4TYTNLTJ.js +1087 -0
- package/dist/apply-completion-mode-AW2MOS3R.js +89 -0
- package/dist/artifact-migrator-23L45ISD.js +267 -0
- package/dist/ask-HWVV6QFR.js +135 -0
- package/dist/auth-5KEDKAPP.js +67 -0
- package/dist/auth-7TT3JINV.js +78 -0
- package/dist/auth-E6XUNZ5M.js +68 -0
- package/dist/auth-H3XDZE22.js +134 -0
- package/dist/auth-OC4E6633.js +71 -0
- package/dist/auth-S4CTX4GQ.js +44 -0
- package/dist/auth-YLH3S7EY.js +69 -0
- package/dist/auth-liveness-TDDEZY3T.js +46 -0
- package/dist/auto-optimizer-VPRWOWGU.js +80 -0
- package/dist/autoloop-cleanup-JOGKTLWA.js +109 -0
- package/dist/autoloop-evidence-gates-PCDALGVJ.js +38 -0
- package/dist/autoloop-gate-policy-Z7FVJVXV.js +175 -0
- package/dist/autoloop-helpers-2KTK43GL.js +22 -0
- package/dist/autoloop-ledger-W7QO62PX.js +96 -0
- package/dist/autoloop-ledger-subscriber-Z5RW5EIE.js +162 -0
- package/dist/autoloop-run-defaults-FERGRZGP.js +19 -0
- package/dist/autoloop-run-lock-CT2NWSV3.js +197 -0
- package/dist/autoloop-success-J2VKKBXQ.js +85 -0
- package/dist/autoloop-termination-summary-RN75F422.js +58 -0
- package/dist/autoloop-trajectory-XFIEXP5Z.js +282 -0
- package/dist/autoloop-wall-clock-T7ZVIQOC.js +12 -0
- package/dist/autonomous-loop-controller-PW4XVFGQ.js +4120 -0
- package/dist/backend-P3DAYRP7.js +29 -0
- package/dist/background-executor-CBN552V4.js +257 -0
- package/dist/backlog-execution-intent-U4XYPTV6.js +15 -0
- package/dist/backup-active-store-5WXG6SXT.js +17 -0
- package/dist/backup-telemetry-db-XGLXAA3N.js +254 -0
- package/dist/basic-competition-master-VCSBJVRN.js +344 -0
- package/dist/branch-archive-manager-RP6FP3ZC.js +254 -0
- package/dist/branch-cleanup-manager-RSHAI7YR.js +20 -0
- package/dist/build-gate-YHGRB4QB.js +71 -0
- package/dist/change-summary-WY7J5TOX.js +33 -0
- package/dist/chunk-23A26CSN.js +123 -0
- package/dist/chunk-27UTJD3M.js +265 -0
- package/dist/chunk-2A5CILN3.js +310 -0
- package/dist/chunk-2AQUWJPW.js +225 -0
- package/dist/chunk-2BZ53J6J.js +2344 -0
- package/dist/chunk-2DY5KNVM.js +276 -0
- package/dist/chunk-2EIA4RBE.js +659 -0
- package/dist/chunk-2IUXIGQD.js +1064 -0
- package/dist/chunk-2N42MS2Z.js +2193 -0
- package/dist/chunk-2QJ3H3L7.js +42 -0
- package/dist/chunk-2QLYTVYX.js +791 -0
- package/dist/chunk-2VSQMRCB.js +18 -0
- package/dist/chunk-2XQXYBM2.js +130 -0
- package/dist/chunk-2YFUIER7.js +426 -0
- package/dist/chunk-34CNP2HB.js +148 -0
- package/dist/chunk-35P3W3JX.js +456 -0
- package/dist/chunk-36PEBZAF.js +18 -0
- package/dist/chunk-3AOKYKA7.js +22 -0
- package/dist/chunk-3BCUNHKF.js +475 -0
- package/dist/chunk-3FLXLLB7.js +26 -0
- package/dist/chunk-3FOZ2VHV.js +100 -0
- package/dist/chunk-3G6FZCRF.js +106 -0
- package/dist/chunk-3M57DADF.js +44 -0
- package/dist/chunk-3S62LLWJ.js +543 -0
- package/dist/chunk-3ZPQLGV7.js +2904 -0
- package/dist/chunk-44N5WZZH.js +45 -0
- package/dist/chunk-45FPSA3B.js +125 -0
- package/dist/chunk-4BJ3O4RQ.js +929 -0
- package/dist/chunk-4BOP7T6L.js +2321 -0
- package/dist/chunk-4BTDUT2S.js +42 -0
- package/dist/chunk-4CSS7WVI.js +143 -0
- package/dist/chunk-4CUY7AFY.js +247 -0
- package/dist/chunk-4GSN5O5J.js +62 -0
- package/dist/chunk-4HN6DF7V.js +1027 -0
- package/dist/chunk-4IAPOG5V.js +415 -0
- package/dist/chunk-4IYIZQWV.js +48 -0
- package/dist/chunk-4NXPXE62.js +84 -0
- package/dist/chunk-4RCCF5BR.js +712 -0
- package/dist/chunk-4V7NAQ4R.js +19527 -0
- package/dist/chunk-4VJITV4P.js +445 -0
- package/dist/chunk-4YX7J6QL.js +1859 -0
- package/dist/chunk-527OBWQB.js +338 -0
- package/dist/chunk-5D4A7E3D.js +448 -0
- package/dist/chunk-5DLKYTQX.js +4003 -0
- package/dist/chunk-5EERLVZB.js +139 -0
- package/dist/chunk-5H72XE4H.js +256 -0
- package/dist/chunk-5HDP7XZG.js +91 -0
- package/dist/chunk-5LAMRH4X.js +969 -0
- package/dist/chunk-5LTN43AS.js +1972 -0
- package/dist/chunk-5RBHQYDB.js +2206 -0
- package/dist/chunk-5WCQRDRB.js +588 -0
- package/dist/chunk-5WRLK5KW.js +700 -0
- package/dist/chunk-5ZCXC23G.js +595 -0
- package/dist/chunk-6BM72ZMG.js +31 -0
- package/dist/chunk-6EDAJH2I.js +68 -0
- package/dist/chunk-6EQKFDBH.js +457 -0
- package/dist/chunk-6MFRHTRI.js +115 -0
- package/dist/chunk-6TZJRKNW.js +321 -0
- package/dist/chunk-6V7HGQFZ.js +20 -0
- package/dist/chunk-6VWVO2TU.js +3746 -0
- package/dist/chunk-6Y5TWI7H.js +146 -0
- package/dist/chunk-6Y5U7UFY.js +91 -0
- package/dist/chunk-6ZLAK3XG.js +14 -0
- package/dist/chunk-72XCRQED.js +34 -0
- package/dist/chunk-72YTZMAM.js +589 -0
- package/dist/chunk-73BBJOKA.js +263 -0
- package/dist/chunk-73NCTWKL.js +188 -0
- package/dist/chunk-77I6G4CO.js +1 -0
- package/dist/chunk-7DHRDWFS.js +3104 -0
- package/dist/chunk-7DNSNKJV.js +78 -0
- package/dist/chunk-7FMJVYBS.js +307 -0
- package/dist/chunk-7GDRQBQC.js +419 -0
- package/dist/chunk-7GPCEFUV.js +270 -0
- package/dist/chunk-7IL4A2PP.js +948 -0
- package/dist/chunk-7MMLMXO6.js +3679 -0
- package/dist/chunk-7QCZSLBH.js +1 -0
- package/dist/chunk-7R2WLDOS.js +62 -0
- package/dist/chunk-7T7RUJI7.js +949 -0
- package/dist/chunk-A2WKSVX7.js +43 -0
- package/dist/chunk-A3YGID55.js +133 -0
- package/dist/chunk-A722DEVA.js +14 -0
- package/dist/chunk-ACDIT7OP.js +1376 -0
- package/dist/chunk-ACKIPAOJ.js +302 -0
- package/dist/chunk-APPNGGN7.js +365 -0
- package/dist/chunk-APV6MK5E.js +137 -0
- package/dist/chunk-ASEP3M2W.js +968 -0
- package/dist/chunk-AT4TNPWW.js +2931 -0
- package/dist/chunk-ATVKCSGT.js +250 -0
- package/dist/chunk-AURFMVCD.js +899 -0
- package/dist/chunk-AX6GWE7V.js +47 -0
- package/dist/chunk-AY5EWJPF.js +385 -0
- package/dist/chunk-BAV2HJXS.js +194 -0
- package/dist/chunk-BBHRN366.js +89 -0
- package/dist/chunk-BBPSTTPA.js +1004 -0
- package/dist/chunk-BDATUDDJ.js +992 -0
- package/dist/chunk-BKE5PG5L.js +20414 -0
- package/dist/chunk-BLPHWMDK.js +797 -0
- package/dist/chunk-BSE4R6XD.js +322 -0
- package/dist/chunk-BXGMAU4A.js +396 -0
- package/dist/chunk-BXI4NXL3.js +507 -0
- package/dist/chunk-BZU2O43F.js +1125 -0
- package/dist/chunk-C3L6YQ7P.js +147 -0
- package/dist/chunk-C57KJOUQ.js +121 -0
- package/dist/chunk-CCV7BPSY.js +118 -0
- package/dist/chunk-CD7ISXA4.js +826 -0
- package/dist/chunk-CE5OZBYY.js +21 -0
- package/dist/chunk-CFBIG37O.js +154 -0
- package/dist/chunk-CJC6GZ46.js +457 -0
- package/dist/chunk-CKCDGXIV.js +408 -0
- package/dist/chunk-CKNX3TTP.js +10338 -0
- package/dist/chunk-CLKSETWE.js +10 -0
- package/dist/chunk-CP7H3HSP.js +1552 -0
- package/dist/chunk-CPLEWXNZ.js +66 -0
- package/dist/chunk-CQM3A35X.js +844 -0
- package/dist/chunk-CS6P76D3.js +103 -0
- package/dist/chunk-CTLUBNCW.js +622 -0
- package/dist/chunk-CUSRN6RJ.js +632 -0
- package/dist/chunk-D5EP5D2W.js +26 -0
- package/dist/chunk-D5RYINO7.js +412 -0
- package/dist/chunk-DBFS4HMH.js +751 -0
- package/dist/chunk-DC4ZYMXJ.js +115 -0
- package/dist/chunk-DCBZP442.js +9243 -0
- package/dist/chunk-DCYF7EKG.js +26 -0
- package/dist/chunk-DDMAFNUK.js +5351 -0
- package/dist/chunk-DF5SLDE4.js +966 -0
- package/dist/chunk-DJFW7SMX.js +348 -0
- package/dist/chunk-DQFLLBJW.js +121 -0
- package/dist/chunk-DSFM56OA.js +1203 -0
- package/dist/chunk-DTY4APYV.js +218 -0
- package/dist/chunk-DXH5JBRS.js +437 -0
- package/dist/chunk-E3NTPHEO.js +194 -0
- package/dist/chunk-E74LUIID.js +302 -0
- package/dist/chunk-EI57M5BI.js +194 -0
- package/dist/chunk-EJ4ZAWLV.js +305 -0
- package/dist/chunk-EJGCZGUF.js +1483 -0
- package/dist/chunk-EK2U3YEG.js +486 -0
- package/dist/chunk-ELMFYNV7.js +109 -0
- package/dist/chunk-ENRGKNR5.js +25 -0
- package/dist/chunk-ETYDXCK7.js +679 -0
- package/dist/chunk-EYRH3W74.js +239 -0
- package/dist/chunk-F23DIWNN.js +1024 -0
- package/dist/chunk-F5JO7HAS.js +94 -0
- package/dist/chunk-F7ER6ANO.js +174 -0
- package/dist/chunk-FATUVIIN.js +374 -0
- package/dist/chunk-FC23CTXV.js +322 -0
- package/dist/chunk-FD4ERYEG.js +22 -0
- package/dist/chunk-FH7WOJAE.js +79 -0
- package/dist/chunk-FHOWVDF5.js +1342 -0
- package/dist/chunk-FKAUPHMR.js +96 -0
- package/dist/chunk-FONNFJXA.js +137 -0
- package/dist/chunk-FQ7FSZRW.js +75 -0
- package/dist/chunk-FRG57FPM.js +240 -0
- package/dist/chunk-FUVP64ER.js +31 -0
- package/dist/chunk-FYOILAN5.js +6818 -0
- package/dist/chunk-G35UYQ6Z.js +69 -0
- package/dist/chunk-G36W2T2S.js +1143 -0
- package/dist/chunk-G3L2L5XZ.js +49 -0
- package/dist/chunk-G4THLFV3.js +148 -0
- package/dist/chunk-GAFJEYLK.js +29 -0
- package/dist/chunk-GFGYB2ZU.js +1689 -0
- package/dist/chunk-GG6KJYW6.js +85 -0
- package/dist/chunk-GHHFDU2U.js +55 -0
- package/dist/chunk-GHTZSQB7.js +57 -0
- package/dist/chunk-GK37ILCX.js +1231 -0
- package/dist/chunk-GKN3HMN4.js +2809 -0
- package/dist/chunk-GNVDBVJP.js +156 -0
- package/dist/chunk-GPEL3FBM.js +924 -0
- package/dist/chunk-GPSNGPZS.js +1604 -0
- package/dist/chunk-GSGD5E4C.js +30 -0
- package/dist/chunk-GSUQJJXQ.js +331 -0
- package/dist/chunk-GSV226X6.js +110 -0
- package/dist/chunk-H2AYQIHW.js +224 -0
- package/dist/chunk-H66DXFS5.js +1374 -0
- package/dist/chunk-H6G7QRMQ.js +35 -0
- package/dist/chunk-H7JAGHZT.js +59 -0
- package/dist/chunk-H7ZEFECA.js +316 -0
- package/dist/chunk-HAC6EHZZ.js +79 -0
- package/dist/chunk-HIO33P3G.js +19 -0
- package/dist/chunk-HMVQIPH3.js +547 -0
- package/dist/chunk-HOYBYAOA.js +257 -0
- package/dist/chunk-HX5P3CSP.js +1784 -0
- package/dist/chunk-I46EG2XQ.js +65 -0
- package/dist/chunk-I4NPDSCB.js +279 -0
- package/dist/chunk-I5WRT7ZR.js +514 -0
- package/dist/chunk-IC7I6YIJ.js +301 -0
- package/dist/chunk-IDCSOZVN.js +78 -0
- package/dist/chunk-IFCBYECK.js +112 -0
- package/dist/chunk-IFZCPEY2.js +66 -0
- package/dist/chunk-IKFP2KUR.js +48 -0
- package/dist/chunk-IRFBN336.js +878 -0
- package/dist/chunk-IUXNGKA6.js +152 -0
- package/dist/chunk-IV2RVAN7.js +1356 -0
- package/dist/chunk-IZYIQQ7X.js +53 -0
- package/dist/chunk-J3UUR7DE.js +210 -0
- package/dist/chunk-J54VUAY2.js +566 -0
- package/dist/chunk-J62KPGII.js +464 -0
- package/dist/chunk-J6P3FWWW.js +130 -0
- package/dist/chunk-JKBHJQMT.js +434 -0
- package/dist/chunk-JLS65NUQ.js +14 -0
- package/dist/chunk-JOSYEJKY.js +141 -0
- package/dist/chunk-JPKSXKNB.js +94 -0
- package/dist/chunk-JQNJB2BB.js +2440 -0
- package/dist/chunk-JRVNSWSQ.js +2626 -0
- package/dist/chunk-JSWMRQYG.js +28 -0
- package/dist/chunk-JZKNBBIB.js +152 -0
- package/dist/chunk-K54Y7MLK.js +160 -0
- package/dist/chunk-K65546RE.js +183 -0
- package/dist/chunk-KM3F7SYE.js +702 -0
- package/dist/chunk-KNDYMVWN.js +102 -0
- package/dist/chunk-KNT72VR6.js +132 -0
- package/dist/chunk-KP6YILMU.js +482 -0
- package/dist/chunk-KQEBSK3T.js +274 -0
- package/dist/chunk-KT6VHSPF.js +84 -0
- package/dist/chunk-KTMW65UX.js +14 -0
- package/dist/chunk-KZ5CJVBP.js +1813 -0
- package/dist/chunk-L4427KOE.js +26 -0
- package/dist/chunk-L6OM4A22.js +189 -0
- package/dist/chunk-L7E6XCRA.js +1042 -0
- package/dist/chunk-L7JZLXQL.js +173 -0
- package/dist/chunk-LAMKBTXX.js +40 -0
- package/dist/chunk-LDCF2MKN.js +132 -0
- package/dist/chunk-LEIIWPWQ.js +78 -0
- package/dist/chunk-LFTQVYBB.js +31 -0
- package/dist/chunk-LG3PDRTR.js +2872 -0
- package/dist/chunk-LGHII5ET.js +23 -0
- package/dist/chunk-LH5QY22X.js +1205 -0
- package/dist/chunk-LIVJ6266.js +438 -0
- package/dist/chunk-LKRAR3IH.js +276 -0
- package/dist/chunk-LO23NABI.js +3063 -0
- package/dist/chunk-LO3X5NLS.js +326 -0
- package/dist/chunk-LPITVM7M.js +339 -0
- package/dist/chunk-LR5IJIGV.js +810 -0
- package/dist/chunk-LRGMHROI.js +79 -0
- package/dist/chunk-LUVKFFWR.js +315 -0
- package/dist/chunk-LWCBNGH6.js +35 -0
- package/dist/chunk-LWTN3WRV.js +260 -0
- package/dist/chunk-M3HQYKQX.js +308 -0
- package/dist/chunk-M6GVSPEH.js +1016 -0
- package/dist/chunk-MF5HP6XV.js +221 -0
- package/dist/chunk-MHLCBZJB.js +328 -0
- package/dist/chunk-MN6PL4AN.js +120 -0
- package/dist/chunk-MOEY65Z6.js +5678 -0
- package/dist/chunk-MQ4WA34C.js +206 -0
- package/dist/chunk-MWHF5V7U.js +223 -0
- package/dist/chunk-MWHLBPNU.js +211 -0
- package/dist/chunk-MWK5UZQ3.js +1068 -0
- package/dist/chunk-MYLH2S4G.js +64 -0
- package/dist/chunk-MZ5WYKNA.js +28 -0
- package/dist/chunk-N4VTRA7K.js +131 -0
- package/dist/chunk-N5DX4JAK.js +12 -0
- package/dist/chunk-N65O4XTH.js +5011 -0
- package/dist/chunk-NEQUQHOX.js +223 -0
- package/dist/chunk-NGK6PEOG.js +25 -0
- package/dist/chunk-NJVD4PWE.js +1033 -0
- package/dist/chunk-NM6PMYY4.js +768 -0
- package/dist/chunk-NN4PJDHP.js +708 -0
- package/dist/chunk-NP2V3L7K.js +226 -0
- package/dist/chunk-NRLY536M.js +260 -0
- package/dist/chunk-NRNE225N.js +1270 -0
- package/dist/chunk-NS2K2GTP.js +605 -0
- package/dist/chunk-NS33V3IM.js +51 -0
- package/dist/chunk-NSHASMZZ.js +71 -0
- package/dist/chunk-NWOYOA6H.js +378 -0
- package/dist/chunk-NWRUSEXG.js +322 -0
- package/dist/chunk-NZGDAWIP.js +1905 -0
- package/dist/chunk-O47UOKU3.js +395 -0
- package/dist/chunk-O4G2W3ST.js +9831 -0
- package/dist/chunk-O6V4PNLO.js +63 -0
- package/dist/chunk-OA4GPILI.js +624 -0
- package/dist/chunk-OGMSYL6J.js +227 -0
- package/dist/chunk-OIS7J26S.js +319 -0
- package/dist/chunk-OMX5VL43.js +79 -0
- package/dist/chunk-ON3G73BU.js +264 -0
- package/dist/chunk-OPYFYTWQ.js +220 -0
- package/dist/chunk-OQ6K46CK.js +1734 -0
- package/dist/chunk-OYZYO7TV.js +47 -0
- package/dist/chunk-OZ4REHIN.js +4386 -0
- package/dist/chunk-P47VN5F4.js +282 -0
- package/dist/chunk-P5I4X7A7.js +353 -0
- package/dist/chunk-P6VQROCO.js +594 -0
- package/dist/chunk-P7CQGPLS.js +30 -0
- package/dist/chunk-PCAGC5MR.js +1763 -0
- package/dist/chunk-PFTQBXS6.js +5181 -0
- package/dist/chunk-PGSPX4SU.js +15 -0
- package/dist/chunk-PIKLF7BM.js +432 -0
- package/dist/chunk-PJWJ3SAV.js +22 -0
- package/dist/chunk-PKIFMV72.js +100 -0
- package/dist/chunk-PL37JDNA.js +189 -0
- package/dist/chunk-PTH5E5XO.js +195 -0
- package/dist/chunk-PW6AXLQC.js +4128 -0
- package/dist/chunk-PYITM4N2.js +79 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-Q3RO7N35.js +45 -0
- package/dist/chunk-Q3WIC6GQ.js +2670 -0
- package/dist/chunk-Q5XIXSHX.js +271 -0
- package/dist/chunk-QGJDMEOV.js +34 -0
- package/dist/chunk-QNOB37UH.js +58 -0
- package/dist/chunk-QOCOVLUH.js +462 -0
- package/dist/chunk-QTCJ5CC5.js +172 -0
- package/dist/chunk-QVFV5IP2.js +232 -0
- package/dist/chunk-QVZMFDYG.js +29 -0
- package/dist/chunk-R35PEUKH.js +511 -0
- package/dist/chunk-R4UWBC35.js +200 -0
- package/dist/chunk-R6MLCREW.js +44 -0
- package/dist/chunk-R6W23LKN.js +254 -0
- package/dist/chunk-R7XDDX2A.js +4007 -0
- package/dist/chunk-RDFRCT64.js +168 -0
- package/dist/chunk-RE5VZDFQ.js +58 -0
- package/dist/chunk-RGROYC2G.js +638 -0
- package/dist/chunk-RGWHUI5E.js +68 -0
- package/dist/chunk-RHVG6UNL.js +205 -0
- package/dist/chunk-RI4EX2QE.js +18 -0
- package/dist/chunk-RJAAKDRX.js +17 -0
- package/dist/chunk-ROMHOSMQ.js +185 -0
- package/dist/chunk-RWREC3KJ.js +517 -0
- package/dist/chunk-RYNETRDJ.js +118 -0
- package/dist/chunk-RZY7RKM5.js +500 -0
- package/dist/chunk-S2MNWFAG.js +52 -0
- package/dist/chunk-S2VQCZO4.js +22 -0
- package/dist/chunk-S54MKU6V.js +117 -0
- package/dist/chunk-S5OIDFRM.js +302 -0
- package/dist/chunk-SCW4ZF6R.js +150 -0
- package/dist/chunk-SLS2N4PH.js +2213 -0
- package/dist/chunk-SNLBO4BF.js +247 -0
- package/dist/chunk-SP3LFU6B.js +60 -0
- package/dist/chunk-SQQTDDQA.js +537 -0
- package/dist/chunk-STV6LYYE.js +45 -0
- package/dist/chunk-SURZ2WFE.js +326 -0
- package/dist/chunk-SZ7DL357.js +1047 -0
- package/dist/chunk-T5E4NQCM.js +5742 -0
- package/dist/chunk-TAJPCXSB.js +13 -0
- package/dist/chunk-TAOOYK3P.js +358 -0
- package/dist/chunk-TDSEX5CF.js +24 -0
- package/dist/chunk-TFTGP7Z7.js +174 -0
- package/dist/chunk-TGON4N4O.js +197 -0
- package/dist/chunk-TGQZHDY2.js +2373 -0
- package/dist/chunk-TJJZ2OKV.js +6783 -0
- package/dist/chunk-TRSVUPCX.js +25 -0
- package/dist/chunk-TSXGRLPR.js +107 -0
- package/dist/chunk-U2S3YEX3.js +224 -0
- package/dist/chunk-U45RTRGY.js +818 -0
- package/dist/chunk-U6C3VVWS.js +13 -0
- package/dist/chunk-UDMS5AD3.js +307 -0
- package/dist/chunk-UGX7GI37.js +94 -0
- package/dist/chunk-UH3JJSHK.js +1 -0
- package/dist/chunk-UHRBTZYY.js +208 -0
- package/dist/chunk-UHT2KRG5.js +193 -0
- package/dist/chunk-UI2F6DJ5.js +118 -0
- package/dist/chunk-UMAKSB4Z.js +284 -0
- package/dist/chunk-UR2IBGIM.js +521 -0
- package/dist/chunk-UUH2RVKS.js +585 -0
- package/dist/chunk-UVJ4PJDK.js +4533 -0
- package/dist/chunk-V53UG2OT.js +9924 -0
- package/dist/chunk-V72RMBM4.js +48 -0
- package/dist/chunk-VBNCKCCI.js +16 -0
- package/dist/chunk-VNSYKC25.js +24 -0
- package/dist/chunk-VPHVFK4A.js +1984 -0
- package/dist/chunk-VTLNJQ44.js +133 -0
- package/dist/chunk-VTW7HP3R.js +100 -0
- package/dist/chunk-VVGEPBPS.js +218 -0
- package/dist/chunk-VVLLL7I4.js +166 -0
- package/dist/chunk-VX3UMWB7.js +483 -0
- package/dist/chunk-VXF4Q3FW.js +20 -0
- package/dist/chunk-W3JMT2YY.js +712 -0
- package/dist/chunk-WDUORIHF.js +877 -0
- package/dist/chunk-WDVTY4U6.js +148 -0
- package/dist/chunk-WIFZCHEG.js +12566 -0
- package/dist/chunk-WIRN3F75.js +736 -0
- package/dist/chunk-WMPU2UOV.js +82 -0
- package/dist/chunk-WTOKKE2V.js +784 -0
- package/dist/chunk-WX5IRLF6.js +740 -0
- package/dist/chunk-WZ6U3O7I.js +1581 -0
- package/dist/chunk-XAONGNST.js +57 -0
- package/dist/chunk-XB5YLCLB.js +13 -0
- package/dist/chunk-XEP5NQ5Y.js +688 -0
- package/dist/chunk-XGYF2QMZ.js +351 -0
- package/dist/chunk-XHEKQENB.js +255 -0
- package/dist/chunk-XNO4FM6X.js +1124 -0
- package/dist/chunk-XPHKYHO4.js +878 -0
- package/dist/chunk-XPK2I4NC.js +405 -0
- package/dist/chunk-XPU2XADL.js +2175 -0
- package/dist/chunk-XQD2B6BB.js +507 -0
- package/dist/chunk-XT2ZQVTC.js +558 -0
- package/dist/chunk-XTXJGGUV.js +351 -0
- package/dist/chunk-XUXVDPSZ.js +50 -0
- package/dist/chunk-XV7NZL4B.js +57 -0
- package/dist/chunk-XZE4XARH.js +563 -0
- package/dist/chunk-XZYR5EDO.js +152 -0
- package/dist/chunk-Y3J4JMTU.js +61 -0
- package/dist/chunk-Y55LZN5U.js +52 -0
- package/dist/chunk-Y5VN7DMP.js +112 -0
- package/dist/chunk-Y67VAHN4.js +459 -0
- package/dist/chunk-YAXSKEUH.js +3544 -0
- package/dist/chunk-YB3JD4E6.js +975 -0
- package/dist/chunk-YESZJFK6.js +220 -0
- package/dist/chunk-YGXKOBQQ.js +292 -0
- package/dist/chunk-YMX76IOS.js +31 -0
- package/dist/chunk-YOTXESEH.js +257 -0
- package/dist/chunk-YTYIEZQY.js +119 -0
- package/dist/chunk-YVORHQ2S.js +579 -0
- package/dist/chunk-Z2CUQDAR.js +3429 -0
- package/dist/chunk-Z2KYOFHS.js +7000 -0
- package/dist/chunk-Z6PC7QX6.js +161 -0
- package/dist/chunk-ZDTGECTN.js +235 -0
- package/dist/chunk-ZIPWI2LY.js +151 -0
- package/dist/chunk-ZSKKMTQJ.js +554 -0
- package/dist/chunk-ZTT5IVPY.js +1160 -0
- package/dist/chunk-ZX65UI5W.js +78 -0
- package/dist/chunk-ZYG7GGKO.js +218 -0
- package/dist/chunk-ZZZUL4OF.js +38 -0
- package/dist/cli-utils-IXL26KJT.js +40 -0
- package/dist/cli-validation-WLXWLUJL.js +111 -0
- package/dist/commit-utils-P7CRJF5N.js +202 -0
- package/dist/compete-config-resolver-R5ULXMYH.js +76 -0
- package/dist/compete-request-62JESVET.js +364 -0
- package/dist/compete-results-QEPG6FZX.js +208 -0
- package/dist/competition-outcome-tracker-R32OOZ43.js +38 -0
- package/dist/config-JCOKXOQT.js +166 -0
- package/dist/config-audit-EL3GHXS7.js +360 -0
- package/dist/config-explain-TNILDEVJ.js +12 -0
- package/dist/config-loader-VPFPFO4B.js +38 -0
- package/dist/config-manager-7T4E2BQO.js +67 -0
- package/dist/config-validator-KKOHOW7O.js +72 -0
- package/dist/cost-YC42XUF3.js +53 -0
- package/dist/coverage-orchestrator-CISNKJC2.js +772 -0
- package/dist/coverage-scanner-YVD62MLH.js +12 -0
- package/dist/createCompetitionRunner-LJG6ZQSO.js +46 -0
- package/dist/data-migration-state-VJK64DLT.js +31 -0
- package/dist/db-migrator-XSEIOGWY.js +34 -0
- package/dist/discovery-JGYGDD25.js +25 -0
- package/dist/doctor-GJOYE7AE.js +186 -0
- package/dist/domain-analyzer-AWF3RP6E.js +29 -0
- package/dist/ensure-initialized-744DIZEZ.js +52 -0
- package/dist/entry.js +940 -0
- package/dist/environment-YS45U5ZI.js +81 -0
- package/dist/error-parser-core-PPZ36A4S.js +50 -0
- package/dist/escalation-ladder-MKTCEGSA.js +130 -0
- package/dist/evaluation-context-KV4GZNN7.js +35 -0
- package/dist/evidence-plan-resolver-GBSYSAPX.js +85 -0
- package/dist/execution-data-writer-3ODTBDLH.js +53 -0
- package/dist/execution-policy-applier-ZHD3DCF3.js +31 -0
- package/dist/execution-preflight-Z4Y64V3I.js +209 -0
- package/dist/execution-roles-R3DPPMD3.js +66 -0
- package/dist/exit-code-error-Z4SW2DVK.js +14 -0
- package/dist/external-temp-cleanup-N2RZH4QP.js +311 -0
- package/dist/factory-XBN7DFIM.js +149 -0
- package/dist/file-utils-XIVR2ZMA.js +47 -0
- package/dist/flywheel-autoloop-executor-UILGOFFK.js +249 -0
- package/dist/flywheel-competition-executor-JOBEXTRB.js +259 -0
- package/dist/flywheel-executor-6JFLA6J5.js +369 -0
- package/dist/flywheel-git-isolation-UC5NIEVU.js +48 -0
- package/dist/flywheel-manifest-5UC4VBMA.js +108 -0
- package/dist/flywheel-model-defaults-7CVKP53V.js +27 -0
- package/dist/flywheel-preflight-XEY5SHM6.js +104 -0
- package/dist/flywheel-resume-7HELH7QS.js +23 -0
- package/dist/flywheel-safety-7U7DPOAP.js +28 -0
- package/dist/flywheel-scope-decision-ZHKF6ZIN.js +11 -0
- package/dist/get-telemetry-logger-DPVWWIYY.js +73 -0
- package/dist/git-worktree-IY6V6DUO.js +18 -0
- package/dist/github-pr-manager-4LXEKJTA.js +211 -0
- package/dist/guidance-profile-config-E7XJL2VP.js +117 -0
- package/dist/guidance-profile-renderer-QK2YMAAP.js +21 -0
- package/dist/index-query-4JEUKA44.js +31 -0
- package/dist/index.js +108661 -0
- package/dist/instructions-6BURLQJC.js +64 -0
- package/dist/invoke-provider-auth-GRUYP44M.js +223 -0
- package/dist/judge-scoring.json +80 -0
- package/dist/judge-test.js +555 -0
- package/dist/lab-mode-OTWVHN63.js +39 -0
- package/dist/lab-utils-BJRFLGK4.js +23 -0
- package/dist/linux-host-class-UY4KVLFD.js +29 -0
- package/dist/llm-judge-executor-F7Z3HO77.js +133 -0
- package/dist/local-executor-CEHWE2PL.js +223 -0
- package/dist/logger-33TR6EE5.js +72 -0
- package/dist/loop-spec-XTLQAVZ7.js +191 -0
- package/dist/manager-2HJG5CLT.js +25 -0
- package/dist/matrix-prompt-builder-6UDN5X3F.js +497 -0
- package/dist/matrix-result-parser-7W35EUOV.js +15 -0
- package/dist/metadata-HQ43NDEK.js +22 -0
- package/dist/model-invocations-db-QBO4Y7WZ.js +38 -0
- package/dist/monitor-XGYBG2DW.js +72 -0
- package/dist/next-PB5ZXZKB.js +90 -0
- package/dist/next-ZBDK725T.js +69 -0
- package/dist/optimization-config-resolver-O6WKR47U.js +75 -0
- package/dist/package-3YCWIQQ5.js +10 -0
- package/dist/parallel-orchestrator-NL7YJJU2.js +297 -0
- package/dist/parallel-worker.js +279 -0
- package/dist/parse-git-status-PIAF3OTP.js +10 -0
- package/dist/path-security-ETTH6TZL.js +39 -0
- package/dist/pattern-bank-UNJR5ADV.js +15 -0
- package/dist/plan-backlog-list-fast-VAGYBV3J.js +270 -0
- package/dist/plan-backlog-show-fast-SG2CZPW3.js +406 -0
- package/dist/plan-sprint-list-fast-LURAX47V.js +162 -0
- package/dist/plan-sprint-status-fast-FJAODCHD.js +208 -0
- package/dist/plan-task-status-fast-HNYBMQI4.js +349 -0
- package/dist/plan-to-flywheel-Z4U3GWPN.js +456 -0
- package/dist/planning-backlog-3CQ5QQBQ.js +319 -0
- package/dist/poetic-root-V5DNXEAE.js +17 -0
- package/dist/preflight-OPANWMME.js +12 -0
- package/dist/process-registry-PDZJPTAS.js +27 -0
- package/dist/process-scanner-C22SYXYD.js +22 -0
- package/dist/processes-KMQUD7HY.js +29 -0
- package/dist/processes-json-fast-IBDXZ6WW.js +135 -0
- package/dist/profile-SE5EVXAP.js +149 -0
- package/dist/prompts-WZZPGWBX.js +168 -0
- package/dist/protected-branches-RNTEX7ET.js +57 -0
- package/dist/provider-aware-resource-manager-QBTZJMNP.js +18 -0
- package/dist/provider-matrix-N5X57V3Y.js +154 -0
- package/dist/provider-matrix-renderer-MZDWKKID.js +139 -0
- package/dist/provider-output-forwarding-DERID3TP.js +30 -0
- package/dist/provider-registry-J5NRBJKI.js +128 -0
- package/dist/provider-temp-cleanup-KYGLF6YU.js +14 -0
- package/dist/prune-engine-Q6E2DXBN.js +451 -0
- package/dist/quality-gate-YRHMQAEI.js +70 -0
- package/dist/quickstart-FIRLKGJI.js +62 -0
- package/dist/readiness-VZTMSG6J.js +96 -0
- package/dist/registry-YODFY77A.js +153 -0
- package/dist/repair-helpers-TTV5FEF4.js +117 -0
- package/dist/repo-root-2IGD4H7X.js +22 -0
- package/dist/resolution-engine-D6TX6FRG.js +127 -0
- package/dist/resolve-provider-cli-O2P6XZBZ.js +35 -0
- package/dist/restore-telemetry-db-BQL6DTQF.js +360 -0
- package/dist/result-streamer-WEHW476R.js +19 -0
- package/dist/routing-events-DHPUZDDF.js +27 -0
- package/dist/routing-history-store-UWCEXAP3.js +46 -0
- package/dist/routing-planner-F5WQEUOM.js +160 -0
- package/dist/run-poetic-tui-J6LWRJUP.js +13517 -0
- package/dist/run-simulation.js +376 -0
- package/dist/runner-CS2TNGHA.js +1092 -0
- package/dist/runner-HHH7ZN2Z.js +1571 -0
- package/dist/safety-OCC4KJJR.js +64 -0
- package/dist/safety-stanza-7SV3HGG3.js +60 -0
- package/dist/sandbox-32QXJEMV.js +81 -0
- package/dist/sandbox-RSO7VP6V.js +94 -0
- package/dist/schema-extensions.sql +234 -0
- package/dist/security-2KI5XNDG.js +17 -0
- package/dist/setup-bb-plugin-QAQWZCV5.js +738 -0
- package/dist/setup-claude-code-P7BTTSPO.js +317 -0
- package/dist/setup-temp-cleanup-GASCWZ2I.js +20 -0
- package/dist/shared-utils-D6U6UREP.js +37 -0
- package/dist/simple-artifact-goal-OTXNGZY6.js +25 -0
- package/dist/sprint-UIXPYOXQ.js +338 -0
- package/dist/sprint-bridge-4BHMSC3M.js +68 -0
- package/dist/sprint-execution-service-2B7WHL5L.js +311 -0
- package/dist/sprint-manager-IDQUDNQI.js +91 -0
- package/dist/sqlite-wrapper-UDWCTNLJ.js +17 -0
- package/dist/stale-cleanup-orchestrator-XWXV7KCZ.js +62 -0
- package/dist/storage-access-F6ALDN4S.js +22 -0
- package/dist/synthesis-XD6EDVHF.js +185 -0
- package/dist/task-classifier-LNLXSHOM.js +494 -0
- package/dist/task-list-fast-PTMA4LUC.js +147 -0
- package/dist/task-manager-XAT7GOWB.js +93 -0
- package/dist/task-splitter-U7DJ7W6A.js +79 -0
- package/dist/task-type-detector-OLVOMXZO.js +21 -0
- package/dist/task-type-resolver-KKNQYSB4.js +42 -0
- package/dist/telemetry-5HWBBXLX.js +131 -0
- package/dist/telemetry-health-tracker-DMSSGBZ2.js +11 -0
- package/dist/telemetry-path-resolver-7ICSG276.js +28 -0
- package/dist/telemetry-reliability-XUOZNZZ6.js +125 -0
- package/dist/token-estimator-MEQUL3EJ.js +37 -0
- package/dist/transports-KNSSV2V4.js +255 -0
- package/dist/unified-cost-tracker-4TFRBVPR.js +35 -0
- package/dist/unified-state-cleanup-LJPF2BOW.js +276 -0
- package/dist/universal-cost-calculator-BAJV35GA.js +42 -0
- package/dist/user-agents-GHUGCSKN.js +22 -0
- package/dist/variant-delivery-state-KLRNG6XA.js +33 -0
- package/dist/variant-state-cleanup-MZGGLQAJ.js +280 -0
- package/dist/variant-success-PJXWE3ZV.js +12 -0
- package/dist/variant-worker-KUWKBLAH.js +4196 -0
- package/dist/variant-worker.js +24 -0
- package/dist/variants-JE2FSAQS.js +233 -0
- package/dist/verification-toolchains-V24UHW6Q.js +28 -0
- package/dist/verify-commands-H3DTCSGI.js +37 -0
- package/dist/warning-EGQGUUIO.js +12 -0
- package/dist/work-edge-RWEDWJZF.js +49 -0
- package/dist/work-item-writer-WHAVZXAY.js +19 -0
- package/dist/worker-7SA2VS2Y.js +1435 -0
- package/dist/worker.js +19 -0
- package/dist/worktree-metrics-3DFUYVYE.js +26 -0
- package/dist/writeback-VZKHYHSN.js +44 -0
- package/docs/CLI_REFERENCE.md +7042 -0
- package/docs/PROVIDER_SETUP.md +1570 -0
- package/docs/README.md +96 -0
- package/docs/TROUBLESHOOTING.md +2232 -0
- package/docs/getting-started/QUICK_START.md +294 -0
- package/docs/getting-started/README.md +188 -0
- package/docs/getting-started/SETUP.md +52 -0
- package/docs/reference/AGENT_CONTEXT.md +38 -0
- package/docs/reference/PROVIDER_RELEASE_TIERS.md +34 -0
- package/docs/reference/README.md +381 -0
- package/docs/reference/SECURITY.md +262 -0
- package/integrations/bb-plugin-poetic/README.md +362 -0
- package/integrations/bb-plugin-poetic/app.css +341 -0
- package/integrations/bb-plugin-poetic/app.tsx +5059 -0
- package/integrations/bb-plugin-poetic/package.json +45 -0
- package/integrations/bb-plugin-poetic/server.ts +444 -0
- package/integrations/bb-plugin-poetic/src/adapter.ts +3312 -0
- package/integrations/bb-plugin-poetic/src/backlog-authoring-model.ts +150 -0
- package/integrations/bb-plugin-poetic/src/backlog-authoring-schema.ts +355 -0
- package/integrations/bb-plugin-poetic/src/backlog-authoring-service.ts +419 -0
- package/integrations/bb-plugin-poetic/src/backlog-authoring-view.ts +1026 -0
- package/integrations/bb-plugin-poetic/src/competition-defaults-schema.ts +122 -0
- package/integrations/bb-plugin-poetic/src/competition-defaults-service.ts +105 -0
- package/integrations/bb-plugin-poetic/src/competition-defaults-view.ts +138 -0
- package/integrations/bb-plugin-poetic/src/contract.ts +615 -0
- package/integrations/bb-plugin-poetic/src/finalize-model.ts +205 -0
- package/integrations/bb-plugin-poetic/src/finalize-schema.ts +158 -0
- package/integrations/bb-plugin-poetic/src/finalize-service.ts +382 -0
- package/integrations/bb-plugin-poetic/src/finalize-view.ts +159 -0
- package/integrations/bb-plugin-poetic/src/judge-operations-readback.ts +303 -0
- package/integrations/bb-plugin-poetic/src/judge-operations-schema.ts +33 -0
- package/integrations/bb-plugin-poetic/src/judge-operations-view.ts +459 -0
- package/integrations/bb-plugin-poetic/src/model.ts +1302 -0
- package/integrations/bb-plugin-poetic/src/monitor-service.ts +233 -0
- package/integrations/bb-plugin-poetic/src/panel-read-ux.ts +97 -0
- package/integrations/bb-plugin-poetic/src/patch-preview-schema.ts +166 -0
- package/integrations/bb-plugin-poetic/src/patch-preview-service.ts +480 -0
- package/integrations/bb-plugin-poetic/src/patch-preview-view.ts +109 -0
- package/integrations/bb-plugin-poetic/src/planning-readback.ts +204 -0
- package/integrations/bb-plugin-poetic/src/planning-service.ts +594 -0
- package/integrations/bb-plugin-poetic/src/planning-workspace-schema.ts +95 -0
- package/integrations/bb-plugin-poetic/src/planning-workspace.ts +323 -0
- package/integrations/bb-plugin-poetic/src/poll-handoff.ts +136 -0
- package/integrations/bb-plugin-poetic/src/project-target-server.ts +21 -0
- package/integrations/bb-plugin-poetic/src/project-target.ts +45 -0
- package/integrations/bb-plugin-poetic/src/reference-index.ts +192 -0
- package/integrations/bb-plugin-poetic/src/repository-schema.ts +30 -0
- package/integrations/bb-plugin-poetic/src/result-explorer-view.ts +597 -0
- package/integrations/bb-plugin-poetic/src/result-readback-model.ts +163 -0
- package/integrations/bb-plugin-poetic/src/result-readback-schema.ts +319 -0
- package/integrations/bb-plugin-poetic/src/result-readback-service.ts +333 -0
- package/integrations/bb-plugin-poetic/src/run-page-view.ts +177 -0
- package/integrations/bb-plugin-poetic/src/setup-config-schema.ts +291 -0
- package/integrations/bb-plugin-poetic/src/setup-config-service.ts +590 -0
- package/integrations/bb-plugin-poetic/src/setup-config-view.ts +413 -0
- package/integrations/bb-plugin-poetic/src/sprint-close-model.ts +150 -0
- package/integrations/bb-plugin-poetic/src/sprint-close-schema.ts +136 -0
- package/integrations/bb-plugin-poetic/src/sprint-close-service.ts +341 -0
- package/integrations/bb-plugin-poetic/src/sprint-close-view.ts +116 -0
- package/integrations/bb-plugin-poetic/src/sprint-composition-model.ts +714 -0
- package/integrations/bb-plugin-poetic/src/sprint-composition-readback.ts +231 -0
- package/integrations/bb-plugin-poetic/src/sprint-composition-schema.ts +601 -0
- package/integrations/bb-plugin-poetic/src/sprint-composition-service.ts +775 -0
- package/integrations/bb-plugin-poetic/src/sprint-composition-view.ts +1221 -0
- package/integrations/bb-plugin-poetic/src/task-authoring-model.ts +567 -0
- package/integrations/bb-plugin-poetic/src/task-authoring-schema.ts +635 -0
- package/integrations/bb-plugin-poetic/src/task-authoring-service.ts +842 -0
- package/integrations/bb-plugin-poetic/src/task-authoring-view.ts +1227 -0
- package/integrations/bb-plugin-poetic/src/task-detail-readback.ts +56 -0
- package/integrations/bb-plugin-poetic/src/task-detail-schema.ts +144 -0
- package/integrations/bb-plugin-poetic/src/task-navigation.ts +446 -0
- package/integrations/bb-plugin-poetic/src/workbench-route.ts +74 -0
- package/integrations/bb-plugin-poetic/tests/host-contract.test.ts +773 -0
- package/integrations/bb-plugin-poetic/tsconfig.json +18 -0
- package/integrations/bb-plugin-poetic/types/PROVENANCE.json +52 -0
- package/integrations/bb-plugin-poetic/types/bb-plugin-sdk-app.d.ts +1444 -0
- package/integrations/bb-plugin-poetic/types/bb-plugin-sdk.d.ts +13030 -0
- package/integrations/bb-plugin-poetic/vitest.host.config.ts +71 -0
- package/npm-shrinkwrap.json +4950 -0
- package/package.json +331 -0
- package/schemas/README.md +80 -0
- package/schemas/config-v1.schema.json +136 -0
- package/schemas/execution-config.schema.json +47 -0
- package/schemas/judge-scoring.schema.json +370 -0
- package/schemas/poetic.config.schema.json +2214 -0
- package/schemas/provider-config.schema.json +203 -0
- package/schemas/telemetry-config.schema.json +79 -0
- package/schemas/user-preferences.schema.json +141 -0
- package/scripts/assert-node-runtime.mjs +140 -0
- package/scripts/preflight-native.mjs +49 -0
- package/scripts/preinstall-node-check.mjs +78 -0
- package/scripts/setup-git-hooks.mjs +24 -0
- package/scripts/sync.sh +2722 -0
- package/scripts/write-node-launcher.sh +108 -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/.poetic/README.md +37 -0
- package/templates/.poetic/agents/README.md +296 -0
- package/templates/.poetic/agents/api-documenter.md +147 -0
- package/templates/.poetic/agents/backend-architect.md +31 -0
- package/templates/.poetic/agents/code-reviewer.md +157 -0
- package/templates/.poetic/agents/data-scientist.md +179 -0
- package/templates/.poetic/agents/database-optimizer.md +145 -0
- package/templates/.poetic/agents/debugger.md +31 -0
- package/templates/.poetic/agents/deployment-engineer.md +164 -0
- package/templates/.poetic/agents/devops-troubleshooter.md +139 -0
- package/templates/.poetic/agents/frontend-developer.md +150 -0
- package/templates/.poetic/agents/javascript-pro.md +36 -0
- package/templates/.poetic/agents/performance-engineer.md +151 -0
- package/templates/.poetic/agents/python-pro.md +137 -0
- package/templates/.poetic/agents/test-automator.md +147 -0
- package/templates/.poetic/agents/typescript-pro.md +34 -0
- package/templates/.poetic/config/poetic.config.jsonc +69 -0
- package/templates/.poetic/config/project-context.template.json +6 -0
- package/templates/.poetic/config/task-type-aliases.presets/kanban.yaml +14 -0
- package/templates/.poetic/config/task-type-aliases.presets/scrum.yaml +17 -0
- package/templates/.poetic/config/task-type-aliases.presets/xp.yaml +12 -0
- package/templates/.poetic/config/task-type-aliases.yaml +28 -0
- package/templates/.poetic/gitignore.template +55 -0
- package/templates/.poetic/task-types/analysis.yaml +40 -0
- package/templates/.poetic/task-types/architecture.yaml +38 -0
- package/templates/.poetic/task-types/doc.yaml +35 -0
- package/templates/.poetic/task-types/feature.yaml +23 -0
- package/templates/.poetic/task-types/general.yaml +6 -0
- package/templates/.poetic/task-types/security.yaml +39 -0
- package/templates/AGENTS.template.md +99 -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/compete-streak.yaml +78 -0
- package/templates/builtin-workflows/hello-verify.yaml +10 -0
- package/templates/builtin-workflows/judge-regression.yaml +114 -0
- package/templates/builtin-workflows/skills/code-review/SKILL.md +60 -0
- package/templates/builtin-workflows/skills/hello-verify/SKILL.md +6 -0
- package/templates/guard-kit/GUARD_SETUP.md.template +255 -0
- package/templates/guard-kit/check.mjs.template +777 -0
- package/templates/guard-kit/config.json.template +6 -0
- package/templates/guard-kit/poetic-guard.yml.template +189 -0
- package/templates/profiles/README.md +56 -0
- package/templates/profiles/frontier-claude.json +27 -0
- package/templates/profiles/frontier-codex.json +26 -0
package/scripts/sync.sh
ADDED
|
@@ -0,0 +1,2722 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# ============================================================================
|
|
5
|
+
# REPO ROOT DETECTION - Navigate to git root before any operations
|
|
6
|
+
# ============================================================================
|
|
7
|
+
# This allows the script to be called from any directory within the repo
|
|
8
|
+
# without requiring the caller to cd first.
|
|
9
|
+
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
10
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
11
|
+
echo " Error: not inside a git repository"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
cd "$REPO_ROOT"
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# This script is invoked by `poetic sync` or as a legacy fallback engine.
|
|
18
|
+
#
|
|
19
|
+
# It commits, merges, and pushes across environments, so review the script and
|
|
20
|
+
# the current repository state before running it.
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
# Commit, Merge & Sync - Multi-environment git workflow automation
|
|
24
|
+
# Usage: ./sync.sh [commit-message] [options]
|
|
25
|
+
#
|
|
26
|
+
# Options:
|
|
27
|
+
# --sync Sync mode (pull + push-if-ahead, no commit)
|
|
28
|
+
# --sync-only Sync only (skip PR phase)
|
|
29
|
+
# --pr-only PR status/ensure only (skip sync + commit)
|
|
30
|
+
# --no-sync-push In sync mode, never push after pull (alias: --pull-only)
|
|
31
|
+
# --pull-only Alias for --no-sync-push (backward compat)
|
|
32
|
+
# --ensure-pr Create PR if missing (default is show-only)
|
|
33
|
+
# --route-main Route main commits via deterministic PR branch
|
|
34
|
+
# --no-route-main Disable route-main for this run (overrides config)
|
|
35
|
+
# --stay-on-main (deprecated, now default) Return to main after routing
|
|
36
|
+
# --publish Force publishing new branches (push -u) when no upstream exists
|
|
37
|
+
# --no-publish Disable publishing new branches for this run (overrides auto-publish)
|
|
38
|
+
# --push-all-branches With --all-branches, also push branches that are ahead
|
|
39
|
+
# --status Preflight-only status (no changes)
|
|
40
|
+
# --rebase Use rebase strategy (default)
|
|
41
|
+
# --merge Use merge strategy
|
|
42
|
+
# --all-branches Sync all tracked branches
|
|
43
|
+
# --dry-run Preview without making changes
|
|
44
|
+
# --pr Create a PR instead of pushing directly (enables CI/auto-fix flow)
|
|
45
|
+
# --no-pr Force direct push even on protected branches
|
|
46
|
+
# --allow-protected Allow commits to main/master/release branches (legacy)
|
|
47
|
+
# --no-protect-main Bypass protected branch prompt for this run
|
|
48
|
+
# --no-orphan-warn Suppress orphan branch warnings (Guardrail 2)
|
|
49
|
+
# --force Skip ALL guardrails (nuclear option, use sparingly)
|
|
50
|
+
# --verbose Show detailed output
|
|
51
|
+
# -h, --help Print usage and exit
|
|
52
|
+
#
|
|
53
|
+
# Exit codes:
|
|
54
|
+
# 0 Journey completed (sync and/or commit/push succeeded; warnings may appear above)
|
|
55
|
+
# 1 Failure — an operation did not complete (commit/push/merge/guard refusal, etc.)
|
|
56
|
+
#
|
|
57
|
+
# Recovery checkpoints: before the first mutating operation a checkpoint branch
|
|
58
|
+
# backup/pre-sync-<epoch> is created. Failed runs keep theirs and print its name;
|
|
59
|
+
# successful runs remove their own and also prune checkpoints orphaned by prior
|
|
60
|
+
# failed runs once they are older than the retention window AND provably preserved
|
|
61
|
+
# in HEAD (ancestor, or patch-equivalent linear range). Checkpoints with unique
|
|
62
|
+
# commits are always kept and named. POETIC_SYNC_BACKUP_RETENTION_DAYS: integer
|
|
63
|
+
# days (0 = no age gate), or "off" to disable pruning; default 7.
|
|
64
|
+
#
|
|
65
|
+
# Guardrails:
|
|
66
|
+
# 1. Protected branch check: Confirms before committing to main/master
|
|
67
|
+
# - Modes: off (no message), warn (print warning, proceed), confirm (prompt y/N), block (always block)
|
|
68
|
+
# - Precedence (highest to lowest):
|
|
69
|
+
# 1. --no-protect-main or --force flag → bypass entirely
|
|
70
|
+
# 2. POETIC_PROTECT_MAIN env var → sets mode
|
|
71
|
+
# 3. poetic config get --scope user sync.protectMain → sets mode
|
|
72
|
+
# 4. Default: confirm
|
|
73
|
+
# - In confirm mode: POETIC_ALLOW_MAIN_COMMIT=1 allows non-TTY commits (ignored in other modes)
|
|
74
|
+
# 2. Orphaned work detection: Warns about recent branches with unpushed commits (when committing)
|
|
75
|
+
# - Only checks branches with activity in last 7 days to reduce noise
|
|
76
|
+
# - Suppress with --no-orphan-warn (does NOT affect Guardrail 1)
|
|
77
|
+
# 3. Large commit preview: Shows summary when staging >10 files
|
|
78
|
+
|
|
79
|
+
# Colors for output (severity-based: 4 levels)
|
|
80
|
+
RED='\033[0;31m' # Errors that stop execution
|
|
81
|
+
YELLOW='\033[0;33m' # Warnings (non-bold)
|
|
82
|
+
GREEN='\033[0;32m' # Positive outcomes
|
|
83
|
+
DIM='\033[2m' # De-emphasized secondary info
|
|
84
|
+
NC='\033[0m'
|
|
85
|
+
|
|
86
|
+
if [ "${NO_COLOR:-}" != "" ] || [ "${FORCE_COLOR:-}" = "0" ]; then
|
|
87
|
+
RED=''
|
|
88
|
+
YELLOW=''
|
|
89
|
+
GREEN=''
|
|
90
|
+
DIM=''
|
|
91
|
+
NC=''
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Parse arguments
|
|
95
|
+
ORIGINAL_ARGS=("$@")
|
|
96
|
+
COMMIT_MESSAGE=""
|
|
97
|
+
DRY_RUN=false
|
|
98
|
+
SYNC_MODE=false
|
|
99
|
+
SYNC_ONLY=false
|
|
100
|
+
PR_ONLY=false
|
|
101
|
+
STATUS_ONLY=false
|
|
102
|
+
NO_SYNC_PUSH=false
|
|
103
|
+
PUBLISH=false
|
|
104
|
+
NO_PUBLISH=false
|
|
105
|
+
PUSH_ALL_BRANCHES=false
|
|
106
|
+
ENSURE_PR=false
|
|
107
|
+
ROUTE_MAIN_FORCE=false
|
|
108
|
+
NO_ROUTE_MAIN=false
|
|
109
|
+
STAY_ON_MAIN=false
|
|
110
|
+
REBASE=true # Default to rebase for cleaner history
|
|
111
|
+
ALL_BRANCHES=false
|
|
112
|
+
SKIP_TESTS=false
|
|
113
|
+
VERBOSE=false
|
|
114
|
+
ALLOW_PROTECTED=false # Guardrail: require explicit flag for protected branches (legacy)
|
|
115
|
+
NO_PROTECT_MAIN=false # Guardrail 1: bypass protected branch prompt for this run
|
|
116
|
+
NO_ORPHAN_WARN=false # Guardrail 2: suppress orphan branch warnings
|
|
117
|
+
FORCE=false # Guardrail: skip ALL safety checks (nuclear option)
|
|
118
|
+
CREATE_PR=false # Create PR instead of direct push (enables CI/auto-fix)
|
|
119
|
+
NO_PR=false # Force direct push even on protected branches
|
|
120
|
+
IS_PROTECTED=false # Guardrail 1: whether current branch is protected (set in guard block)
|
|
121
|
+
|
|
122
|
+
# State tracking variables (Phase 1 improvements)
|
|
123
|
+
UPSTREAM_EXISTS=false
|
|
124
|
+
UPSTREAM_BRANCH=""
|
|
125
|
+
PUSH_SUCCESS=false
|
|
126
|
+
SYNC_SUCCESS=true
|
|
127
|
+
HAS_WARNINGS=false
|
|
128
|
+
HAS_FAILURE=false
|
|
129
|
+
STASHED=false # Track whether we actually stashed changes
|
|
130
|
+
ROUTE_MAIN_NEEDED=false
|
|
131
|
+
ROUTE_MAIN_AHEAD_COUNT=0
|
|
132
|
+
ROUTE_MAIN_MODE_ACTIVE=false
|
|
133
|
+
ROUTE_MAIN_WIP_BRANCH=""
|
|
134
|
+
CREATED_PR_URL="" # Track PR URL to show helpful note when switching back to main
|
|
135
|
+
CREATED_COMMIT_SHA="" # Commit owned by this invocation (if Step 3 created one)
|
|
136
|
+
CREATED_COMMIT_PARENT_SHA=""
|
|
137
|
+
|
|
138
|
+
print_sync_usage() {
|
|
139
|
+
cat <<'EOF'
|
|
140
|
+
Commit, Merge & Sync - Multi-environment git workflow automation
|
|
141
|
+
Usage: ./sync.sh [commit-message] [options]
|
|
142
|
+
|
|
143
|
+
Options:
|
|
144
|
+
--sync Sync mode (pull + push-if-ahead, no commit)
|
|
145
|
+
--sync-only Sync only (skip PR phase)
|
|
146
|
+
--pr-only PR status/ensure only (skip sync + commit)
|
|
147
|
+
--no-sync-push In sync mode, never push after pull (alias: --pull-only)
|
|
148
|
+
--pull-only Alias for --no-sync-push (backward compat)
|
|
149
|
+
--ensure-pr Create PR if missing (default is show-only)
|
|
150
|
+
--route-main Route main commits via deterministic PR branch
|
|
151
|
+
--no-route-main Disable route-main for this run (overrides config)
|
|
152
|
+
--stay-on-main (deprecated, now default) Return to main after routing
|
|
153
|
+
--publish Force publishing new branches (push -u) when no upstream exists
|
|
154
|
+
--no-publish Disable publishing new branches for this run (overrides auto-publish)
|
|
155
|
+
--push-all-branches With --all-branches, also push branches that are ahead
|
|
156
|
+
--status Preflight-only status (no changes)
|
|
157
|
+
--rebase Use rebase strategy (default)
|
|
158
|
+
--merge Use merge strategy
|
|
159
|
+
--all-branches Sync all tracked branches
|
|
160
|
+
--dry-run Preview without making changes
|
|
161
|
+
--pr Create a PR instead of pushing directly (enables CI/auto-fix flow)
|
|
162
|
+
--no-pr Force direct push even on protected branches
|
|
163
|
+
--allow-protected Allow commits to main/master/release branches (legacy)
|
|
164
|
+
--no-protect-main Bypass protected branch prompt for this run
|
|
165
|
+
--no-orphan-warn Suppress orphan branch warnings (Guardrail 2)
|
|
166
|
+
--force Skip ALL guardrails (nuclear option, use sparingly)
|
|
167
|
+
--verbose Show detailed output
|
|
168
|
+
-h, --help Print usage and exit
|
|
169
|
+
|
|
170
|
+
Commit semantics:
|
|
171
|
+
When committing staged changes, the local commit is created before any push
|
|
172
|
+
attempt (--sync and --sync-only do not commit). If the push fails, the commit
|
|
173
|
+
remains on your current branch. Before the first mutating step, a checkpoint
|
|
174
|
+
branch backup/pre-sync-<epoch> is created for recovery; failed runs keep theirs.
|
|
175
|
+
Stale checkpoints are pruned per POETIC_SYNC_BACKUP_RETENTION_DAYS (see script header).
|
|
176
|
+
|
|
177
|
+
Exit codes:
|
|
178
|
+
0 Journey completed (sync and/or commit/push succeeded; warnings may appear above)
|
|
179
|
+
1 Failure — an operation did not complete (commit/push/merge/guard refusal, etc.)
|
|
180
|
+
EOF
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for arg in "$@"; do
|
|
184
|
+
case $arg in
|
|
185
|
+
-h|--help)
|
|
186
|
+
print_sync_usage
|
|
187
|
+
exit 0
|
|
188
|
+
;;
|
|
189
|
+
--dry-run)
|
|
190
|
+
DRY_RUN=true
|
|
191
|
+
;;
|
|
192
|
+
--status)
|
|
193
|
+
STATUS_ONLY=true
|
|
194
|
+
;;
|
|
195
|
+
--sync)
|
|
196
|
+
SYNC_MODE=true
|
|
197
|
+
;;
|
|
198
|
+
--sync-only)
|
|
199
|
+
SYNC_MODE=true
|
|
200
|
+
SYNC_ONLY=true
|
|
201
|
+
;;
|
|
202
|
+
--pr-only)
|
|
203
|
+
PR_ONLY=true
|
|
204
|
+
;;
|
|
205
|
+
--rebase)
|
|
206
|
+
REBASE=true
|
|
207
|
+
;;
|
|
208
|
+
--merge)
|
|
209
|
+
REBASE=false
|
|
210
|
+
;;
|
|
211
|
+
--all-branches)
|
|
212
|
+
ALL_BRANCHES=true
|
|
213
|
+
;;
|
|
214
|
+
--push-all-branches)
|
|
215
|
+
PUSH_ALL_BRANCHES=true
|
|
216
|
+
;;
|
|
217
|
+
--no-sync-push)
|
|
218
|
+
NO_SYNC_PUSH=true
|
|
219
|
+
;;
|
|
220
|
+
--pull-only)
|
|
221
|
+
NO_SYNC_PUSH=true
|
|
222
|
+
;;
|
|
223
|
+
--publish)
|
|
224
|
+
PUBLISH=true
|
|
225
|
+
;;
|
|
226
|
+
--no-publish)
|
|
227
|
+
NO_PUBLISH=true
|
|
228
|
+
;;
|
|
229
|
+
--ensure-pr)
|
|
230
|
+
ENSURE_PR=true
|
|
231
|
+
;;
|
|
232
|
+
--route-main)
|
|
233
|
+
ROUTE_MAIN_FORCE=true
|
|
234
|
+
;;
|
|
235
|
+
--no-route-main)
|
|
236
|
+
NO_ROUTE_MAIN=true
|
|
237
|
+
;;
|
|
238
|
+
--stay-on-main)
|
|
239
|
+
# Deprecated: return-to-main is now the default behavior.
|
|
240
|
+
# Kept for backward compatibility; flag is a no-op.
|
|
241
|
+
STAY_ON_MAIN=true
|
|
242
|
+
;;
|
|
243
|
+
--skip-tests)
|
|
244
|
+
SKIP_TESTS=true
|
|
245
|
+
;;
|
|
246
|
+
--verbose)
|
|
247
|
+
VERBOSE=true
|
|
248
|
+
;;
|
|
249
|
+
--allow-protected)
|
|
250
|
+
ALLOW_PROTECTED=true
|
|
251
|
+
NO_PROTECT_MAIN=true # --allow-protected implies --no-protect-main
|
|
252
|
+
;;
|
|
253
|
+
--no-protect-main)
|
|
254
|
+
NO_PROTECT_MAIN=true
|
|
255
|
+
# Log bypass to stderr for visibility
|
|
256
|
+
printf " ${YELLOW}%-16s %s${NC}\n" "Protect-main" "bypassed (--no-protect-main)" >&2
|
|
257
|
+
;;
|
|
258
|
+
--no-orphan-warn)
|
|
259
|
+
NO_ORPHAN_WARN=true
|
|
260
|
+
;;
|
|
261
|
+
--force)
|
|
262
|
+
FORCE=true
|
|
263
|
+
ALLOW_PROTECTED=true # --force implies --allow-protected
|
|
264
|
+
NO_PROTECT_MAIN=true # --force implies --no-protect-main
|
|
265
|
+
NO_ORPHAN_WARN=true # --force implies --no-orphan-warn
|
|
266
|
+
;;
|
|
267
|
+
--pr)
|
|
268
|
+
CREATE_PR=true
|
|
269
|
+
;;
|
|
270
|
+
--no-pr)
|
|
271
|
+
NO_PR=true
|
|
272
|
+
;;
|
|
273
|
+
--message=*|-m=*)
|
|
274
|
+
printf " ${RED}%-16s %s${NC}\n" "Error" "unknown option: $arg"
|
|
275
|
+
printf " %-16s %s\n" "Hint" 'Use positional form: poetic sync "your message" (not --message)'
|
|
276
|
+
exit 1
|
|
277
|
+
;;
|
|
278
|
+
--message|-m)
|
|
279
|
+
printf " ${RED}%-16s %s${NC}\n" "Error" "unknown option: $arg"
|
|
280
|
+
printf " %-16s %s\n" "Hint" 'Use positional form: poetic sync "your message" (not --message)'
|
|
281
|
+
exit 1
|
|
282
|
+
;;
|
|
283
|
+
--*)
|
|
284
|
+
printf " ${RED}%-16s %s${NC}\n" "Error" "unknown option: $arg"
|
|
285
|
+
exit 1
|
|
286
|
+
;;
|
|
287
|
+
*)
|
|
288
|
+
COMMIT_MESSAGE="$arg"
|
|
289
|
+
;;
|
|
290
|
+
esac
|
|
291
|
+
done
|
|
292
|
+
|
|
293
|
+
if [ "$PUBLISH" = true ] && [ "$NO_PUBLISH" = true ]; then
|
|
294
|
+
printf " ${RED}%-16s %s${NC}\n" "Error" "conflicting flags: --publish and --no-publish"
|
|
295
|
+
exit 1
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
# Header
|
|
299
|
+
echo ""
|
|
300
|
+
echo " sync — $(basename "$REPO_ROOT")"
|
|
301
|
+
printf ' %0.s' $(seq 1 40); echo ""
|
|
302
|
+
echo ""
|
|
303
|
+
|
|
304
|
+
# Helper functions — 2-space indent, 16-char left-aligned label column
|
|
305
|
+
log_ok() { printf " %-16s ${GREEN}%s${NC}\n" "$1" "$2"; }
|
|
306
|
+
log_warn() { printf " %-16s ${YELLOW}%s${NC}\n" "$1" "$2"; }
|
|
307
|
+
log_err() { printf " ${RED}%-16s %s${NC}\n" "$1" "$2"; }
|
|
308
|
+
log_dim() { printf " ${DIM}%-16s %s${NC}\n" "$1" "$2"; }
|
|
309
|
+
|
|
310
|
+
LAST_GIT_PUSH_ERROR=""
|
|
311
|
+
|
|
312
|
+
run_git_push() {
|
|
313
|
+
LAST_GIT_PUSH_ERROR=""
|
|
314
|
+
|
|
315
|
+
local err_file
|
|
316
|
+
err_file=$(mktemp 2>/dev/null) || true
|
|
317
|
+
if [ -z "${err_file:-}" ]; then
|
|
318
|
+
git push "$@" >/dev/null 2>&1
|
|
319
|
+
return $?
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
if git push "$@" >/dev/null 2>"$err_file"; then
|
|
323
|
+
rm -f "$err_file"
|
|
324
|
+
return 0
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
local filtered_error
|
|
328
|
+
filtered_error=$(
|
|
329
|
+
{
|
|
330
|
+
grep -Eiv '^(To |remote: (Resolving deltas|Compressing objects|Counting objects))' "$err_file" || true
|
|
331
|
+
} | sed 's/^remote: //' | sed '/^[[:space:]]*$/d'
|
|
332
|
+
)
|
|
333
|
+
LAST_GIT_PUSH_ERROR=$(printf '%s\n' "$filtered_error" | grep -Eiv '^hint:' | tail -n 3 || true)
|
|
334
|
+
if [ -z "$LAST_GIT_PUSH_ERROR" ]; then
|
|
335
|
+
LAST_GIT_PUSH_ERROR=$(printf '%s\n' "$filtered_error" | tail -n 3)
|
|
336
|
+
fi
|
|
337
|
+
rm -f "$err_file"
|
|
338
|
+
return 1
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
log_git_push_reason() {
|
|
342
|
+
if [ -z "$LAST_GIT_PUSH_ERROR" ]; then
|
|
343
|
+
return 0
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
local line
|
|
347
|
+
while IFS= read -r line; do
|
|
348
|
+
[ -n "$line" ] || continue
|
|
349
|
+
log_warn "Reason" "$line"
|
|
350
|
+
done <<< "$LAST_GIT_PUSH_ERROR"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
move_lock_blocked_route_ref() {
|
|
354
|
+
local branch="$1"
|
|
355
|
+
|
|
356
|
+
case "$branch" in
|
|
357
|
+
wip/route-main/*) ;;
|
|
358
|
+
*) return 1 ;;
|
|
359
|
+
esac
|
|
360
|
+
if ! git check-ref-format --branch "$branch" >/dev/null 2>&1; then
|
|
361
|
+
return 1
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
local current_branch
|
|
365
|
+
current_branch=$(git branch --show-current 2>/dev/null || true)
|
|
366
|
+
if [ "$branch" = "$current_branch" ]; then
|
|
367
|
+
return 1
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
local ref_path
|
|
371
|
+
ref_path=$(git rev-parse --git-path "refs/heads/$branch" 2>/dev/null || true)
|
|
372
|
+
if [ -z "$ref_path" ] || [ ! -f "$ref_path" ]; then
|
|
373
|
+
return 1
|
|
374
|
+
fi
|
|
375
|
+
|
|
376
|
+
local archive_root archive_path safe_name
|
|
377
|
+
archive_root="${TMPDIR:-/tmp}/poetic-merged-refs"
|
|
378
|
+
safe_name=$(echo "$branch" | sed 's#[^A-Za-z0-9._-]#_#g')
|
|
379
|
+
archive_path="$archive_root/$safe_name-$(date +%s)"
|
|
380
|
+
|
|
381
|
+
mkdir -p "$archive_root" 2>/dev/null || return 1
|
|
382
|
+
if mv "$ref_path" "$archive_path" 2>/dev/null; then
|
|
383
|
+
log_warn "Cleanup" "local ref lock blocked; moved merged $branch"
|
|
384
|
+
log_dim "Ref backup" "$archive_path"
|
|
385
|
+
return 0
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
return 1
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
delete_local_route_branch() {
|
|
392
|
+
local branch="$1"
|
|
393
|
+
local err_file
|
|
394
|
+
err_file=$(mktemp 2>/dev/null) || true
|
|
395
|
+
|
|
396
|
+
if [ -z "${err_file:-}" ]; then
|
|
397
|
+
git branch -D "$branch" >/dev/null 2>&1
|
|
398
|
+
return $?
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
if git branch -D "$branch" >/dev/null 2>"$err_file"; then
|
|
402
|
+
rm -f "$err_file"
|
|
403
|
+
return 0
|
|
404
|
+
fi
|
|
405
|
+
|
|
406
|
+
local delete_error
|
|
407
|
+
delete_error=$(cat "$err_file" 2>/dev/null || true)
|
|
408
|
+
rm -f "$err_file"
|
|
409
|
+
|
|
410
|
+
if echo "$delete_error" | grep -qi 'cannot lock ref' && \
|
|
411
|
+
echo "$delete_error" | grep -qi 'Operation not permitted'; then
|
|
412
|
+
move_lock_blocked_route_ref "$branch"
|
|
413
|
+
return $?
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
return 1
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
# Extract Poetic backlog IDs from arbitrary text.
|
|
420
|
+
# Format: 20260124T155335Z-d3b36b5e35bb
|
|
421
|
+
extract_poetic_backlog_ids() {
|
|
422
|
+
local text="${1:-}"
|
|
423
|
+
if [ -z "$text" ]; then
|
|
424
|
+
return 0
|
|
425
|
+
fi
|
|
426
|
+
echo "$text" | grep -Eo '[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}' 2>/dev/null | sort -u || true
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
is_generated_sync_artifact_path() {
|
|
430
|
+
local file="$1"
|
|
431
|
+
case "$file" in
|
|
432
|
+
schemas/poetic.config.schema.json|schemas/judge-scoring.schema.json|providers/pricing.json|providers/registry.json|.poetic/providers/pricing.json|.poetic/providers/registry.json)
|
|
433
|
+
return 0
|
|
434
|
+
;;
|
|
435
|
+
src/core/providers/generated/*|src/core/providers/routing/generated/*)
|
|
436
|
+
return 0
|
|
437
|
+
;;
|
|
438
|
+
*)
|
|
439
|
+
return 1
|
|
440
|
+
;;
|
|
441
|
+
esac
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
filter_generated_sync_artifacts() {
|
|
445
|
+
local file
|
|
446
|
+
while IFS= read -r file; do
|
|
447
|
+
[ -n "$file" ] || continue
|
|
448
|
+
if is_generated_sync_artifact_path "$file"; then
|
|
449
|
+
echo "$file"
|
|
450
|
+
fi
|
|
451
|
+
done
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
get_staged_generated_artifacts() {
|
|
455
|
+
git diff --cached --name-only 2>/dev/null | filter_generated_sync_artifacts
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
get_last_commit_generated_artifacts() {
|
|
459
|
+
git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | filter_generated_sync_artifacts
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
get_last_commit_subject() {
|
|
463
|
+
git log -1 --pretty=%s 2>/dev/null || echo ""
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
get_last_commit_sha_short() {
|
|
467
|
+
git rev-parse --short=12 HEAD 2>/dev/null || echo ""
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
# Basic PR body template for sync-created PRs.
|
|
471
|
+
# Goals:
|
|
472
|
+
# - Human skimmable (Summary/Verification)
|
|
473
|
+
# - Agent parseable (stable Poetic key/value lines)
|
|
474
|
+
build_sync_pr_body() {
|
|
475
|
+
local mode="${1:-}"
|
|
476
|
+
local head_branch="${2:-}"
|
|
477
|
+
local base_branch="${3:-}"
|
|
478
|
+
|
|
479
|
+
local subject="${COMMIT_MESSAGE:-}"
|
|
480
|
+
if [ -z "$subject" ]; then
|
|
481
|
+
subject=$(get_last_commit_subject)
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
local sha
|
|
485
|
+
sha=$(get_last_commit_sha_short)
|
|
486
|
+
|
|
487
|
+
local summary_line_1
|
|
488
|
+
case "$mode" in
|
|
489
|
+
route-main)
|
|
490
|
+
summary_line_1="Route protected-branch updates via PR"
|
|
491
|
+
;;
|
|
492
|
+
ensure-pr)
|
|
493
|
+
summary_line_1="Open PR for branch updates"
|
|
494
|
+
;;
|
|
495
|
+
pr)
|
|
496
|
+
summary_line_1="Run CI/auto-fix via PR workflow"
|
|
497
|
+
;;
|
|
498
|
+
*)
|
|
499
|
+
summary_line_1="Update"
|
|
500
|
+
;;
|
|
501
|
+
esac
|
|
502
|
+
|
|
503
|
+
local summary_line_2=""
|
|
504
|
+
if [ -n "$subject" ]; then
|
|
505
|
+
summary_line_2="Commit: $subject"
|
|
506
|
+
if [ -n "$sha" ]; then
|
|
507
|
+
summary_line_2="$summary_line_2 ($sha)"
|
|
508
|
+
fi
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
local backlog_ids
|
|
512
|
+
backlog_ids=$(extract_poetic_backlog_ids "$subject" | tr '\n' ' ' | sed 's/[[:space:]]*$//') || true
|
|
513
|
+
|
|
514
|
+
echo "Summary:"
|
|
515
|
+
echo "- $summary_line_1"
|
|
516
|
+
if [ -n "$summary_line_2" ]; then
|
|
517
|
+
echo "- $summary_line_2"
|
|
518
|
+
fi
|
|
519
|
+
echo ""
|
|
520
|
+
|
|
521
|
+
if [ "$mode" = "route-main" ]; then
|
|
522
|
+
echo "Why:"
|
|
523
|
+
echo "- Avoid direct commits to protected branches"
|
|
524
|
+
echo ""
|
|
525
|
+
fi
|
|
526
|
+
|
|
527
|
+
echo "Verification:"
|
|
528
|
+
echo "- not run (created by poetic sync; rely on CI)"
|
|
529
|
+
|
|
530
|
+
local generated_artifacts
|
|
531
|
+
generated_artifacts=$(get_last_commit_generated_artifacts)
|
|
532
|
+
if [ -n "$generated_artifacts" ]; then
|
|
533
|
+
echo ""
|
|
534
|
+
echo "Generated artifacts:"
|
|
535
|
+
echo "- Sync/build hooks updated committed generated files"
|
|
536
|
+
echo "$generated_artifacts" | sed 's/^/- /'
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
if [ -n "$backlog_ids" ]; then
|
|
540
|
+
echo ""
|
|
541
|
+
echo "Links:"
|
|
542
|
+
echo "- Backlog: $backlog_ids"
|
|
543
|
+
fi
|
|
544
|
+
|
|
545
|
+
echo ""
|
|
546
|
+
echo "Poetic:"
|
|
547
|
+
echo "- Mode: $mode"
|
|
548
|
+
echo "- Branch: $head_branch"
|
|
549
|
+
echo "- Base: $base_branch"
|
|
550
|
+
|
|
551
|
+
# Include Refs: and Poetic-Sync: lines from commit message for task linkage
|
|
552
|
+
# This enables `poetic sync reconcile` to find merged PRs by task ID
|
|
553
|
+
local commit_body
|
|
554
|
+
commit_body=$(git log -1 --pretty=%B 2>/dev/null || true)
|
|
555
|
+
local refs_line poetic_sync_line
|
|
556
|
+
refs_line=$(echo "$commit_body" | grep -E '^Refs: task/' || true)
|
|
557
|
+
poetic_sync_line=$(echo "$commit_body" | grep -E '^Poetic-Sync:' || true)
|
|
558
|
+
if [ -n "$refs_line" ] || [ -n "$poetic_sync_line" ]; then
|
|
559
|
+
echo ""
|
|
560
|
+
[ -n "$refs_line" ] && echo "$refs_line"
|
|
561
|
+
[ -n "$poetic_sync_line" ] && echo "$poetic_sync_line"
|
|
562
|
+
fi
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# Project-local post-sync hooks (optional extension points):
|
|
567
|
+
# - $REPO_ROOT/.claude/hooks/post-sync.sh
|
|
568
|
+
# - $REPO_ROOT/.claude/hooks/post-sync-cleanup.sh
|
|
569
|
+
# Notes:
|
|
570
|
+
# - Hooks are skipped in --dry-run mode.
|
|
571
|
+
# - Hook failures never fail the main sync run.
|
|
572
|
+
run_post_sync_hooks() {
|
|
573
|
+
if [ "${CLAUDE_SYNC_HOOKS_DISABLED:-}" = "1" ]; then
|
|
574
|
+
if [ "${VERBOSE}" = true ]; then
|
|
575
|
+
log_dim "Hooks" "disabled (CLAUDE_SYNC_HOOKS_DISABLED=1)"
|
|
576
|
+
fi
|
|
577
|
+
return 0
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
local hook_paths=(
|
|
581
|
+
"$REPO_ROOT/.claude/hooks/post-sync.sh"
|
|
582
|
+
"$REPO_ROOT/.claude/hooks/post-sync-cleanup.sh"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
local found_any=false
|
|
586
|
+
local seen_hooks=$'\n'
|
|
587
|
+
for hook in "${hook_paths[@]}"; do
|
|
588
|
+
if [ ! -f "$hook" ]; then
|
|
589
|
+
continue
|
|
590
|
+
fi
|
|
591
|
+
if [[ "$seen_hooks" == *$'\n'"$hook"$'\n'* ]]; then
|
|
592
|
+
continue
|
|
593
|
+
fi
|
|
594
|
+
seen_hooks+=$'\n'"${hook}"$'\n'
|
|
595
|
+
|
|
596
|
+
found_any=true
|
|
597
|
+
if [ ! -x "$hook" ]; then
|
|
598
|
+
log_warn "Hook" "not executable: $hook"
|
|
599
|
+
continue
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
log_dim "Hook" "${hook#$REPO_ROOT/}"
|
|
603
|
+
if CLAUDE_SYNC_REPO_ROOT="$REPO_ROOT" \
|
|
604
|
+
CLAUDE_SYNC_DRY_RUN="$DRY_RUN" \
|
|
605
|
+
CLAUDE_SYNC_MODE="$SYNC_MODE" \
|
|
606
|
+
CLAUDE_SYNC_ONLY="$SYNC_ONLY" \
|
|
607
|
+
CLAUDE_SYNC_PR_ONLY="$PR_ONLY" \
|
|
608
|
+
CLAUDE_SYNC_STATUS_ONLY="$STATUS_ONLY" \
|
|
609
|
+
CLAUDE_SYNC_PUSH_SUCCESS="$PUSH_SUCCESS" \
|
|
610
|
+
CLAUDE_SYNC_HAS_WARNINGS="$HAS_WARNINGS" \
|
|
611
|
+
CLAUDE_SYNC_ALREADY_FETCHED="${SYNC_FETCH_DONE:-false}" \
|
|
612
|
+
CLAUDE_SYNC_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")" \
|
|
613
|
+
bash "$hook" ${ORIGINAL_ARGS[@]+"${ORIGINAL_ARGS[@]}"}; then
|
|
614
|
+
:
|
|
615
|
+
else
|
|
616
|
+
log_warn "Hook" "failed: ${hook#$REPO_ROOT/}"
|
|
617
|
+
HAS_WARNINGS=true
|
|
618
|
+
fi
|
|
619
|
+
done
|
|
620
|
+
|
|
621
|
+
if [ "$found_any" = false ] && [ "${VERBOSE}" = true ]; then
|
|
622
|
+
log_dim "Hooks" "none found"
|
|
623
|
+
fi
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
# Normalize common truthy values to bash-friendly true/false
|
|
627
|
+
is_truthy() {
|
|
628
|
+
case "${1:-}" in
|
|
629
|
+
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
|
630
|
+
*) return 1 ;;
|
|
631
|
+
esac
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
has_tracked_changes() {
|
|
635
|
+
local tracked
|
|
636
|
+
tracked=$(git status --porcelain | grep -Ev "^[\\?!]" || true)
|
|
637
|
+
[ -n "$tracked" ]
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
# ============================================================================
|
|
641
|
+
# CONFIG CACHE - load all sync settings once at startup (avoids repeated CLI spawns)
|
|
642
|
+
# ============================================================================
|
|
643
|
+
# Priority per key: 1. env var (highest) 2. poetic config --scope user 3. default
|
|
644
|
+
#
|
|
645
|
+
# Cache is populated lazily on first access. Uses `poetic config get --scope user --json`
|
|
646
|
+
# which returns {"path":"...","scope":"user","value":...} when set, or omits "value" when not.
|
|
647
|
+
_SYNC_CONFIG_LOADED=false
|
|
648
|
+
_CFG_ROUTE_MAIN=""
|
|
649
|
+
_CFG_AUTO_PUBLISH=""
|
|
650
|
+
_CFG_PROTECT_MAIN=""
|
|
651
|
+
_CFG_CHECKOUT_MAIN=""
|
|
652
|
+
|
|
653
|
+
_load_sync_config() {
|
|
654
|
+
if [ "$_SYNC_CONFIG_LOADED" = true ]; then
|
|
655
|
+
return
|
|
656
|
+
fi
|
|
657
|
+
_SYNC_CONFIG_LOADED=true
|
|
658
|
+
|
|
659
|
+
# Only attempt CLI reads if poetic is available
|
|
660
|
+
if ! command -v poetic >/dev/null 2>&1; then
|
|
661
|
+
return
|
|
662
|
+
fi
|
|
663
|
+
|
|
664
|
+
# Batch-read all 4 sync keys via `poetic config get` (portable; no tsx/source dependency).
|
|
665
|
+
local batch_json
|
|
666
|
+
batch_json=$(
|
|
667
|
+
poetic config get sync --scope user --json 2>/dev/null | node -e '
|
|
668
|
+
let s = "";
|
|
669
|
+
process.stdin.on("data", d => s += d);
|
|
670
|
+
process.stdin.on("end", () => {
|
|
671
|
+
try {
|
|
672
|
+
const parsed = JSON.parse(s);
|
|
673
|
+
process.stdout.write(JSON.stringify(parsed.value ?? {}));
|
|
674
|
+
} catch {
|
|
675
|
+
process.stdout.write("{}");
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
' 2>/dev/null || echo "{}"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Parse batch result (single jq-free parse via grep)
|
|
682
|
+
if [ -n "$batch_json" ] && [ "$batch_json" != "{}" ]; then
|
|
683
|
+
if [ -z "${POETIC_PR_ROUTE_MAIN:-}" ]; then
|
|
684
|
+
_CFG_ROUTE_MAIN=$(echo "$batch_json" | sed -n 's/.*"routeMain"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/p' | tr -d '"' || true)
|
|
685
|
+
fi
|
|
686
|
+
if [ -z "${POETIC_SYNC_AUTO_PUBLISH:-}" ]; then
|
|
687
|
+
_CFG_AUTO_PUBLISH=$(echo "$batch_json" | sed -n 's/.*"autoPublish"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/p' | tr -d '"' || true)
|
|
688
|
+
fi
|
|
689
|
+
if [ -z "${POETIC_PROTECT_MAIN:-}" ]; then
|
|
690
|
+
_CFG_PROTECT_MAIN=$(echo "$batch_json" | sed -n 's/.*"protectMain"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' || true)
|
|
691
|
+
fi
|
|
692
|
+
if [ -z "${POETIC_SYNC_CHECKOUT_MAIN:-}" ]; then
|
|
693
|
+
_CFG_CHECKOUT_MAIN=$(echo "$batch_json" | sed -n 's/.*"checkoutMain"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/p' | tr -d '"' || true)
|
|
694
|
+
fi
|
|
695
|
+
fi
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
# Get route-main default from config or environment
|
|
699
|
+
# Priority: 1. POETIC_PR_ROUTE_MAIN env var 2. poetic config 3. default false
|
|
700
|
+
# Default is false: direct-to-main push. Enable via config for PR-centric CI workflows.
|
|
701
|
+
get_route_main_default() {
|
|
702
|
+
if [ -n "${POETIC_PR_ROUTE_MAIN:-}" ]; then
|
|
703
|
+
echo "${POETIC_PR_ROUTE_MAIN}"
|
|
704
|
+
return
|
|
705
|
+
fi
|
|
706
|
+
_load_sync_config
|
|
707
|
+
if [ -n "$_CFG_ROUTE_MAIN" ]; then
|
|
708
|
+
echo "$_CFG_ROUTE_MAIN"
|
|
709
|
+
return
|
|
710
|
+
fi
|
|
711
|
+
echo "false"
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
# Get sync auto-publish default from config or environment
|
|
715
|
+
# Priority: 1. POETIC_SYNC_AUTO_PUBLISH env var 2. poetic config 3. default false
|
|
716
|
+
get_sync_auto_publish_default() {
|
|
717
|
+
if [ -n "${POETIC_SYNC_AUTO_PUBLISH:-}" ]; then
|
|
718
|
+
echo "${POETIC_SYNC_AUTO_PUBLISH}"
|
|
719
|
+
return
|
|
720
|
+
fi
|
|
721
|
+
_load_sync_config
|
|
722
|
+
if [ -n "$_CFG_AUTO_PUBLISH" ]; then
|
|
723
|
+
echo "$_CFG_AUTO_PUBLISH"
|
|
724
|
+
return
|
|
725
|
+
fi
|
|
726
|
+
echo "false"
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
# Get sync checkout-main default from config or environment
|
|
730
|
+
# Priority: 1. POETIC_SYNC_CHECKOUT_MAIN env var 2. poetic config 3. default false
|
|
731
|
+
get_sync_checkout_main_default() {
|
|
732
|
+
if [ -n "${POETIC_SYNC_CHECKOUT_MAIN:-}" ]; then
|
|
733
|
+
echo "${POETIC_SYNC_CHECKOUT_MAIN}"
|
|
734
|
+
return
|
|
735
|
+
fi
|
|
736
|
+
_load_sync_config
|
|
737
|
+
if [ -n "$_CFG_CHECKOUT_MAIN" ]; then
|
|
738
|
+
echo "$_CFG_CHECKOUT_MAIN"
|
|
739
|
+
return
|
|
740
|
+
fi
|
|
741
|
+
echo "false"
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
is_wip_named_branch() {
|
|
745
|
+
local branch="$1"
|
|
746
|
+
case "$branch" in
|
|
747
|
+
local/*|wip/*|tmp/*|scratch/*|throwaway/*|validation/*) return 0 ;;
|
|
748
|
+
*) return 1 ;;
|
|
749
|
+
esac
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
should_publish_new_branch() {
|
|
753
|
+
local branch="$1"
|
|
754
|
+
|
|
755
|
+
if [ "$NO_PUBLISH" = true ]; then
|
|
756
|
+
return 1
|
|
757
|
+
fi
|
|
758
|
+
if [ "$PUBLISH" = true ]; then
|
|
759
|
+
return 0
|
|
760
|
+
fi
|
|
761
|
+
if is_wip_named_branch "$branch"; then
|
|
762
|
+
return 1
|
|
763
|
+
fi
|
|
764
|
+
|
|
765
|
+
local auto_publish_default
|
|
766
|
+
auto_publish_default=$(get_sync_auto_publish_default)
|
|
767
|
+
if is_truthy "$auto_publish_default"; then
|
|
768
|
+
return 0
|
|
769
|
+
fi
|
|
770
|
+
|
|
771
|
+
return 1
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
# Get protect-main mode from config or environment
|
|
775
|
+
# Priority: 1. POETIC_PROTECT_MAIN env var 2. poetic config 3. default "confirm"
|
|
776
|
+
get_protect_main_mode() {
|
|
777
|
+
if [ -n "${POETIC_PROTECT_MAIN:-}" ]; then
|
|
778
|
+
echo "${POETIC_PROTECT_MAIN}"
|
|
779
|
+
return
|
|
780
|
+
fi
|
|
781
|
+
_load_sync_config
|
|
782
|
+
if [ -n "$_CFG_PROTECT_MAIN" ]; then
|
|
783
|
+
echo "$_CFG_PROTECT_MAIN"
|
|
784
|
+
return
|
|
785
|
+
fi
|
|
786
|
+
echo "confirm"
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
# Generate commit message from staged changes
|
|
790
|
+
# Format: type(scope): subject
|
|
791
|
+
# Types: feat, fix, chore, docs, refactor, test
|
|
792
|
+
# Research: Based on task 20260124T155335Z-d3b36b5e35bb recommendations
|
|
793
|
+
generate_commit_message() {
|
|
794
|
+
# If Poetic repo with poetic CLI, try intelligent task-aware generation first
|
|
795
|
+
# Contract: poetic sync message reads --cached, returns message on stdout, exits 0
|
|
796
|
+
# On failure, exits non-zero and we fall back to diff-based generation
|
|
797
|
+
if [ -d ".poetic" ] && command -v poetic >/dev/null 2>&1; then
|
|
798
|
+
local poetic_msg
|
|
799
|
+
local poetic_status=0
|
|
800
|
+
local force_flag=""
|
|
801
|
+
if [ "$FORCE" = true ]; then force_flag="--force"; fi
|
|
802
|
+
poetic_msg=$(poetic sync message $force_flag 2>/dev/null) || poetic_status=$?
|
|
803
|
+
if [ "$poetic_status" -eq 0 ] && [ -n "$poetic_msg" ]; then
|
|
804
|
+
echo "$poetic_msg"
|
|
805
|
+
return
|
|
806
|
+
fi
|
|
807
|
+
if [ "$poetic_status" -eq 2 ]; then
|
|
808
|
+
return 2
|
|
809
|
+
fi
|
|
810
|
+
fi
|
|
811
|
+
|
|
812
|
+
local staged_files
|
|
813
|
+
staged_files=$(git diff --cached --name-only 2>/dev/null)
|
|
814
|
+
|
|
815
|
+
if [ -z "$staged_files" ]; then
|
|
816
|
+
# Fallback to timestamp if no staged files
|
|
817
|
+
echo "chore: checkpoint $(date '+%Y-%m-%d %H:%M')"
|
|
818
|
+
return
|
|
819
|
+
fi
|
|
820
|
+
|
|
821
|
+
local file_count
|
|
822
|
+
file_count=$(echo "$staged_files" | wc -l | tr -d ' ')
|
|
823
|
+
|
|
824
|
+
# Analyze file types and changes
|
|
825
|
+
local has_src=false has_test=false has_config=false has_docs=false has_build=false
|
|
826
|
+
local src_count=0 test_count=0 doc_count=0 config_count=0
|
|
827
|
+
local scope="" commit_type=""
|
|
828
|
+
|
|
829
|
+
while IFS= read -r file; do
|
|
830
|
+
case "$file" in
|
|
831
|
+
src/*|lib/*|app/*|components/*|utils/*|services/*|core/*|modules/*)
|
|
832
|
+
has_src=true
|
|
833
|
+
src_count=$((src_count + 1))
|
|
834
|
+
# Extract scope from path (first directory after src/)
|
|
835
|
+
if [ -z "$scope" ]; then
|
|
836
|
+
scope=$(echo "$file" | sed 's|^[^/]*/||' | cut -d'/' -f1)
|
|
837
|
+
fi
|
|
838
|
+
;;
|
|
839
|
+
test/*|tests/*|__tests__/*|*.test.*|*.spec.*|*_test.*|*_spec.*)
|
|
840
|
+
has_test=true
|
|
841
|
+
test_count=$((test_count + 1))
|
|
842
|
+
;;
|
|
843
|
+
*.md|docs/*|README*|CHANGELOG*|CONTRIBUTING*)
|
|
844
|
+
has_docs=true
|
|
845
|
+
doc_count=$((doc_count + 1))
|
|
846
|
+
;;
|
|
847
|
+
package.json|package-lock.json|*.config.js|*.config.ts|tsconfig.json|.gitignore|.npmignore)
|
|
848
|
+
has_config=true
|
|
849
|
+
config_count=$((config_count + 1))
|
|
850
|
+
;;
|
|
851
|
+
scripts/*|build/*|Makefile|Dockerfile|*.sh)
|
|
852
|
+
has_build=true
|
|
853
|
+
;;
|
|
854
|
+
esac
|
|
855
|
+
done <<< "$staged_files"
|
|
856
|
+
|
|
857
|
+
# Detect change type (add/modify/delete)
|
|
858
|
+
local primary_action="update"
|
|
859
|
+
local added_count deleted_count modified_count
|
|
860
|
+
added_count=$(git diff --cached --name-only --diff-filter=A | wc -l | tr -d ' ')
|
|
861
|
+
deleted_count=$(git diff --cached --name-only --diff-filter=D | wc -l | tr -d ' ')
|
|
862
|
+
modified_count=$(git diff --cached --name-only --diff-filter=M | wc -l | tr -d ' ')
|
|
863
|
+
|
|
864
|
+
if [ "$added_count" -gt "$deleted_count" ] && [ "$added_count" -gt "$modified_count" ]; then
|
|
865
|
+
primary_action="add"
|
|
866
|
+
elif [ "$deleted_count" -gt "$added_count" ] && [ "$deleted_count" -gt "$modified_count" ]; then
|
|
867
|
+
primary_action="remove"
|
|
868
|
+
fi
|
|
869
|
+
|
|
870
|
+
# Classify commit type using rule-based heuristics.
|
|
871
|
+
# Source changes take precedence over test majorities so mixed implementation
|
|
872
|
+
# commits do not get mislabeled as test-only work.
|
|
873
|
+
if [ "$has_docs" = true ] && [ "$doc_count" -ge "$((file_count * 70 / 100))" ]; then
|
|
874
|
+
# 70%+ docs files
|
|
875
|
+
commit_type="docs"
|
|
876
|
+
scope=""
|
|
877
|
+
elif [ "$has_test" = true ] && \
|
|
878
|
+
[ "$has_src" = false ] && \
|
|
879
|
+
[ "$has_build" = false ] && \
|
|
880
|
+
[ "$has_config" = false ] && \
|
|
881
|
+
[ "$test_count" -ge "$((file_count * 70 / 100))" ]; then
|
|
882
|
+
# 70%+ test files with no implementation/build/config changes
|
|
883
|
+
commit_type="test"
|
|
884
|
+
elif [ "$has_config" = true ] && [ "$config_count" -ge "$((file_count * 50 / 100))" ]; then
|
|
885
|
+
# 50%+ config files
|
|
886
|
+
commit_type="chore"
|
|
887
|
+
scope="config"
|
|
888
|
+
elif [ "$has_build" = true ] && [ "$file_count" -le 5 ]; then
|
|
889
|
+
# Build/script changes (when small batches)
|
|
890
|
+
commit_type="chore"
|
|
891
|
+
scope="build"
|
|
892
|
+
elif [ "$has_src" = true ]; then
|
|
893
|
+
# Source code changes - detect feat vs fix vs refactor
|
|
894
|
+
local diff_content
|
|
895
|
+
diff_content=$(git diff --cached 2>/dev/null)
|
|
896
|
+
|
|
897
|
+
# Heuristic: look for patterns in diff to classify feat/fix/refactor
|
|
898
|
+
if echo "$diff_content" | grep -qiE "^[+].*\b(bug|fix|issue|error|crash|fail)\b" || \
|
|
899
|
+
echo "$diff_content" | grep -qiE "^[+].*\b(correct|repair|resolve)\b"; then
|
|
900
|
+
commit_type="fix"
|
|
901
|
+
elif echo "$diff_content" | grep -qiE "^[+].*\b(refactor|rename|restructure|reorganize)\b" || \
|
|
902
|
+
[ "$added_count" -gt 0 ] && [ "$deleted_count" -gt 0 ] && [ "$added_count" -eq "$deleted_count" ]; then
|
|
903
|
+
commit_type="refactor"
|
|
904
|
+
elif [ "$primary_action" = "add" ] || echo "$diff_content" | grep -qE "^\+[^+].*\b(new|add|create|implement)\b"; then
|
|
905
|
+
commit_type="feat"
|
|
906
|
+
else
|
|
907
|
+
# Default for src changes
|
|
908
|
+
commit_type="feat"
|
|
909
|
+
fi
|
|
910
|
+
else
|
|
911
|
+
# Uncertain classification - fallback to chore
|
|
912
|
+
commit_type="chore"
|
|
913
|
+
fi
|
|
914
|
+
|
|
915
|
+
# Generate human-readable summary
|
|
916
|
+
local summary=""
|
|
917
|
+
|
|
918
|
+
if [ "$file_count" -eq 1 ]; then
|
|
919
|
+
# Single file - use filename
|
|
920
|
+
local filename
|
|
921
|
+
filename=$(basename "$(echo "$staged_files" | head -1)" | sed 's/\.[^.]*$//')
|
|
922
|
+
summary="${primary_action} ${filename}"
|
|
923
|
+
elif [ "$has_docs" = true ] && [ "$doc_count" = "$file_count" ]; then
|
|
924
|
+
# All docs
|
|
925
|
+
if [ "$file_count" -eq 1 ]; then
|
|
926
|
+
summary="update documentation"
|
|
927
|
+
else
|
|
928
|
+
summary="update documentation (${file_count} files)"
|
|
929
|
+
fi
|
|
930
|
+
elif [ "$has_test" = true ] && [ "$test_count" = "$file_count" ]; then
|
|
931
|
+
# All tests
|
|
932
|
+
summary="${primary_action} tests for ${scope:-components}"
|
|
933
|
+
elif [ "$has_config" = true ] && [ "$config_count" = "$file_count" ]; then
|
|
934
|
+
# All config
|
|
935
|
+
summary="update configuration"
|
|
936
|
+
else
|
|
937
|
+
# Mixed changes - summarize by dominant type
|
|
938
|
+
local parts=""
|
|
939
|
+
if [ "$src_count" -gt 0 ]; then
|
|
940
|
+
parts="source"
|
|
941
|
+
if [ -n "$scope" ] && [ "$src_count" -le 3 ]; then
|
|
942
|
+
parts="${scope}"
|
|
943
|
+
fi
|
|
944
|
+
fi
|
|
945
|
+
if [ "$test_count" -gt 0 ]; then
|
|
946
|
+
[ -n "$parts" ] && parts="${parts} + tests" || parts="tests"
|
|
947
|
+
fi
|
|
948
|
+
if [ "$doc_count" -gt 0 ]; then
|
|
949
|
+
[ -n "$parts" ] && parts="${parts} + docs" || parts="docs"
|
|
950
|
+
fi
|
|
951
|
+
if [ "$config_count" -gt 0 ]; then
|
|
952
|
+
[ -n "$parts" ] && parts="${parts} + config" || parts="config"
|
|
953
|
+
fi
|
|
954
|
+
|
|
955
|
+
if [ -z "$parts" ]; then
|
|
956
|
+
parts="${file_count} files"
|
|
957
|
+
fi
|
|
958
|
+
|
|
959
|
+
summary="${primary_action} ${parts}"
|
|
960
|
+
fi
|
|
961
|
+
|
|
962
|
+
# Build final message in conventional commit format
|
|
963
|
+
local message
|
|
964
|
+
if [ -n "$scope" ] && [ "$commit_type" != "docs" ]; then
|
|
965
|
+
message="${commit_type}(${scope}): ${summary}"
|
|
966
|
+
else
|
|
967
|
+
message="${commit_type}: ${summary}"
|
|
968
|
+
fi
|
|
969
|
+
|
|
970
|
+
# Truncate if too long (git recommends <72 chars for subject, we target 50-60)
|
|
971
|
+
if [ ${#message} -gt 72 ]; then
|
|
972
|
+
# Keep type and scope, truncate summary
|
|
973
|
+
local prefix
|
|
974
|
+
if [ -n "$scope" ] && [ "$commit_type" != "docs" ]; then
|
|
975
|
+
prefix="${commit_type}(${scope}): "
|
|
976
|
+
else
|
|
977
|
+
prefix="${commit_type}: "
|
|
978
|
+
fi
|
|
979
|
+
local max_summary_len=$((72 - ${#prefix} - 3)) # -3 for "..."
|
|
980
|
+
summary="${summary:0:$max_summary_len}..."
|
|
981
|
+
message="${prefix}${summary}"
|
|
982
|
+
fi
|
|
983
|
+
|
|
984
|
+
echo "$message"
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
staged_has_mixed_system_and_code() {
|
|
988
|
+
local staged_files
|
|
989
|
+
staged_files=$(git diff --cached --name-only 2>/dev/null || true)
|
|
990
|
+
|
|
991
|
+
local has_system=false
|
|
992
|
+
local has_code=false
|
|
993
|
+
|
|
994
|
+
while IFS= read -r file; do
|
|
995
|
+
[ -z "$file" ] && continue
|
|
996
|
+
case "$file" in
|
|
997
|
+
task/*|sprint/*|backlog/*)
|
|
998
|
+
has_system=true
|
|
999
|
+
;;
|
|
1000
|
+
*)
|
|
1001
|
+
has_code=true
|
|
1002
|
+
;;
|
|
1003
|
+
esac
|
|
1004
|
+
|
|
1005
|
+
if [ "$has_system" = true ] && [ "$has_code" = true ]; then
|
|
1006
|
+
return 0
|
|
1007
|
+
fi
|
|
1008
|
+
done <<EOF
|
|
1009
|
+
$staged_files
|
|
1010
|
+
EOF
|
|
1011
|
+
|
|
1012
|
+
return 1
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
# Stash recovery helper (DRY - used in multiple places)
|
|
1016
|
+
handle_stash_conflict() {
|
|
1017
|
+
local branch_context="$1"
|
|
1018
|
+
log_err "Stash" "conflict when re-applying changes"
|
|
1019
|
+
HAS_WARNINGS=true
|
|
1020
|
+
|
|
1021
|
+
# Auto-recovery: Create recovery branch from stash
|
|
1022
|
+
RECOVERY_BRANCH="recovery/stash-conflict-$(date +%s)"
|
|
1023
|
+
if git stash branch "$RECOVERY_BRANCH" >/dev/null 2>&1; then
|
|
1024
|
+
log_ok "Recovery" "$RECOVERY_BRANCH"
|
|
1025
|
+
echo ""
|
|
1026
|
+
echo "Note: You are now on $RECOVERY_BRANCH"
|
|
1027
|
+
echo "To merge back to $branch_context:"
|
|
1028
|
+
echo " 1. git checkout $branch_context"
|
|
1029
|
+
echo " 2. git merge $RECOVERY_BRANCH"
|
|
1030
|
+
echo " 3. Resolve conflicts manually"
|
|
1031
|
+
else
|
|
1032
|
+
log_err "Recovery" "failed to create branch"
|
|
1033
|
+
local orphan_ref
|
|
1034
|
+
orphan_ref=$(git stash list --grep='cm-auto-stash-' 2>/dev/null | head -1 | cut -d: -f1)
|
|
1035
|
+
if [ -n "$orphan_ref" ]; then
|
|
1036
|
+
echo " Your uncommitted changes are in stash: $orphan_ref"
|
|
1037
|
+
echo " Inspect: git stash show -p $orphan_ref"
|
|
1038
|
+
echo " Restore: git stash pop $orphan_ref"
|
|
1039
|
+
echo " Discard: git stash drop $orphan_ref"
|
|
1040
|
+
else
|
|
1041
|
+
echo "Your changes are safe in stash. Use: git stash list"
|
|
1042
|
+
fi
|
|
1043
|
+
fi
|
|
1044
|
+
exit 1
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
# ============================================================================
|
|
1048
|
+
# SHARED PULL HELPER - stash, pull/rebase, conflict recovery, unstash
|
|
1049
|
+
# ============================================================================
|
|
1050
|
+
# Encapsulates the stash → pull → conflict-abort → unstash flow used by
|
|
1051
|
+
# both sync mode and commit mode.
|
|
1052
|
+
#
|
|
1053
|
+
# Usage:
|
|
1054
|
+
# do_pull_with_stash # normal pull from upstream
|
|
1055
|
+
# do_pull_with_stash --rebase-onto origin/main # rebase onto ref (no upstream)
|
|
1056
|
+
#
|
|
1057
|
+
# Globals read:
|
|
1058
|
+
# HAS_TRACKED_CHANGES, HAS_UNTRACKED_ONLY, REBASE, CURRENT_BRANCH
|
|
1059
|
+
#
|
|
1060
|
+
# Globals written:
|
|
1061
|
+
# STASHED (true if changes were stashed and need external restore on early return)
|
|
1062
|
+
#
|
|
1063
|
+
# Returns:
|
|
1064
|
+
# 0 = success (stash restored on success path)
|
|
1065
|
+
# 1 = conflict (rebase/merge aborted, stash restored, caller should print
|
|
1066
|
+
# resolution instructions and exit)
|
|
1067
|
+
#
|
|
1068
|
+
do_pull_with_stash() {
|
|
1069
|
+
local rebase_onto=""
|
|
1070
|
+
while [ $# -gt 0 ]; do
|
|
1071
|
+
case "$1" in
|
|
1072
|
+
--rebase-onto) rebase_onto="$2"; shift 2 ;;
|
|
1073
|
+
*) shift ;;
|
|
1074
|
+
esac
|
|
1075
|
+
done
|
|
1076
|
+
|
|
1077
|
+
# --- Stash ---
|
|
1078
|
+
STASHED=false
|
|
1079
|
+
if [ "$HAS_TRACKED_CHANGES" = true ]; then
|
|
1080
|
+
ensure_backup_branch
|
|
1081
|
+
if git stash push -u -q -m "cm-auto-stash-$(date +%s)"; then
|
|
1082
|
+
STASHED=true
|
|
1083
|
+
log_dim "Stash" "saved"
|
|
1084
|
+
else
|
|
1085
|
+
log_warn "Stash" "failed, continuing without"
|
|
1086
|
+
fi
|
|
1087
|
+
elif [ "$HAS_UNTRACKED_ONLY" = true ]; then
|
|
1088
|
+
log_dim "Stash" "not needed (untracked only)"
|
|
1089
|
+
fi
|
|
1090
|
+
|
|
1091
|
+
# --- Pull strategy ---
|
|
1092
|
+
local pull_strategy="--rebase=merges"
|
|
1093
|
+
if [ "$REBASE" = false ]; then
|
|
1094
|
+
pull_strategy="--merge"
|
|
1095
|
+
fi
|
|
1096
|
+
|
|
1097
|
+
# --- Execute pull or rebase-onto ---
|
|
1098
|
+
local pull_ok=false
|
|
1099
|
+
|
|
1100
|
+
if [ -n "$rebase_onto" ]; then
|
|
1101
|
+
# No upstream: rebase or merge onto an explicit ref (route-main path)
|
|
1102
|
+
if [ "$REBASE" = true ]; then
|
|
1103
|
+
if git rebase --rebase-merges -q "$rebase_onto" >/dev/null 2>&1; then
|
|
1104
|
+
pull_ok=true
|
|
1105
|
+
log_ok "Rebase" "onto $rebase_onto"
|
|
1106
|
+
else
|
|
1107
|
+
log_err "Rebase" "failed — $rebase_onto"
|
|
1108
|
+
git rebase --abort 2>/dev/null || true
|
|
1109
|
+
fi
|
|
1110
|
+
else
|
|
1111
|
+
if git merge -q "$rebase_onto" --no-edit >/dev/null 2>&1; then
|
|
1112
|
+
pull_ok=true
|
|
1113
|
+
log_ok "Merge" "from $rebase_onto"
|
|
1114
|
+
else
|
|
1115
|
+
log_err "Merge" "failed — $rebase_onto"
|
|
1116
|
+
git merge --abort 2>/dev/null || true
|
|
1117
|
+
fi
|
|
1118
|
+
fi
|
|
1119
|
+
elif git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
|
|
1120
|
+
# Normal upstream pull
|
|
1121
|
+
ensure_backup_branch
|
|
1122
|
+
if git pull $pull_strategy >/dev/null 2>&1; then
|
|
1123
|
+
pull_ok=true
|
|
1124
|
+
log_ok "Pull" "done"
|
|
1125
|
+
else
|
|
1126
|
+
log_err "Pull" "conflict detected"
|
|
1127
|
+
git rebase --abort 2>/dev/null || git merge --abort 2>/dev/null || true
|
|
1128
|
+
fi
|
|
1129
|
+
else
|
|
1130
|
+
# No upstream, no rebase-onto: nothing to pull
|
|
1131
|
+
pull_ok=true
|
|
1132
|
+
fi
|
|
1133
|
+
|
|
1134
|
+
# --- Restore stash ---
|
|
1135
|
+
if [ "$STASHED" = true ]; then
|
|
1136
|
+
if git stash pop -q 2>/dev/null; then
|
|
1137
|
+
log_ok "Stash" "restored"
|
|
1138
|
+
STASHED=false
|
|
1139
|
+
else
|
|
1140
|
+
if [ "$pull_ok" = true ]; then
|
|
1141
|
+
# Pull succeeded but stash pop conflicted
|
|
1142
|
+
handle_stash_conflict "$CURRENT_BRANCH"
|
|
1143
|
+
# handle_stash_conflict exits; control does not return here
|
|
1144
|
+
else
|
|
1145
|
+
# Pull also failed; restore stash best-effort and report below
|
|
1146
|
+
log_warn "Stash" "restore failed — use 'git stash list' to recover"
|
|
1147
|
+
fi
|
|
1148
|
+
fi
|
|
1149
|
+
fi
|
|
1150
|
+
|
|
1151
|
+
if [ "$pull_ok" = true ]; then
|
|
1152
|
+
return 0
|
|
1153
|
+
else
|
|
1154
|
+
return 1
|
|
1155
|
+
fi
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
# Try to find an open PR URL for a head branch (returns empty string if none / gh missing)
|
|
1159
|
+
# TODO: For fork workflows, add --repo flag to scope to intended base repo
|
|
1160
|
+
# e.g., gh pr list --repo owner/repo --head ...
|
|
1161
|
+
get_open_pr_url_for_head() {
|
|
1162
|
+
local head_branch="$1"
|
|
1163
|
+
|
|
1164
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
1165
|
+
echo ""
|
|
1166
|
+
return
|
|
1167
|
+
fi
|
|
1168
|
+
|
|
1169
|
+
gh pr list \
|
|
1170
|
+
--head "$head_branch" \
|
|
1171
|
+
--state open \
|
|
1172
|
+
--limit 1 \
|
|
1173
|
+
--json url \
|
|
1174
|
+
--jq '.[0].url' 2>/dev/null || echo ""
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
try_add_automerge_label() {
|
|
1178
|
+
local pr_ref="$1" # number, URL, or branch
|
|
1179
|
+
|
|
1180
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
1181
|
+
return 1
|
|
1182
|
+
fi
|
|
1183
|
+
|
|
1184
|
+
gh pr edit "$pr_ref" --add-label "automerge" 2>/dev/null
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
ensure_pr_for_head() {
|
|
1188
|
+
local head_branch="$1"
|
|
1189
|
+
local base_branch="$2"
|
|
1190
|
+
local title="$3"
|
|
1191
|
+
local body="$4"
|
|
1192
|
+
|
|
1193
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
1194
|
+
log_warn "PR" "gh cli not found"
|
|
1195
|
+
HAS_WARNINGS=true
|
|
1196
|
+
return 1
|
|
1197
|
+
fi
|
|
1198
|
+
|
|
1199
|
+
local existing_url
|
|
1200
|
+
existing_url=$(get_open_pr_url_for_head "$head_branch")
|
|
1201
|
+
if [ -n "$existing_url" ]; then
|
|
1202
|
+
log_ok "PR" "$existing_url (existing)"
|
|
1203
|
+
CREATED_PR_URL="$existing_url"
|
|
1204
|
+
return 0
|
|
1205
|
+
fi
|
|
1206
|
+
|
|
1207
|
+
local pr_url
|
|
1208
|
+
pr_url=$(gh pr create --head "$head_branch" --base "$base_branch" --title "$title" --body "$body" 2>&1) || true
|
|
1209
|
+
if echo "$pr_url" | grep -Eq "^https?://"; then
|
|
1210
|
+
log_ok "PR" "$pr_url"
|
|
1211
|
+
CREATED_PR_URL="$pr_url"
|
|
1212
|
+
if try_add_automerge_label "$pr_url"; then
|
|
1213
|
+
log_ok "PR label" "automerge"
|
|
1214
|
+
else
|
|
1215
|
+
log_warn "PR label" "automerge not added"
|
|
1216
|
+
HAS_WARNINGS=true
|
|
1217
|
+
fi
|
|
1218
|
+
return 0
|
|
1219
|
+
fi
|
|
1220
|
+
|
|
1221
|
+
log_warn "PR" "creation failed: $pr_url"
|
|
1222
|
+
HAS_WARNINGS=true
|
|
1223
|
+
return 1
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
route_main_head_to_pr() {
|
|
1227
|
+
local base_branch="$1" # main/master
|
|
1228
|
+
local stay_on_main="$2" # true/false
|
|
1229
|
+
local short_sha
|
|
1230
|
+
local route_branch
|
|
1231
|
+
local pr_title
|
|
1232
|
+
local pr_body
|
|
1233
|
+
|
|
1234
|
+
short_sha=$(git rev-parse --short=7 HEAD 2>/dev/null || echo "")
|
|
1235
|
+
if [ -z "$short_sha" ]; then
|
|
1236
|
+
log_err "Route-main" "failed to resolve HEAD"
|
|
1237
|
+
return 1
|
|
1238
|
+
fi
|
|
1239
|
+
|
|
1240
|
+
route_branch="wip/route-main/$short_sha"
|
|
1241
|
+
|
|
1242
|
+
# Create/reset local route branch at current HEAD, then checkout
|
|
1243
|
+
if ! git checkout -B "$route_branch" HEAD >/dev/null 2>&1; then
|
|
1244
|
+
log_err "Route-main" "checkout failed: $route_branch"
|
|
1245
|
+
return 1
|
|
1246
|
+
fi
|
|
1247
|
+
CURRENT_BRANCH="$route_branch"
|
|
1248
|
+
|
|
1249
|
+
# Publish route branch (idempotent for same HEAD)
|
|
1250
|
+
if ! run_git_push -u origin "$route_branch"; then
|
|
1251
|
+
log_err "Push" "failed: origin/$route_branch"
|
|
1252
|
+
log_git_push_reason
|
|
1253
|
+
return 1
|
|
1254
|
+
fi
|
|
1255
|
+
log_ok "Push" "origin/$route_branch"
|
|
1256
|
+
UPSTREAM_EXISTS=true
|
|
1257
|
+
UPSTREAM_BRANCH="origin/$route_branch"
|
|
1258
|
+
|
|
1259
|
+
# Ensure PR for the route branch (always ensure; routing implies intent)
|
|
1260
|
+
pr_title="Route ${base_branch}: $(git log -1 --pretty=%s 2>/dev/null || echo "$short_sha")"
|
|
1261
|
+
pr_body=$(build_sync_pr_body "route-main" "$route_branch" "$base_branch")
|
|
1262
|
+
ensure_pr_for_head "$route_branch" "$base_branch" "$pr_title" "$pr_body" || true
|
|
1263
|
+
|
|
1264
|
+
if [ "$stay_on_main" = true ]; then
|
|
1265
|
+
if ! git checkout "$base_branch" >/dev/null 2>&1; then
|
|
1266
|
+
log_warn "Checkout" "failed: $base_branch"
|
|
1267
|
+
HAS_WARNINGS=true
|
|
1268
|
+
else
|
|
1269
|
+
log_dim "Checkout" "$base_branch"
|
|
1270
|
+
CURRENT_BRANCH="$base_branch"
|
|
1271
|
+
# Fast-forward local base branch if behind origin (fetch already ran)
|
|
1272
|
+
local behind_count
|
|
1273
|
+
behind_count=$(git rev-list --count "HEAD..origin/$base_branch" 2>/dev/null || echo "0")
|
|
1274
|
+
if [ "$behind_count" -gt 0 ]; then
|
|
1275
|
+
if git merge --ff-only "origin/$base_branch" >/dev/null 2>&1; then
|
|
1276
|
+
log_dim "Sync" "$base_branch (ff +$behind_count)"
|
|
1277
|
+
else
|
|
1278
|
+
log_warn "Sync" "$base_branch diverged from origin (ff-only rejected)"
|
|
1279
|
+
HAS_WARNINGS=true
|
|
1280
|
+
fi
|
|
1281
|
+
fi
|
|
1282
|
+
fi
|
|
1283
|
+
else
|
|
1284
|
+
# Default: return to base branch after routing to prevent AI agents
|
|
1285
|
+
# from being stranded on an ephemeral wip/route-main/* branch and
|
|
1286
|
+
# accidentally targeting it as the base for subsequent PRs.
|
|
1287
|
+
if ! git checkout "$base_branch" >/dev/null 2>&1; then
|
|
1288
|
+
log_err "Checkout" "failed: $base_branch (stranded on $route_branch)"
|
|
1289
|
+
HAS_WARNINGS=true
|
|
1290
|
+
else
|
|
1291
|
+
log_dim "Checkout" "$base_branch"
|
|
1292
|
+
CURRENT_BRANCH="$base_branch"
|
|
1293
|
+
# Fast-forward local base branch if behind origin (fetch already ran)
|
|
1294
|
+
local behind_count
|
|
1295
|
+
behind_count=$(git rev-list --count "HEAD..origin/$base_branch" 2>/dev/null || echo "0")
|
|
1296
|
+
if [ "$behind_count" -gt 0 ]; then
|
|
1297
|
+
if git merge --ff-only "origin/$base_branch" >/dev/null 2>&1; then
|
|
1298
|
+
log_dim "Sync" "$base_branch (ff +$behind_count)"
|
|
1299
|
+
else
|
|
1300
|
+
log_warn "Sync" "$base_branch diverged from origin (ff-only rejected)"
|
|
1301
|
+
HAS_WARNINGS=true
|
|
1302
|
+
fi
|
|
1303
|
+
fi
|
|
1304
|
+
fi
|
|
1305
|
+
fi
|
|
1306
|
+
|
|
1307
|
+
return 0
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
# Reset PR URL tracking for this sync run (prevents stale values from previous iterations)
|
|
1311
|
+
CREATED_PR_URL=""
|
|
1312
|
+
|
|
1313
|
+
# Status/PR-only should never mutate the repo
|
|
1314
|
+
if [ "$STATUS_ONLY" = true ] || [ "$PR_ONLY" = true ]; then
|
|
1315
|
+
DRY_RUN=true
|
|
1316
|
+
fi
|
|
1317
|
+
|
|
1318
|
+
# Get current branch
|
|
1319
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || true)
|
|
1320
|
+
DETACHED_HEAD=false
|
|
1321
|
+
DETACHED_HEAD_SHA=""
|
|
1322
|
+
if [ -z "$CURRENT_BRANCH" ]; then
|
|
1323
|
+
if [ "$STATUS_ONLY" = true ]; then
|
|
1324
|
+
DETACHED_HEAD=true
|
|
1325
|
+
DETACHED_HEAD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
1326
|
+
CURRENT_BRANCH="HEAD"
|
|
1327
|
+
log_dim "Branch" "detached HEAD ($DETACHED_HEAD_SHA)"
|
|
1328
|
+
else
|
|
1329
|
+
log_err "Branch" "detached HEAD"
|
|
1330
|
+
exit 1
|
|
1331
|
+
fi
|
|
1332
|
+
fi
|
|
1333
|
+
|
|
1334
|
+
# Create safety backup branch (before any operations)
|
|
1335
|
+
BACKUP_BRANCH=""
|
|
1336
|
+
BACKUP_BRANCH_CREATED=false
|
|
1337
|
+
RECOVERY_BRANCH=""
|
|
1338
|
+
|
|
1339
|
+
# Create backup branch lazily (only before first mutating operation)
|
|
1340
|
+
ensure_backup_branch() {
|
|
1341
|
+
if [ "$BACKUP_BRANCH_CREATED" = true ]; then return 0; fi
|
|
1342
|
+
if [ "$DRY_RUN" = true ]; then return 0; fi
|
|
1343
|
+
BACKUP_BRANCH="backup/pre-sync-$(date +%s)"
|
|
1344
|
+
if git branch "$BACKUP_BRANCH" >/dev/null 2>&1; then
|
|
1345
|
+
BACKUP_BRANCH_CREATED=true
|
|
1346
|
+
log_dim "Checkpoint" "$BACKUP_BRANCH"
|
|
1347
|
+
else
|
|
1348
|
+
log_err "Checkpoint" "failed to create backup"
|
|
1349
|
+
exit 1
|
|
1350
|
+
fi
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
# Print recovery hint: use backup branch if created, otherwise point to reflog/ORIG_HEAD
|
|
1354
|
+
print_recovery_hint() {
|
|
1355
|
+
echo ""
|
|
1356
|
+
if [ "$BACKUP_BRANCH_CREATED" = true ]; then
|
|
1357
|
+
echo "Your work is safe in: $BACKUP_BRANCH"
|
|
1358
|
+
else
|
|
1359
|
+
echo "Your work is safe in git reflog (run: git reflog show HEAD)"
|
|
1360
|
+
fi
|
|
1361
|
+
echo ""
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
# Retention window for pruning stale backup branches (env-only knob; not a config key).
|
|
1365
|
+
# unset/empty -> 7 days; off|never -> pruning disabled; integer (0 allowed) -> days,
|
|
1366
|
+
# where 0 disables the age gate. Anything else falls back to 7 (fail-closed default).
|
|
1367
|
+
get_backup_retention_days() {
|
|
1368
|
+
local v="${POETIC_SYNC_BACKUP_RETENTION_DAYS:-}"
|
|
1369
|
+
case "$v" in
|
|
1370
|
+
"") echo 7 ;;
|
|
1371
|
+
off|never) echo disabled ;;
|
|
1372
|
+
*)
|
|
1373
|
+
# Digit cap avoids arithmetic overflow; 10# forces base-10 so leading
|
|
1374
|
+
# zeroes ("08") cannot trigger bash octal-parse errors mid-cleanup.
|
|
1375
|
+
if [[ "$v" =~ ^[0-9]{1,4}$ ]]; then
|
|
1376
|
+
echo "$((10#$v))"
|
|
1377
|
+
else
|
|
1378
|
+
echo 7
|
|
1379
|
+
fi
|
|
1380
|
+
;;
|
|
1381
|
+
esac
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
# Reap backup/pre-sync-* branches orphaned by prior failed/interrupted runs.
|
|
1385
|
+
# Lossless-only: a branch is deleted ONLY when its content is provably preserved —
|
|
1386
|
+
# tip is an ancestor of HEAD, or a linear (merge-free) range whose every commit is
|
|
1387
|
+
# patch-equivalent to one in HEAD. Any safety-check failure retains the branch
|
|
1388
|
+
# (false-keep is harmless; false-delete is forbidden). Never touches the current
|
|
1389
|
+
# run's checkpoint, branches checked out in other worktrees, or exit/warning state.
|
|
1390
|
+
prune_stale_backup_branches() {
|
|
1391
|
+
local retention_days now cutoff pruned=0 kept=""
|
|
1392
|
+
retention_days=$(get_backup_retention_days)
|
|
1393
|
+
[ "$retention_days" = "disabled" ] && return 0
|
|
1394
|
+
now=$(date +%s)
|
|
1395
|
+
cutoff=$((now - retention_days * 86400))
|
|
1396
|
+
|
|
1397
|
+
# Fail-closed: if the worktree list cannot be read, skip pruning this run
|
|
1398
|
+
# (we cannot prove a candidate branch is not checked out elsewhere).
|
|
1399
|
+
local wt_list
|
|
1400
|
+
wt_list=$(git worktree list --porcelain 2>/dev/null) || return 0
|
|
1401
|
+
|
|
1402
|
+
local name epoch tip safe merges cherry_out
|
|
1403
|
+
while IFS= read -r name; do
|
|
1404
|
+
[ -n "$name" ] || continue
|
|
1405
|
+
[ "$name" = "${BACKUP_BRANCH:-}" ] && continue
|
|
1406
|
+
epoch="${name##*-}"
|
|
1407
|
+
[[ "$epoch" =~ ^[0-9]{1,12}$ ]] || continue
|
|
1408
|
+
[ "$epoch" -le "$cutoff" ] || continue
|
|
1409
|
+
tip=$(git rev-parse --verify --quiet "refs/heads/$name^{commit}") || continue
|
|
1410
|
+
|
|
1411
|
+
safe=false
|
|
1412
|
+
if git merge-base --is-ancestor "$tip" HEAD 2>/dev/null; then
|
|
1413
|
+
safe=true
|
|
1414
|
+
else
|
|
1415
|
+
# cherry is patch-id based and blind to merge commits: only trust it on
|
|
1416
|
+
# merge-free ranges, and only when it ran successfully (fail-closed).
|
|
1417
|
+
merges=$(git rev-list --merges "HEAD..$tip" 2>/dev/null) || merges="unknown"
|
|
1418
|
+
if [ -z "$merges" ]; then
|
|
1419
|
+
if cherry_out=$(git cherry HEAD "$tip" 2>/dev/null); then
|
|
1420
|
+
# here-string, not a pipeline: under pipefail a grep -q early
|
|
1421
|
+
# exit can SIGPIPE the writer and invert the safety verdict
|
|
1422
|
+
if ! grep -q '^+' <<<"$cherry_out"; then
|
|
1423
|
+
safe=true
|
|
1424
|
+
fi
|
|
1425
|
+
fi
|
|
1426
|
+
fi
|
|
1427
|
+
fi
|
|
1428
|
+
if [ "$safe" != true ]; then
|
|
1429
|
+
kept="$kept $name"
|
|
1430
|
+
continue
|
|
1431
|
+
fi
|
|
1432
|
+
|
|
1433
|
+
# Skip branches checked out in any worktree (shared branch namespace), then
|
|
1434
|
+
# compare-and-swap delete against the verified tip so a moved ref is never pruned.
|
|
1435
|
+
if grep -qxF "branch refs/heads/$name" <<<"$wt_list"; then
|
|
1436
|
+
continue
|
|
1437
|
+
fi
|
|
1438
|
+
if git update-ref -d "refs/heads/$name" "$tip" 2>/dev/null; then
|
|
1439
|
+
pruned=$((pruned + 1))
|
|
1440
|
+
fi
|
|
1441
|
+
done < <(git for-each-ref --format='%(refname:short)' 'refs/heads/backup/pre-sync-*' 2>/dev/null)
|
|
1442
|
+
|
|
1443
|
+
if [ "$pruned" -gt 0 ]; then
|
|
1444
|
+
log_dim "Cleanup" "pruned $pruned stale backup branch(es)"
|
|
1445
|
+
fi
|
|
1446
|
+
if [ -n "$kept" ]; then
|
|
1447
|
+
log_dim "Cleanup" "kept stale backup(s) with unique commits:$kept (inspect: git log HEAD..<name>)"
|
|
1448
|
+
fi
|
|
1449
|
+
return 0
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
# Get main branch name (main or master)
|
|
1453
|
+
# TODO: Support exotic default branches (e.g., develop) via:
|
|
1454
|
+
# git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'
|
|
1455
|
+
# or: gh repo view --json defaultBranchRef -q .defaultBranchRef.name
|
|
1456
|
+
if git show-ref --verify --quiet refs/heads/main; then
|
|
1457
|
+
MAIN_BRANCH="main"
|
|
1458
|
+
elif git show-ref --verify --quiet refs/heads/master; then
|
|
1459
|
+
MAIN_BRANCH="master"
|
|
1460
|
+
else
|
|
1461
|
+
log_err "Branch" "no main or master found"
|
|
1462
|
+
exit 1
|
|
1463
|
+
fi
|
|
1464
|
+
|
|
1465
|
+
# Resolve route-main effective setting (flag overrides config)
|
|
1466
|
+
ROUTE_MAIN_EFFECTIVE=false
|
|
1467
|
+
if [ "$NO_ROUTE_MAIN" = true ]; then
|
|
1468
|
+
ROUTE_MAIN_EFFECTIVE=false
|
|
1469
|
+
elif [ "$ROUTE_MAIN_FORCE" = true ]; then
|
|
1470
|
+
ROUTE_MAIN_EFFECTIVE=true
|
|
1471
|
+
else
|
|
1472
|
+
ROUTE_MAIN_DEFAULT=$(get_route_main_default)
|
|
1473
|
+
if is_truthy "$ROUTE_MAIN_DEFAULT"; then
|
|
1474
|
+
ROUTE_MAIN_EFFECTIVE=true
|
|
1475
|
+
fi
|
|
1476
|
+
fi
|
|
1477
|
+
|
|
1478
|
+
# Backup branch is created lazily via ensure_backup_branch() before first mutating op
|
|
1479
|
+
|
|
1480
|
+
# Check for uncommitted changes
|
|
1481
|
+
CHANGED_FILES=$(git status --porcelain)
|
|
1482
|
+
HAS_CHANGES=false
|
|
1483
|
+
HAS_TRACKED_CHANGES=false
|
|
1484
|
+
HAS_UNTRACKED_ONLY=false
|
|
1485
|
+
|
|
1486
|
+
if [ -n "$CHANGED_FILES" ]; then
|
|
1487
|
+
HAS_CHANGES=true
|
|
1488
|
+
|
|
1489
|
+
# Distinguish tracked changes from untracked-only (??) or ignored (!!)
|
|
1490
|
+
# Git porcelain format: XY where X=index status, Y=worktree status
|
|
1491
|
+
# Blacklist approach: exclude lines starting with ? or ! (covers all tracked codes)
|
|
1492
|
+
TRACKED_CHANGES=$(echo "$CHANGED_FILES" | grep -Ev "^[\?!]" || true)
|
|
1493
|
+
if [ -n "$TRACKED_CHANGES" ]; then
|
|
1494
|
+
HAS_TRACKED_CHANGES=true
|
|
1495
|
+
else
|
|
1496
|
+
HAS_UNTRACKED_ONLY=true
|
|
1497
|
+
fi
|
|
1498
|
+
fi
|
|
1499
|
+
|
|
1500
|
+
# Auto-detect mode: if clean and no message, assume sync mode
|
|
1501
|
+
if [ "$PR_ONLY" = false ] && [ "$STATUS_ONLY" = false ] && [ "$HAS_CHANGES" = false ] && [ -z "$COMMIT_MESSAGE" ] && [ "$SYNC_MODE" = false ]; then
|
|
1502
|
+
log_dim "Mode" "clean — switching to sync"
|
|
1503
|
+
SYNC_MODE=true
|
|
1504
|
+
fi
|
|
1505
|
+
|
|
1506
|
+
# Status/PR-only: force non-mutating "sync-style" control flow (skip commit/push phases)
|
|
1507
|
+
if [ "$STATUS_ONLY" = true ] || [ "$PR_ONLY" = true ]; then
|
|
1508
|
+
SYNC_MODE=true
|
|
1509
|
+
fi
|
|
1510
|
+
|
|
1511
|
+
# ============================================================================
|
|
1512
|
+
# Route-main pre-commit guardrail bypass (commit on a non-protected branch)
|
|
1513
|
+
# ============================================================================
|
|
1514
|
+
# If route-main is enabled and we're on main/master in commit mode, switch to a
|
|
1515
|
+
# temporary route branch BEFORE the protected-branch check so we never commit on
|
|
1516
|
+
# main/master (avoids requiring POETIC_ALLOW_MAIN_COMMIT in non-interactive runs).
|
|
1517
|
+
if [ "$DRY_RUN" = false ] && [ "$SYNC_MODE" = false ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
|
|
1518
|
+
if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$ROUTE_MAIN_EFFECTIVE" = true ]; then
|
|
1519
|
+
ensure_backup_branch
|
|
1520
|
+
ROUTE_MAIN_MODE_ACTIVE=true
|
|
1521
|
+
ROUTE_MAIN_WIP_BRANCH="wip/route-main/wip-$(date +%s)"
|
|
1522
|
+
if ! git checkout -B "$ROUTE_MAIN_WIP_BRANCH" >/dev/null 2>&1; then
|
|
1523
|
+
log_err "Route-main" "branch creation failed"
|
|
1524
|
+
exit 1
|
|
1525
|
+
fi
|
|
1526
|
+
CURRENT_BRANCH="$ROUTE_MAIN_WIP_BRANCH"
|
|
1527
|
+
fi
|
|
1528
|
+
fi
|
|
1529
|
+
|
|
1530
|
+
# ============================================================================
|
|
1531
|
+
# GUARDRAIL 1: Protected Branch Check (Soft by Default)
|
|
1532
|
+
# ============================================================================
|
|
1533
|
+
# Modes: off (no message), warn (print warning, proceed), confirm (prompt y/N), block (always block)
|
|
1534
|
+
# Configure: POETIC_PROTECT_MAIN=<mode> or poetic config set --scope user sync.protectMain <mode>
|
|
1535
|
+
# Default: confirm (interactive prompt when TTY, block when non-TTY unless POETIC_ALLOW_MAIN_COMMIT=1)
|
|
1536
|
+
|
|
1537
|
+
if [ "$FORCE" = false ] && [ "$NO_PROTECT_MAIN" = false ]; then
|
|
1538
|
+
# Define protected branches (main, master, and any branch starting with release/)
|
|
1539
|
+
IS_PROTECTED=false
|
|
1540
|
+
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
|
|
1541
|
+
IS_PROTECTED=true
|
|
1542
|
+
elif [[ "$CURRENT_BRANCH" == release/* ]]; then
|
|
1543
|
+
IS_PROTECTED=true
|
|
1544
|
+
fi
|
|
1545
|
+
|
|
1546
|
+
if [ "$DRY_RUN" = false ] && [ "$IS_PROTECTED" = true ] && [ "$SYNC_MODE" = false ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
|
|
1547
|
+
PROTECT_MODE=$(get_protect_main_mode)
|
|
1548
|
+
|
|
1549
|
+
case "$PROTECT_MODE" in
|
|
1550
|
+
off)
|
|
1551
|
+
# No message, proceed silently
|
|
1552
|
+
;;
|
|
1553
|
+
warn)
|
|
1554
|
+
# Print warning to stderr, proceed
|
|
1555
|
+
printf " ${YELLOW}%-16s %s${NC}\n" "Protect-main" "committing to '$CURRENT_BRANCH'" >&2
|
|
1556
|
+
;;
|
|
1557
|
+
block)
|
|
1558
|
+
# Always block (original behavior)
|
|
1559
|
+
echo ""
|
|
1560
|
+
log_err "Blocked" "commits to '$CURRENT_BRANCH' are not allowed"
|
|
1561
|
+
echo ""
|
|
1562
|
+
echo " To change this behavior:"
|
|
1563
|
+
echo " poetic config set --scope user sync.protectMain confirm # Interactive prompt"
|
|
1564
|
+
echo " poetic config set --scope user sync.protectMain warn # Warn and proceed"
|
|
1565
|
+
echo " poetic config set --scope user sync.protectMain off # No protection"
|
|
1566
|
+
echo ""
|
|
1567
|
+
echo " Or bypass for this run:"
|
|
1568
|
+
echo " poetic sync --no-protect-main"
|
|
1569
|
+
echo ""
|
|
1570
|
+
[ "$BACKUP_BRANCH_CREATED" = true ] && git branch -D "$BACKUP_BRANCH" >/dev/null 2>&1 || true
|
|
1571
|
+
exit 1
|
|
1572
|
+
;;
|
|
1573
|
+
confirm|*)
|
|
1574
|
+
# Default: Interactive confirm when TTY, block otherwise
|
|
1575
|
+
# Robust TTY check: both stdin AND stdout must be terminals
|
|
1576
|
+
# Note: This is intentionally strict - redirected stdin with TTY stdout
|
|
1577
|
+
# still goes to non-interactive path (safer default for piped input)
|
|
1578
|
+
if [ -t 0 ] && [ -t 1 ]; then
|
|
1579
|
+
# Interactive terminal - prompt user
|
|
1580
|
+
echo ""
|
|
1581
|
+
printf " ${YELLOW}%-16s %s${NC}\n" "Protect-main" "committing to '$CURRENT_BRANCH'"
|
|
1582
|
+
echo -n "Continue? [y/N] "
|
|
1583
|
+
|
|
1584
|
+
# Read with timeout (5s) to prevent hanging on weird stdin states
|
|
1585
|
+
# Empty input or timeout = "No" (safe default)
|
|
1586
|
+
# Note: if statement captures exit code, safe with set -e
|
|
1587
|
+
if read -r -n 1 -t 5 response 2>/dev/null; then
|
|
1588
|
+
echo "" # newline after single char input
|
|
1589
|
+
else
|
|
1590
|
+
# Timeout or read error - treat as "No"
|
|
1591
|
+
echo ""
|
|
1592
|
+
log_dim "Confirm" "timed out — treating as no"
|
|
1593
|
+
response="n"
|
|
1594
|
+
fi
|
|
1595
|
+
|
|
1596
|
+
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
1597
|
+
log_dim "Confirm" "aborted"
|
|
1598
|
+
[ "$BACKUP_BRANCH_CREATED" = true ] && git branch -D "$BACKUP_BRANCH" >/dev/null 2>&1 || true
|
|
1599
|
+
exit 1 # Exit non-zero: user declined or timeout
|
|
1600
|
+
fi
|
|
1601
|
+
else
|
|
1602
|
+
# Non-interactive (CI/script) - check for explicit allow
|
|
1603
|
+
if [ "${POETIC_ALLOW_MAIN_COMMIT:-}" = "1" ]; then
|
|
1604
|
+
printf " ${YELLOW}%-16s %s${NC}\n" "Protect-main" "committing to '$CURRENT_BRANCH' (POETIC_ALLOW_MAIN_COMMIT=1)" >&2
|
|
1605
|
+
else
|
|
1606
|
+
echo ""
|
|
1607
|
+
log_err "Blocked" "non-interactive commit to '$CURRENT_BRANCH'"
|
|
1608
|
+
echo ""
|
|
1609
|
+
echo " For CI/scripts, set:"
|
|
1610
|
+
echo " POETIC_ALLOW_MAIN_COMMIT=1"
|
|
1611
|
+
echo ""
|
|
1612
|
+
echo " Or change default mode:"
|
|
1613
|
+
echo " poetic config set --scope user sync.protectMain warn"
|
|
1614
|
+
echo ""
|
|
1615
|
+
[ "$BACKUP_BRANCH_CREATED" = true ] && git branch -D "$BACKUP_BRANCH" >/dev/null 2>&1 || true
|
|
1616
|
+
exit 1
|
|
1617
|
+
fi
|
|
1618
|
+
fi
|
|
1619
|
+
;;
|
|
1620
|
+
esac
|
|
1621
|
+
fi
|
|
1622
|
+
fi
|
|
1623
|
+
|
|
1624
|
+
# ============================================================================
|
|
1625
|
+
# GUARDRAIL 2: Orphaned Branch Work Detection
|
|
1626
|
+
# ============================================================================
|
|
1627
|
+
# Warn if there are local branches with unpushed commits that might be lost
|
|
1628
|
+
# Scoped to: branches ahead of upstream OR commits not reachable from target ref
|
|
1629
|
+
# Filtered to: only branches with activity in last 7 days (reduces noise)
|
|
1630
|
+
# Note: Only runs when committing (HAS_CHANGES=true), not during sync-only
|
|
1631
|
+
# Throttle: only show orphan warning once per 24h (timestamp file)
|
|
1632
|
+
ORPHAN_THROTTLE_FILE="/tmp/claude/poetic-orphan-warn-$(echo "$REPO_ROOT" | shasum -a 256 | cut -c1-12)"
|
|
1633
|
+
ORPHAN_THROTTLED=false
|
|
1634
|
+
if [ -f "$ORPHAN_THROTTLE_FILE" ]; then
|
|
1635
|
+
LAST_WARN=$(cat "$ORPHAN_THROTTLE_FILE" 2>/dev/null || echo "0")
|
|
1636
|
+
NOW_TS=$(date +%s)
|
|
1637
|
+
if [ $((NOW_TS - LAST_WARN)) -lt 86400 ]; then
|
|
1638
|
+
ORPHAN_THROTTLED=true
|
|
1639
|
+
fi
|
|
1640
|
+
fi
|
|
1641
|
+
|
|
1642
|
+
if [ "$NO_ORPHAN_WARN" = false ] && [ "$ORPHAN_THROTTLED" = false ] && [ "$HAS_CHANGES" = true ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
|
|
1643
|
+
ORPHANED_WORK=""
|
|
1644
|
+
SEVEN_DAYS_AGO=$(( $(date +%s) - 604800 )) # 7 days = 604800 seconds (portable)
|
|
1645
|
+
|
|
1646
|
+
# Check for local branches ahead of their upstream (unpushed work)
|
|
1647
|
+
while IFS= read -r branch; do
|
|
1648
|
+
# Skip current branch and backup branches
|
|
1649
|
+
if [ "$branch" = "$CURRENT_BRANCH" ] || [[ "$branch" == backup/* ]]; then
|
|
1650
|
+
continue
|
|
1651
|
+
fi
|
|
1652
|
+
|
|
1653
|
+
# Filter: Only check branches with recent activity (last 7 days)
|
|
1654
|
+
LAST_COMMIT_TIME=$(git log -1 --format=%ct "$branch" 2>/dev/null || echo "0")
|
|
1655
|
+
if [ "$LAST_COMMIT_TIME" -lt "$SEVEN_DAYS_AGO" ]; then
|
|
1656
|
+
continue # Skip stale branches to reduce noise
|
|
1657
|
+
fi
|
|
1658
|
+
|
|
1659
|
+
# Check if branch has upstream
|
|
1660
|
+
UPSTREAM=$(git rev-parse --abbrev-ref "$branch@{u}" 2>/dev/null || echo "")
|
|
1661
|
+
if [ -n "$UPSTREAM" ]; then
|
|
1662
|
+
# Count commits ahead of upstream
|
|
1663
|
+
AHEAD_COUNT=$(git rev-list --count "$UPSTREAM..$branch" 2>/dev/null || echo "0")
|
|
1664
|
+
if [ "$AHEAD_COUNT" -gt 0 ]; then
|
|
1665
|
+
ORPHANED_WORK="${ORPHANED_WORK} $branch: $AHEAD_COUNT unpushed commit(s)\n"
|
|
1666
|
+
fi
|
|
1667
|
+
else
|
|
1668
|
+
# No upstream - check if it has commits not on current branch
|
|
1669
|
+
UNIQUE_COMMITS=$(git rev-list --count "$CURRENT_BRANCH..$branch" 2>/dev/null || echo "0")
|
|
1670
|
+
if [ "$UNIQUE_COMMITS" -gt 0 ]; then
|
|
1671
|
+
ORPHANED_WORK="${ORPHANED_WORK} $branch: $UNIQUE_COMMITS commit(s) not on $CURRENT_BRANCH (no upstream)\n"
|
|
1672
|
+
fi
|
|
1673
|
+
fi
|
|
1674
|
+
done < <(git for-each-ref --format='%(refname:short)' refs/heads/)
|
|
1675
|
+
|
|
1676
|
+
if [ -n "$ORPHANED_WORK" ]; then
|
|
1677
|
+
echo ""
|
|
1678
|
+
log_warn "Orphaned" "unpushed work on other branches (last 7 days):"
|
|
1679
|
+
echo -e "$ORPHANED_WORK"
|
|
1680
|
+
echo " Current branch: $CURRENT_BRANCH"
|
|
1681
|
+
echo " Suppress: poetic sync --no-orphan-warn"
|
|
1682
|
+
echo ""
|
|
1683
|
+
|
|
1684
|
+
# Don't block, just warn prominently (user can --no-orphan-warn if intentional)
|
|
1685
|
+
# This is a warning, not a hard block, per Codex's feedback
|
|
1686
|
+
HAS_WARNINGS=true
|
|
1687
|
+
# Throttle: don't show again for 24h
|
|
1688
|
+
mkdir -p /tmp/claude 2>/dev/null || true
|
|
1689
|
+
date +%s > "$ORPHAN_THROTTLE_FILE" 2>/dev/null || true
|
|
1690
|
+
fi
|
|
1691
|
+
fi
|
|
1692
|
+
|
|
1693
|
+
# DRY RUN HEADER
|
|
1694
|
+
if [ "$DRY_RUN" = true ]; then
|
|
1695
|
+
log_warn "Mode" "dry run — no changes will be made"
|
|
1696
|
+
fi
|
|
1697
|
+
|
|
1698
|
+
# STEP 0: Fetch from remote(s) (ALWAYS do this first)
|
|
1699
|
+
# Smart remote: fetch only the upstream's remote (usually origin) when single-remote.
|
|
1700
|
+
# Fall back to --all only when multiple remotes are configured.
|
|
1701
|
+
FETCH_REMOTE="origin"
|
|
1702
|
+
REMOTE_COUNT=$(git remote 2>/dev/null | wc -l | tr -d ' ')
|
|
1703
|
+
if [ "$REMOTE_COUNT" -gt 1 ]; then
|
|
1704
|
+
# Multiple remotes: check if branch's upstream points to a specific one
|
|
1705
|
+
UPSTREAM_REMOTE=$(git config "branch.${CURRENT_BRANCH}.remote" 2>/dev/null || echo "")
|
|
1706
|
+
if [ -n "$UPSTREAM_REMOTE" ]; then
|
|
1707
|
+
FETCH_REMOTE="$UPSTREAM_REMOTE"
|
|
1708
|
+
else
|
|
1709
|
+
FETCH_REMOTE="--all"
|
|
1710
|
+
fi
|
|
1711
|
+
fi
|
|
1712
|
+
|
|
1713
|
+
SYNC_FETCH_DONE=false
|
|
1714
|
+
if [ "$DRY_RUN" = false ]; then
|
|
1715
|
+
if [ "$FETCH_REMOTE" = "--all" ]; then
|
|
1716
|
+
if git fetch --all --prune >/dev/null 2>&1; then
|
|
1717
|
+
log_ok "Fetch" "done (all remotes)"
|
|
1718
|
+
SYNC_FETCH_DONE=true
|
|
1719
|
+
else
|
|
1720
|
+
log_warn "Fetch" "completed with warnings"
|
|
1721
|
+
fi
|
|
1722
|
+
else
|
|
1723
|
+
if git fetch "$FETCH_REMOTE" --prune >/dev/null 2>&1; then
|
|
1724
|
+
log_ok "Fetch" "done"
|
|
1725
|
+
SYNC_FETCH_DONE=true
|
|
1726
|
+
else
|
|
1727
|
+
log_warn "Fetch" "completed with warnings"
|
|
1728
|
+
fi
|
|
1729
|
+
fi
|
|
1730
|
+
else
|
|
1731
|
+
echo "Would fetch from ${FETCH_REMOTE}"
|
|
1732
|
+
fi
|
|
1733
|
+
|
|
1734
|
+
# STEP 1: Check upstream tracking (cache result for reuse)
|
|
1735
|
+
if [ "$DETACHED_HEAD" = true ]; then
|
|
1736
|
+
UPSTREAM_EXISTS=false
|
|
1737
|
+
UPSTREAM_BRANCH=""
|
|
1738
|
+
log_dim "Upstream" "not applicable (detached HEAD)"
|
|
1739
|
+
elif git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
|
|
1740
|
+
UPSTREAM_EXISTS=true
|
|
1741
|
+
UPSTREAM_BRANCH=$(git rev-parse --abbrev-ref --symbolic-full-name "@{u}")
|
|
1742
|
+
else
|
|
1743
|
+
if [ "$ROUTE_MAIN_MODE_ACTIVE" = true ]; then
|
|
1744
|
+
log_dim "Upstream" "not configured (route-main work branch)"
|
|
1745
|
+
else
|
|
1746
|
+
log_warn "Upstream" "not configured"
|
|
1747
|
+
HAS_WARNINGS=true
|
|
1748
|
+
fi
|
|
1749
|
+
|
|
1750
|
+
if [ "$DRY_RUN" = false ]; then
|
|
1751
|
+
# Use the resolved fetch remote (respects multi-remote setups)
|
|
1752
|
+
UPSTREAM_CANDIDATE="${FETCH_REMOTE}"
|
|
1753
|
+
# If FETCH_REMOTE is --all, fall back to origin for upstream detection
|
|
1754
|
+
[ "$UPSTREAM_CANDIDATE" = "--all" ] && UPSTREAM_CANDIDATE="origin"
|
|
1755
|
+
|
|
1756
|
+
# Check if remote branch exists using local refs (fetch already ran, no network needed)
|
|
1757
|
+
if git show-ref --verify --quiet "refs/remotes/${UPSTREAM_CANDIDATE}/$CURRENT_BRANCH" 2>/dev/null; then
|
|
1758
|
+
log_dim "Upstream" "setting to ${UPSTREAM_CANDIDATE}/$CURRENT_BRANCH"
|
|
1759
|
+
git branch --set-upstream-to="${UPSTREAM_CANDIDATE}/$CURRENT_BRANCH" "$CURRENT_BRANCH"
|
|
1760
|
+
log_ok "Upstream" "configured"
|
|
1761
|
+
UPSTREAM_EXISTS=true
|
|
1762
|
+
UPSTREAM_BRANCH="${UPSTREAM_CANDIDATE}/$CURRENT_BRANCH"
|
|
1763
|
+
else
|
|
1764
|
+
log_dim "Upstream" "remote branch not yet created"
|
|
1765
|
+
fi
|
|
1766
|
+
else
|
|
1767
|
+
echo "Would set upstream to ${FETCH_REMOTE}/$CURRENT_BRANCH (if exists)"
|
|
1768
|
+
fi
|
|
1769
|
+
fi
|
|
1770
|
+
|
|
1771
|
+
# Compute TRACKED_BRANCHES once (both branch and upstream columns) for reuse everywhere.
|
|
1772
|
+
# Format: "branch_name upstream_ref" per line (e.g., "main origin/main")
|
|
1773
|
+
# Includes any branch with a non-empty upstream (not limited to origin).
|
|
1774
|
+
TRACKED_BRANCHES=$(git for-each-ref --format='%(if)%(upstream:short)%(then)%(refname:short) %(upstream:short)%(end)' refs/heads/ | grep -v '^$' || true)
|
|
1775
|
+
|
|
1776
|
+
# SYNC MODE: Pull all branches and report status
|
|
1777
|
+
if [ "$SYNC_MODE" = true ]; then
|
|
1778
|
+
if [ "$DRY_RUN" = false ]; then
|
|
1779
|
+
if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
|
|
1780
|
+
# Early-exit optimization: skip pull if already synced
|
|
1781
|
+
AHEAD=$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo "0")
|
|
1782
|
+
BEHIND=$(git rev-list --count "HEAD..@{u}" 2>/dev/null || echo "0")
|
|
1783
|
+
|
|
1784
|
+
if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ] && [ "$HAS_CHANGES" = false ] && [ "$ALL_BRANCHES" = false ]; then
|
|
1785
|
+
log_ok "$CURRENT_BRANCH" "up-to-date"
|
|
1786
|
+
else
|
|
1787
|
+
# Pull using shared helper (stash → pull → conflict-abort → unstash)
|
|
1788
|
+
if do_pull_with_stash; then
|
|
1789
|
+
log_ok "Pull" "$CURRENT_BRANCH"
|
|
1790
|
+
|
|
1791
|
+
# Bidirectional sync: push if local is ahead
|
|
1792
|
+
AHEAD_AFTER_PULL=$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo "0")
|
|
1793
|
+
if [ "$AHEAD_AFTER_PULL" -gt 0 ]; then
|
|
1794
|
+
if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$ROUTE_MAIN_EFFECTIVE" = true ]; then
|
|
1795
|
+
ROUTE_MAIN_NEEDED=true
|
|
1796
|
+
ROUTE_MAIN_AHEAD_COUNT="$AHEAD_AFTER_PULL"
|
|
1797
|
+
log_dim "Route-main" "$AHEAD_AFTER_PULL commit(s) ahead — routing via PR"
|
|
1798
|
+
elif [ "$NO_SYNC_PUSH" = false ]; then
|
|
1799
|
+
if run_git_push; then
|
|
1800
|
+
log_ok "Push" "$UPSTREAM_BRANCH"
|
|
1801
|
+
PUSH_SUCCESS=true
|
|
1802
|
+
else
|
|
1803
|
+
log_warn "Push" "failed — push manually"
|
|
1804
|
+
log_git_push_reason
|
|
1805
|
+
HAS_WARNINGS=true
|
|
1806
|
+
fi
|
|
1807
|
+
else
|
|
1808
|
+
log_dim "Push" "skipped ($AHEAD_AFTER_PULL ahead, sync push disabled)"
|
|
1809
|
+
fi
|
|
1810
|
+
fi
|
|
1811
|
+
else
|
|
1812
|
+
SYNC_SUCCESS=false
|
|
1813
|
+
print_recovery_hint
|
|
1814
|
+
echo "To resolve manually:"
|
|
1815
|
+
echo " 1. git pull --rebase (or --merge)"
|
|
1816
|
+
echo " 2. Resolve conflicts in affected files"
|
|
1817
|
+
echo " 3. git add <resolved-files>"
|
|
1818
|
+
echo " 4. git rebase --continue (or git commit)"
|
|
1819
|
+
echo " 5. Run poetic sync again to push"
|
|
1820
|
+
exit 1
|
|
1821
|
+
fi
|
|
1822
|
+
fi
|
|
1823
|
+
else
|
|
1824
|
+
# No upstream: optionally auto-publish (push -u) to establish tracking
|
|
1825
|
+
if [ "$NO_SYNC_PUSH" = true ]; then
|
|
1826
|
+
log_dim "Pull" "no upstream (--no-sync-push)"
|
|
1827
|
+
else
|
|
1828
|
+
MAIN_REMOTE_REF=""
|
|
1829
|
+
if git show-ref --verify --quiet "refs/remotes/origin/$MAIN_BRANCH"; then
|
|
1830
|
+
MAIN_REMOTE_REF="origin/$MAIN_BRANCH"
|
|
1831
|
+
elif git show-ref --verify --quiet "refs/remotes/origin/master"; then
|
|
1832
|
+
MAIN_REMOTE_REF="origin/master"
|
|
1833
|
+
fi
|
|
1834
|
+
if [ -z "$MAIN_REMOTE_REF" ]; then
|
|
1835
|
+
MAIN_REMOTE_REF="$MAIN_BRANCH"
|
|
1836
|
+
fi
|
|
1837
|
+
|
|
1838
|
+
UNIQUE_TO_MAIN=$(git rev-list --count "$MAIN_REMOTE_REF..HEAD" 2>/dev/null || echo "0")
|
|
1839
|
+
|
|
1840
|
+
if [ "$UNIQUE_TO_MAIN" -gt 0 ] && should_publish_new_branch "$CURRENT_BRANCH"; then
|
|
1841
|
+
if run_git_push -u origin "$CURRENT_BRANCH"; then
|
|
1842
|
+
log_ok "Push" "origin/$CURRENT_BRANCH (set upstream)"
|
|
1843
|
+
PUSH_SUCCESS=true
|
|
1844
|
+
UPSTREAM_EXISTS=true
|
|
1845
|
+
UPSTREAM_BRANCH="origin/$CURRENT_BRANCH"
|
|
1846
|
+
else
|
|
1847
|
+
log_warn "Push" "auto-publish failed"
|
|
1848
|
+
log_git_push_reason
|
|
1849
|
+
HAS_WARNINGS=true
|
|
1850
|
+
fi
|
|
1851
|
+
elif [ "$UNIQUE_TO_MAIN" -eq 0 ]; then
|
|
1852
|
+
log_dim "Push" "skipped (no commits beyond $MAIN_REMOTE_REF)"
|
|
1853
|
+
else
|
|
1854
|
+
log_dim "Push" "publishing disabled (use --publish)"
|
|
1855
|
+
fi
|
|
1856
|
+
fi
|
|
1857
|
+
fi
|
|
1858
|
+
|
|
1859
|
+
# Sync all branches if requested
|
|
1860
|
+
if [ "$ALL_BRANCHES" = true ]; then
|
|
1861
|
+
# Keep strategy local to this block (PULL_STRATEGY is not a global).
|
|
1862
|
+
pull_strategy_all="--rebase=merges"
|
|
1863
|
+
if [ "$REBASE" = false ]; then
|
|
1864
|
+
pull_strategy_all="--merge"
|
|
1865
|
+
fi
|
|
1866
|
+
|
|
1867
|
+
# Iterate branch names from TRACKED_BRANCHES (computed after fetch)
|
|
1868
|
+
echo "$TRACKED_BRANCHES" | awk '{print $1}' | while IFS= read -r branch; do
|
|
1869
|
+
[ -z "$branch" ] && continue
|
|
1870
|
+
if [ "$branch" != "$CURRENT_BRANCH" ]; then
|
|
1871
|
+
if git checkout -q "$branch" 2>/dev/null; then
|
|
1872
|
+
if git pull -q "$pull_strategy_all" >/dev/null 2>&1; then
|
|
1873
|
+
local_ahead=$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo "0")
|
|
1874
|
+
if [ "$local_ahead" -gt 0 ]; then
|
|
1875
|
+
if [ "$PUSH_ALL_BRANCHES" = true ] && [ "$NO_SYNC_PUSH" = false ]; then
|
|
1876
|
+
if run_git_push -q; then
|
|
1877
|
+
log_dim "$branch" "pushed $local_ahead commit(s)"
|
|
1878
|
+
else
|
|
1879
|
+
log_warn "$branch" "push failed ($local_ahead ahead)"
|
|
1880
|
+
log_git_push_reason
|
|
1881
|
+
HAS_WARNINGS=true
|
|
1882
|
+
fi
|
|
1883
|
+
else
|
|
1884
|
+
log_dim "$branch" "$local_ahead commit(s) ahead"
|
|
1885
|
+
fi
|
|
1886
|
+
fi
|
|
1887
|
+
fi
|
|
1888
|
+
fi
|
|
1889
|
+
fi
|
|
1890
|
+
done
|
|
1891
|
+
|
|
1892
|
+
# Return to original branch
|
|
1893
|
+
git checkout -q "$CURRENT_BRANCH"
|
|
1894
|
+
log_ok "Sync" "all branches done"
|
|
1895
|
+
fi
|
|
1896
|
+
|
|
1897
|
+
# Route main only after sync operations complete (keeps stash/branch handling coherent)
|
|
1898
|
+
if [ "$ROUTE_MAIN_NEEDED" = true ]; then
|
|
1899
|
+
if route_main_head_to_pr "$MAIN_BRANCH" "$STAY_ON_MAIN"; then
|
|
1900
|
+
PUSH_SUCCESS=true
|
|
1901
|
+
else
|
|
1902
|
+
log_err "Route-main" "failed"
|
|
1903
|
+
exit 1
|
|
1904
|
+
fi
|
|
1905
|
+
fi
|
|
1906
|
+
else
|
|
1907
|
+
echo "Would pull latest changes using $([ "$REBASE" = true ] && echo "rebase" || echo "merge")"
|
|
1908
|
+
if [ "$ALL_BRANCHES" = true ]; then
|
|
1909
|
+
echo "Would sync all tracked branches"
|
|
1910
|
+
fi
|
|
1911
|
+
fi
|
|
1912
|
+
|
|
1913
|
+
# Skip to status report
|
|
1914
|
+
SKIP_COMMIT=true
|
|
1915
|
+
else
|
|
1916
|
+
SKIP_COMMIT=false
|
|
1917
|
+
fi
|
|
1918
|
+
|
|
1919
|
+
# STEP 2: Pull current branch BEFORE committing (critical for multi-env)
|
|
1920
|
+
if [ "$SKIP_COMMIT" = false ] && [ "$DRY_RUN" = false ]; then
|
|
1921
|
+
if [ "$ROUTE_MAIN_MODE_ACTIVE" = true ]; then
|
|
1922
|
+
# Route-main pre-commit branch has no upstream; rebase/merge onto origin/main
|
|
1923
|
+
PULL_RC=0; do_pull_with_stash --rebase-onto "origin/$MAIN_BRANCH" || PULL_RC=$?
|
|
1924
|
+
else
|
|
1925
|
+
PULL_RC=0; do_pull_with_stash || PULL_RC=$?
|
|
1926
|
+
fi
|
|
1927
|
+
|
|
1928
|
+
if [ "$PULL_RC" -ne 0 ]; then
|
|
1929
|
+
echo ""
|
|
1930
|
+
echo "Please resolve conflicts manually and run poetic sync again"
|
|
1931
|
+
exit 1
|
|
1932
|
+
fi
|
|
1933
|
+
fi
|
|
1934
|
+
|
|
1935
|
+
# STEP 3: Stage and Commit (if not sync mode)
|
|
1936
|
+
if [ "$SKIP_COMMIT" = false ]; then
|
|
1937
|
+
if [ "$HAS_CHANGES" = false ]; then
|
|
1938
|
+
log_dim "Commit" "nothing to commit"
|
|
1939
|
+
else
|
|
1940
|
+
if [ "$DRY_RUN" = false ]; then
|
|
1941
|
+
# Stage all changes FIRST (so pre-commit checks see what we're about to commit)
|
|
1942
|
+
git add -A
|
|
1943
|
+
STAGED_COUNT=$(git diff --cached --numstat | wc -l | tr -d ' ')
|
|
1944
|
+
log_ok "Staged" "$STAGED_COUNT files"
|
|
1945
|
+
|
|
1946
|
+
# ============================================================================
|
|
1947
|
+
# GUARDRAIL 3: Staged Changes Preview
|
|
1948
|
+
# ============================================================================
|
|
1949
|
+
# Show what's about to be committed for verification
|
|
1950
|
+
if [ "$FORCE" = false ] && [ "$STAGED_COUNT" -gt 10 ]; then
|
|
1951
|
+
echo ""
|
|
1952
|
+
log_warn "Staging" "$STAGED_COUNT files — review below"
|
|
1953
|
+
echo ""
|
|
1954
|
+
# Show summary by directory
|
|
1955
|
+
git diff --cached --stat | tail -20
|
|
1956
|
+
echo ""
|
|
1957
|
+
# Show file types being changed
|
|
1958
|
+
printf " ${DIM}%-16s${NC}\n" "File types:"
|
|
1959
|
+
git diff --cached --name-only | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -5
|
|
1960
|
+
echo ""
|
|
1961
|
+
echo " Verify this matches your intended changes."
|
|
1962
|
+
echo " To abort: Ctrl+C then 'git reset HEAD'"
|
|
1963
|
+
echo " To skip: poetic sync --force"
|
|
1964
|
+
echo ""
|
|
1965
|
+
# Brief pause for human review (non-blocking in CI)
|
|
1966
|
+
if [ -t 0 ]; then
|
|
1967
|
+
sleep 2
|
|
1968
|
+
fi
|
|
1969
|
+
fi
|
|
1970
|
+
|
|
1971
|
+
# Pre-commit validation
|
|
1972
|
+
log_dim "Pre-commit" "checking..."
|
|
1973
|
+
|
|
1974
|
+
# Check for sensitive filenames (most reliable check)
|
|
1975
|
+
SENSITIVE_FILES=$(git diff --cached --name-only | grep -E '\.(env|key|pem|p12|pfx|credentials|secret)$|^\.env' || true)
|
|
1976
|
+
if [ -n "$SENSITIVE_FILES" ]; then
|
|
1977
|
+
log_err "Blocked" "sensitive files detected"
|
|
1978
|
+
echo "$SENSITIVE_FILES"
|
|
1979
|
+
echo ""
|
|
1980
|
+
echo "To remove: git reset HEAD <file>"
|
|
1981
|
+
exit 1
|
|
1982
|
+
fi
|
|
1983
|
+
|
|
1984
|
+
# Check for actual secret values (pattern: key="value" or key: "value")
|
|
1985
|
+
# Skip known-safe paths (test fixtures, benchmarks, coverage, etc.)
|
|
1986
|
+
SKIP_SECRET_CHECK='\.poetic/|test-results|benchmarks|coverage|\.test\.|__tests__|fixtures|\.snap$'
|
|
1987
|
+
FILES_TO_CHECK=$(git diff --cached --name-only | grep -vE "$SKIP_SECRET_CHECK" || true)
|
|
1988
|
+
|
|
1989
|
+
SECRET_VALUE_PATTERNS=(
|
|
1990
|
+
'["\x27][a-zA-Z0-9_-]{20,}["\x27].*?(api[_-]?key|password|token|secret)'
|
|
1991
|
+
'(api[_-]?key|password|token|secret)["\x27]?\s*[:=]\s*["\x27][^"\x27]{8,}'
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
SECRETS_FOUND=false
|
|
1995
|
+
FLAGGED_FILES=""
|
|
1996
|
+
if [ -n "$FILES_TO_CHECK" ]; then
|
|
1997
|
+
for pattern in "${SECRET_VALUE_PATTERNS[@]}"; do
|
|
1998
|
+
# Only check non-allowlisted files
|
|
1999
|
+
MATCHES=$(git diff --cached -- $FILES_TO_CHECK 2>/dev/null | grep -iE "$pattern" || true)
|
|
2000
|
+
if [ -n "$MATCHES" ]; then
|
|
2001
|
+
FLAGGED_FILES=$(git diff --cached --name-only -- $FILES_TO_CHECK | head -10)
|
|
2002
|
+
SECRETS_FOUND=true
|
|
2003
|
+
break
|
|
2004
|
+
fi
|
|
2005
|
+
done
|
|
2006
|
+
fi
|
|
2007
|
+
|
|
2008
|
+
if [ "$SECRETS_FOUND" = true ]; then
|
|
2009
|
+
# WARN instead of BLOCK - let AI/human review
|
|
2010
|
+
log_warn "Secrets" "possible secrets detected in:"
|
|
2011
|
+
echo "$FLAGGED_FILES"
|
|
2012
|
+
echo ""
|
|
2013
|
+
echo "Review these files: git diff --cached <file>"
|
|
2014
|
+
echo "To unstage if needed: git reset HEAD <file>"
|
|
2015
|
+
echo ""
|
|
2016
|
+
# Continue - don't exit (was: exit 1)
|
|
2017
|
+
HAS_WARNINGS=true
|
|
2018
|
+
fi
|
|
2019
|
+
|
|
2020
|
+
# Check for merge conflict markers
|
|
2021
|
+
if git diff --cached | grep -q "^<<<<<<< \|^======= \|^>>>>>>> "; then
|
|
2022
|
+
log_err "Blocked" "merge conflict markers in staged files"
|
|
2023
|
+
echo "Please resolve conflicts before committing"
|
|
2024
|
+
exit 1
|
|
2025
|
+
fi
|
|
2026
|
+
|
|
2027
|
+
# Check for large files (>10MB) - skip interactive prompt for automated workflows
|
|
2028
|
+
LARGE_FILES=$(git diff --cached --name-only | xargs -I {} find {} -type f -size +10M 2>/dev/null || true)
|
|
2029
|
+
if [ -n "$LARGE_FILES" ]; then
|
|
2030
|
+
log_warn "Large files" "detected (>10MB):"
|
|
2031
|
+
echo "$LARGE_FILES"
|
|
2032
|
+
echo ""
|
|
2033
|
+
echo "Consider using Git LFS for large files."
|
|
2034
|
+
echo "To exclude: git reset HEAD <file>"
|
|
2035
|
+
# Don't block - just warn (for automated end-of-day workflow)
|
|
2036
|
+
fi
|
|
2037
|
+
|
|
2038
|
+
if [ "$FORCE" = false ] && [ "$IS_PROTECTED" = true ] && staged_has_mixed_system_and_code; then
|
|
2039
|
+
echo ""
|
|
2040
|
+
log_err "Blocked" "mixed planning metadata and code changes staged on protected branch '$CURRENT_BRANCH'"
|
|
2041
|
+
echo ""
|
|
2042
|
+
echo " This staged set would create a misleading combined sync commit."
|
|
2043
|
+
echo " Move the work onto a feature/run branch, or split the staged changes first."
|
|
2044
|
+
echo ""
|
|
2045
|
+
echo " Recommended:"
|
|
2046
|
+
echo " git reset HEAD task/ sprint/ backlog/"
|
|
2047
|
+
echo " git checkout -b <run-branch>"
|
|
2048
|
+
echo " poetic sync"
|
|
2049
|
+
echo ""
|
|
2050
|
+
exit 1
|
|
2051
|
+
fi
|
|
2052
|
+
|
|
2053
|
+
GENERATED_SYNC_ARTIFACTS=$(get_staged_generated_artifacts)
|
|
2054
|
+
if [ -n "$GENERATED_SYNC_ARTIFACTS" ]; then
|
|
2055
|
+
generated_count=$(echo "$GENERATED_SYNC_ARTIFACTS" | wc -l | tr -d ' ')
|
|
2056
|
+
log_dim "Generated" "$generated_count generated artifact(s) staged"
|
|
2057
|
+
if [ "$ROUTE_MAIN_MODE_ACTIVE" = true ]; then
|
|
2058
|
+
log_dim "Route-main" "generated artifacts will be routed via PR"
|
|
2059
|
+
fi
|
|
2060
|
+
if [ "$VERBOSE" = true ]; then
|
|
2061
|
+
echo "$GENERATED_SYNC_ARTIFACTS" | while IFS= read -r generated_file; do
|
|
2062
|
+
[ -n "$generated_file" ] || continue
|
|
2063
|
+
log_dim "Generated file" "$generated_file"
|
|
2064
|
+
done
|
|
2065
|
+
fi
|
|
2066
|
+
fi
|
|
2067
|
+
|
|
2068
|
+
# Generate or use commit message
|
|
2069
|
+
if [ -z "$COMMIT_MESSAGE" ]; then
|
|
2070
|
+
# Auto-generate from staged changes
|
|
2071
|
+
COMMIT_MESSAGE=$(generate_commit_message)
|
|
2072
|
+
|
|
2073
|
+
if [ "$VERBOSE" = true ]; then
|
|
2074
|
+
log_dim "Message" "$COMMIT_MESSAGE"
|
|
2075
|
+
fi
|
|
2076
|
+
fi
|
|
2077
|
+
|
|
2078
|
+
# Commit
|
|
2079
|
+
if git commit -m "$COMMIT_MESSAGE" >/dev/null 2>&1; then
|
|
2080
|
+
CREATED_COMMIT_SHA=$(git rev-parse HEAD)
|
|
2081
|
+
CREATED_COMMIT_PARENT_SHA=$(git rev-parse "${CREATED_COMMIT_SHA}^")
|
|
2082
|
+
log_ok "Commit" "$COMMIT_MESSAGE"
|
|
2083
|
+
else
|
|
2084
|
+
log_err "Commit" "failed"
|
|
2085
|
+
HAS_FAILURE=true
|
|
2086
|
+
exit 1
|
|
2087
|
+
fi
|
|
2088
|
+
else
|
|
2089
|
+
echo ""
|
|
2090
|
+
echo "Would stage and commit:"
|
|
2091
|
+
git status --short
|
|
2092
|
+
echo ""
|
|
2093
|
+
if [ -n "$COMMIT_MESSAGE" ]; then
|
|
2094
|
+
echo "Commit message: \"$COMMIT_MESSAGE\""
|
|
2095
|
+
else
|
|
2096
|
+
echo "Commit message: (auto-generated)"
|
|
2097
|
+
fi
|
|
2098
|
+
fi
|
|
2099
|
+
fi
|
|
2100
|
+
fi
|
|
2101
|
+
|
|
2102
|
+
# STEP 4: Merge from main (if not on main)
|
|
2103
|
+
if [ "$CURRENT_BRANCH" != "$MAIN_BRANCH" ] && [ "$DRY_RUN" = false ] && [ "$SKIP_COMMIT" = false ]; then
|
|
2104
|
+
# Use rebase or merge based on flag
|
|
2105
|
+
if [ "$REBASE" = true ]; then
|
|
2106
|
+
if git rebase --rebase-merges "origin/$MAIN_BRANCH" >/dev/null 2>&1; then
|
|
2107
|
+
log_ok "Rebase" "onto $MAIN_BRANCH"
|
|
2108
|
+
else
|
|
2109
|
+
if git diff --quiet HEAD "origin/$MAIN_BRANCH"; then
|
|
2110
|
+
git rebase --abort 2>/dev/null || true
|
|
2111
|
+
log_dim "Rebase" "up-to-date with $MAIN_BRANCH"
|
|
2112
|
+
else
|
|
2113
|
+
log_err "Rebase" "conflict detected"
|
|
2114
|
+
git rebase --abort 2>/dev/null || true
|
|
2115
|
+
print_recovery_hint
|
|
2116
|
+
echo "Conflicted files:"
|
|
2117
|
+
git diff --name-only --diff-filter=U 2>/dev/null || echo "(run git status to see conflicts)"
|
|
2118
|
+
echo ""
|
|
2119
|
+
echo "To resolve manually:"
|
|
2120
|
+
echo " 1. git rebase origin/$MAIN_BRANCH"
|
|
2121
|
+
echo " 2. Edit conflicted files"
|
|
2122
|
+
echo " 3. git add <resolved-files>"
|
|
2123
|
+
echo " 4. git rebase --continue"
|
|
2124
|
+
echo " 5. Run poetic sync again to push"
|
|
2125
|
+
exit 1
|
|
2126
|
+
fi
|
|
2127
|
+
fi
|
|
2128
|
+
else
|
|
2129
|
+
if git merge "origin/$MAIN_BRANCH" --no-edit >/dev/null 2>&1; then
|
|
2130
|
+
log_ok "Merge" "from $MAIN_BRANCH"
|
|
2131
|
+
else
|
|
2132
|
+
if git diff --quiet HEAD "origin/$MAIN_BRANCH"; then
|
|
2133
|
+
log_dim "Merge" "up-to-date with $MAIN_BRANCH"
|
|
2134
|
+
else
|
|
2135
|
+
log_err "Merge" "conflict detected"
|
|
2136
|
+
print_recovery_hint
|
|
2137
|
+
echo "Conflicted files:"
|
|
2138
|
+
git diff --name-only --diff-filter=U
|
|
2139
|
+
echo ""
|
|
2140
|
+
echo "To resolve manually:"
|
|
2141
|
+
echo " 1. Edit conflicted files"
|
|
2142
|
+
echo " 2. git add <resolved-files>"
|
|
2143
|
+
echo " 3. git commit"
|
|
2144
|
+
echo " 4. Run poetic sync again to push"
|
|
2145
|
+
exit 1
|
|
2146
|
+
fi
|
|
2147
|
+
fi
|
|
2148
|
+
fi
|
|
2149
|
+
fi
|
|
2150
|
+
|
|
2151
|
+
# STEP 5: Push to remote or create PR
|
|
2152
|
+
if [ "$DRY_RUN" = false ] && [ "$SKIP_COMMIT" = false ]; then
|
|
2153
|
+
|
|
2154
|
+
# Route-main: if enabled for a main/master commit, route via deterministic PR branch (no direct push to origin/main)
|
|
2155
|
+
if [ "$ROUTE_MAIN_MODE_ACTIVE" = true ] || { [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$ROUTE_MAIN_EFFECTIVE" = true ]; }; then
|
|
2156
|
+
if route_main_head_to_pr "$MAIN_BRANCH" "$STAY_ON_MAIN"; then
|
|
2157
|
+
PUSH_SUCCESS=true
|
|
2158
|
+
# Route-main already ensured the PR for the route branch, then checked
|
|
2159
|
+
# back out to main. A standalone --ensure-pr (ENSURE_PR=true) would now
|
|
2160
|
+
# re-trigger PR creation in Step 5b while on main and hard-fail with
|
|
2161
|
+
# "cannot create from main". Route-main subsumes ensure-pr, so mark it
|
|
2162
|
+
# satisfied here. (--route-main --ensure-pr must be a no-op overlap, not a trap.)
|
|
2163
|
+
ENSURE_PR=false
|
|
2164
|
+
# Best-effort cleanup: remove temporary WIP branch used to avoid committing on main.
|
|
2165
|
+
if [ -n "${ROUTE_MAIN_WIP_BRANCH:-}" ]; then
|
|
2166
|
+
git branch -D "$ROUTE_MAIN_WIP_BRANCH" >/dev/null 2>&1 || true
|
|
2167
|
+
ROUTE_MAIN_WIP_BRANCH=""
|
|
2168
|
+
fi
|
|
2169
|
+
else
|
|
2170
|
+
log_err "Route-main" "failed"
|
|
2171
|
+
exit 1
|
|
2172
|
+
fi
|
|
2173
|
+
else
|
|
2174
|
+
# If --pr is used on a non-main branch, treat it as an intent to ensure a PR for this branch
|
|
2175
|
+
if [ "$CREATE_PR" = true ] && [ "$CURRENT_BRANCH" != "$MAIN_BRANCH" ]; then
|
|
2176
|
+
ENSURE_PR=true
|
|
2177
|
+
fi
|
|
2178
|
+
|
|
2179
|
+
# Determine if we should create a PR
|
|
2180
|
+
SHOULD_CREATE_PR=false
|
|
2181
|
+
if [ "$CREATE_PR" = true ] && [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ]; then
|
|
2182
|
+
SHOULD_CREATE_PR=true
|
|
2183
|
+
fi
|
|
2184
|
+
|
|
2185
|
+
if [ "$SHOULD_CREATE_PR" = true ]; then
|
|
2186
|
+
if [ -z "$CREATED_COMMIT_SHA" ] || [ "$(git rev-parse HEAD)" != "$CREATED_COMMIT_SHA" ]; then
|
|
2187
|
+
log_err "PR" "no sync-owned commit available to route"
|
|
2188
|
+
exit 1
|
|
2189
|
+
fi
|
|
2190
|
+
|
|
2191
|
+
# Generate branch name from commit message or timestamp
|
|
2192
|
+
PR_BRANCH_NAME=""
|
|
2193
|
+
if [ -n "$COMMIT_MESSAGE" ]; then
|
|
2194
|
+
# Convert message to branch-safe name
|
|
2195
|
+
PR_BRANCH_NAME="pr/$(echo "$COMMIT_MESSAGE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//' | cut -c1-50)"
|
|
2196
|
+
else
|
|
2197
|
+
PR_BRANCH_NAME="pr/changes-$(date +%Y%m%d-%H%M%S)"
|
|
2198
|
+
fi
|
|
2199
|
+
|
|
2200
|
+
# Use only the commit created by this invocation. The original branch
|
|
2201
|
+
# is restored with a compare-and-swap below so foreign ref updates can
|
|
2202
|
+
# never be rewound by this cleanup.
|
|
2203
|
+
COMMIT_SHA="$CREATED_COMMIT_SHA"
|
|
2204
|
+
|
|
2205
|
+
# Create and checkout PR branch from current position
|
|
2206
|
+
if git checkout -b "$PR_BRANCH_NAME" >/dev/null 2>&1; then
|
|
2207
|
+
log_ok "Branch" "$PR_BRANCH_NAME"
|
|
2208
|
+
else
|
|
2209
|
+
log_err "Branch" "creation failed"
|
|
2210
|
+
exit 1
|
|
2211
|
+
fi
|
|
2212
|
+
|
|
2213
|
+
# Remove our temporary commit from the original branch before the
|
|
2214
|
+
# network/PR window. update-ref's old-value check makes this rollback
|
|
2215
|
+
# fail closed if another process has already advanced the branch.
|
|
2216
|
+
if git update-ref "refs/heads/$CURRENT_BRANCH" "$CREATED_COMMIT_PARENT_SHA" "$COMMIT_SHA"; then
|
|
2217
|
+
log_dim "Reset" "$CURRENT_BRANCH (commit on PR branch)"
|
|
2218
|
+
else
|
|
2219
|
+
log_err "Reset" "$CURRENT_BRANCH changed concurrently; preserved $PR_BRANCH_NAME"
|
|
2220
|
+
git checkout "$CURRENT_BRANCH" >/dev/null 2>&1 || true
|
|
2221
|
+
exit 1
|
|
2222
|
+
fi
|
|
2223
|
+
|
|
2224
|
+
# Push PR branch
|
|
2225
|
+
if run_git_push -u origin "$PR_BRANCH_NAME"; then
|
|
2226
|
+
log_ok "Push" "origin/$PR_BRANCH_NAME"
|
|
2227
|
+
else
|
|
2228
|
+
log_err "Push" "failed: $PR_BRANCH_NAME"
|
|
2229
|
+
log_git_push_reason
|
|
2230
|
+
git checkout "$CURRENT_BRANCH" >/dev/null 2>&1
|
|
2231
|
+
log_dim "Recovery" "commit preserved on $PR_BRANCH_NAME"
|
|
2232
|
+
exit 1
|
|
2233
|
+
fi
|
|
2234
|
+
|
|
2235
|
+
# Create PR using gh cli
|
|
2236
|
+
PR_TITLE="${COMMIT_MESSAGE:-$(git log -1 --pretty=%s 2>/dev/null || echo "Sync changes")}"
|
|
2237
|
+
PR_BODY=$(build_sync_pr_body "pr" "$PR_BRANCH_NAME" "$CURRENT_BRANCH")
|
|
2238
|
+
|
|
2239
|
+
if command -v gh >/dev/null 2>&1; then
|
|
2240
|
+
PR_URL=$(gh pr create --title "$PR_TITLE" --body "$PR_BODY" --base "$MAIN_BRANCH" 2>&1)
|
|
2241
|
+
if [ $? -eq 0 ]; then
|
|
2242
|
+
log_ok "PR" "$PR_URL"
|
|
2243
|
+
CREATED_PR_URL="$PR_URL"
|
|
2244
|
+
AUTO_MERGE_LABEL_ADDED=false
|
|
2245
|
+
if echo "$PR_URL" | grep -Eq "^https?://"; then
|
|
2246
|
+
if try_add_automerge_label "$PR_URL"; then
|
|
2247
|
+
AUTO_MERGE_LABEL_ADDED=true
|
|
2248
|
+
log_ok "PR label" "automerge"
|
|
2249
|
+
else
|
|
2250
|
+
log_warn "PR label" "automerge not added"
|
|
2251
|
+
fi
|
|
2252
|
+
fi
|
|
2253
|
+
PUSH_SUCCESS=true
|
|
2254
|
+
|
|
2255
|
+
echo ""
|
|
2256
|
+
log_dim "CI" "Quality Gate will run automatically"
|
|
2257
|
+
if [ "$AUTO_MERGE_LABEL_ADDED" = true ]; then
|
|
2258
|
+
log_dim "Merge" "will auto-merge if CI passes"
|
|
2259
|
+
else
|
|
2260
|
+
log_dim "Merge" "label 'automerge' needed for auto-merge"
|
|
2261
|
+
fi
|
|
2262
|
+
else
|
|
2263
|
+
log_warn "PR" "creation failed: $PR_URL"
|
|
2264
|
+
log_dim "PR" "branch pushed — create PR manually"
|
|
2265
|
+
PUSH_SUCCESS=true
|
|
2266
|
+
fi
|
|
2267
|
+
else
|
|
2268
|
+
log_warn "PR" "gh cli not found"
|
|
2269
|
+
echo " Branch: $PR_BRANCH_NAME"
|
|
2270
|
+
echo " URL: https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]//' | sed 's/.git$//')/pull/new/$PR_BRANCH_NAME"
|
|
2271
|
+
PUSH_SUCCESS=true
|
|
2272
|
+
fi
|
|
2273
|
+
|
|
2274
|
+
# Return to original branch
|
|
2275
|
+
git checkout "$CURRENT_BRANCH" >/dev/null 2>&1
|
|
2276
|
+
|
|
2277
|
+
else
|
|
2278
|
+
# Original direct push logic
|
|
2279
|
+
|
|
2280
|
+
# Use cached UPSTREAM_EXISTS instead of rechecking
|
|
2281
|
+
if [ "$UPSTREAM_EXISTS" = false ]; then
|
|
2282
|
+
if ! should_publish_new_branch "$CURRENT_BRANCH"; then
|
|
2283
|
+
log_err "Push" "no upstream — refusing to publish"
|
|
2284
|
+
echo ""
|
|
2285
|
+
if is_wip_named_branch "$CURRENT_BRANCH"; then
|
|
2286
|
+
echo "Note: '$CURRENT_BRANCH' looks like a local-only/WIP branch (local/*, wip/*, tmp/*, validation/*, etc.)"
|
|
2287
|
+
echo ""
|
|
2288
|
+
fi
|
|
2289
|
+
echo "To publish and set upstream (force):"
|
|
2290
|
+
echo " poetic sync --publish"
|
|
2291
|
+
echo ""
|
|
2292
|
+
echo "To enable auto-publish by default:"
|
|
2293
|
+
echo " poetic config set --scope user sync.autoPublish true"
|
|
2294
|
+
echo ""
|
|
2295
|
+
echo "Or manually:"
|
|
2296
|
+
echo " git push -u origin $CURRENT_BRANCH"
|
|
2297
|
+
exit 1
|
|
2298
|
+
fi
|
|
2299
|
+
|
|
2300
|
+
if run_git_push -u origin "$CURRENT_BRANCH"; then
|
|
2301
|
+
log_ok "Push" "origin/$CURRENT_BRANCH (set upstream)"
|
|
2302
|
+
PUSH_SUCCESS=true
|
|
2303
|
+
UPSTREAM_EXISTS=true
|
|
2304
|
+
UPSTREAM_BRANCH="origin/$CURRENT_BRANCH"
|
|
2305
|
+
else
|
|
2306
|
+
log_err "Push" "failed (new branch)"
|
|
2307
|
+
log_git_push_reason
|
|
2308
|
+
PUSH_SUCCESS=false
|
|
2309
|
+
HAS_FAILURE=true
|
|
2310
|
+
exit 1
|
|
2311
|
+
fi
|
|
2312
|
+
else
|
|
2313
|
+
# Try push, with automatic retry on rejection
|
|
2314
|
+
if run_git_push; then
|
|
2315
|
+
log_ok "Push" "$UPSTREAM_BRANCH"
|
|
2316
|
+
PUSH_SUCCESS=true
|
|
2317
|
+
else
|
|
2318
|
+
log_warn "Push" "rejected — remote may have changed"
|
|
2319
|
+
log_git_push_reason
|
|
2320
|
+
log_dim "Push" "retrying after rebase..."
|
|
2321
|
+
|
|
2322
|
+
# Auto-recover: pull, rebase, retry push
|
|
2323
|
+
if git pull --rebase=merges >/dev/null 2>&1; then
|
|
2324
|
+
log_ok "Rebase" "onto remote changes"
|
|
2325
|
+
|
|
2326
|
+
if run_git_push; then
|
|
2327
|
+
log_ok "Push" "$UPSTREAM_BRANCH (after rebase)"
|
|
2328
|
+
PUSH_SUCCESS=true
|
|
2329
|
+
else
|
|
2330
|
+
log_err "Push" "still rejected after rebase"
|
|
2331
|
+
log_git_push_reason
|
|
2332
|
+
PUSH_SUCCESS=false
|
|
2333
|
+
HAS_FAILURE=true
|
|
2334
|
+
|
|
2335
|
+
echo ""
|
|
2336
|
+
print_recovery_hint
|
|
2337
|
+
echo "Push failed even after rebase. Possible causes:"
|
|
2338
|
+
echo " - Force push from another location"
|
|
2339
|
+
echo " - Protected branch rules"
|
|
2340
|
+
echo " - Permission issues"
|
|
2341
|
+
echo ""
|
|
2342
|
+
echo "To investigate:"
|
|
2343
|
+
echo " 1. git log --oneline -3 (see your commits)"
|
|
2344
|
+
echo " 2. git status (see current state)"
|
|
2345
|
+
echo " 3. Check branch protection rules"
|
|
2346
|
+
exit 1
|
|
2347
|
+
fi
|
|
2348
|
+
else
|
|
2349
|
+
log_err "Pull" "failed during recovery"
|
|
2350
|
+
PUSH_SUCCESS=false
|
|
2351
|
+
HAS_FAILURE=true
|
|
2352
|
+
|
|
2353
|
+
print_recovery_hint
|
|
2354
|
+
echo "Recovery pull failed. Resolve manually:"
|
|
2355
|
+
echo " 1. git pull --rebase (resolve any conflicts)"
|
|
2356
|
+
echo " 2. git push"
|
|
2357
|
+
exit 1
|
|
2358
|
+
fi
|
|
2359
|
+
fi
|
|
2360
|
+
fi
|
|
2361
|
+
fi
|
|
2362
|
+
fi
|
|
2363
|
+
fi
|
|
2364
|
+
|
|
2365
|
+
# STEP 5b: PR status / ensure (show-only by default)
|
|
2366
|
+
if [ "$SYNC_ONLY" = false ]; then
|
|
2367
|
+
# Avoid PR creation directly from main/master (use route-main instead)
|
|
2368
|
+
if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ]; then
|
|
2369
|
+
if [ "$ENSURE_PR" = true ]; then
|
|
2370
|
+
log_err "PR" "cannot create from '$MAIN_BRANCH' — use --route-main"
|
|
2371
|
+
exit 1
|
|
2372
|
+
fi
|
|
2373
|
+
else
|
|
2374
|
+
EXISTING_PR_URL=$(get_open_pr_url_for_head "$CURRENT_BRANCH")
|
|
2375
|
+
if [ -n "$EXISTING_PR_URL" ]; then
|
|
2376
|
+
log_dim "PR" "$EXISTING_PR_URL"
|
|
2377
|
+
CREATED_PR_URL="$EXISTING_PR_URL"
|
|
2378
|
+
elif [ "$ENSURE_PR" = true ]; then
|
|
2379
|
+
PR_TITLE="${COMMIT_MESSAGE:-$(git log -1 --pretty=%s 2>/dev/null || echo "$CURRENT_BRANCH")}"
|
|
2380
|
+
PR_BODY=$(build_sync_pr_body "ensure-pr" "$CURRENT_BRANCH" "$MAIN_BRANCH")
|
|
2381
|
+
ensure_pr_for_head "$CURRENT_BRANCH" "$MAIN_BRANCH" "$PR_TITLE" "$PR_BODY" || true
|
|
2382
|
+
fi
|
|
2383
|
+
fi
|
|
2384
|
+
fi
|
|
2385
|
+
|
|
2386
|
+
# STEP 6: Cross-Environment Status Report
|
|
2387
|
+
echo ""
|
|
2388
|
+
echo " Branch Status"
|
|
2389
|
+
printf ' %0.s' $(seq 1 40); echo ""
|
|
2390
|
+
|
|
2391
|
+
# === CACHE BRANCH STATUS (Fix 1: Eliminate duplicate git operations) ===
|
|
2392
|
+
# Scan all branches ONCE and cache the results
|
|
2393
|
+
# Note: Associative arrays require bash 4+
|
|
2394
|
+
# Temporarily disable unbound variable check for caching section
|
|
2395
|
+
set +u
|
|
2396
|
+
|
|
2397
|
+
# Check bash version for associative array support
|
|
2398
|
+
BASH_MAJOR="${BASH_VERSION%%.*}"
|
|
2399
|
+
HAS_ASSOC_ARRAYS=false
|
|
2400
|
+
if [ "$BASH_MAJOR" -ge 4 ] 2>/dev/null; then
|
|
2401
|
+
HAS_ASSOC_ARRAYS=true
|
|
2402
|
+
declare -A BRANCH_AHEAD 2>/dev/null || HAS_ASSOC_ARRAYS=false
|
|
2403
|
+
declare -A BRANCH_BEHIND 2>/dev/null || HAS_ASSOC_ARRAYS=false
|
|
2404
|
+
declare -A BRANCH_HAS_UPSTREAM 2>/dev/null || HAS_ASSOC_ARRAYS=false
|
|
2405
|
+
fi
|
|
2406
|
+
|
|
2407
|
+
# Cache current branch status (for bash 4+) or use simple variables (bash 3.2)
|
|
2408
|
+
if [ "$DETACHED_HEAD" = true ]; then
|
|
2409
|
+
CURRENT_HAS_UPSTREAM=false
|
|
2410
|
+
CURRENT_AHEAD=0
|
|
2411
|
+
CURRENT_BEHIND=0
|
|
2412
|
+
elif git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
|
|
2413
|
+
CURRENT_HAS_UPSTREAM=true
|
|
2414
|
+
CURRENT_AHEAD=$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo "0")
|
|
2415
|
+
CURRENT_BEHIND=$(git rev-list --count "HEAD..@{u}" 2>/dev/null || echo "0")
|
|
2416
|
+
if [ "$HAS_ASSOC_ARRAYS" = true ]; then
|
|
2417
|
+
BRANCH_HAS_UPSTREAM["$CURRENT_BRANCH"]=true
|
|
2418
|
+
BRANCH_AHEAD["$CURRENT_BRANCH"]="$CURRENT_AHEAD"
|
|
2419
|
+
BRANCH_BEHIND["$CURRENT_BRANCH"]="$CURRENT_BEHIND"
|
|
2420
|
+
fi
|
|
2421
|
+
else
|
|
2422
|
+
CURRENT_HAS_UPSTREAM=false
|
|
2423
|
+
CURRENT_AHEAD=0
|
|
2424
|
+
CURRENT_BEHIND=0
|
|
2425
|
+
if [ "$HAS_ASSOC_ARRAYS" = true ]; then
|
|
2426
|
+
BRANCH_HAS_UPSTREAM["$CURRENT_BRANCH"]=false
|
|
2427
|
+
BRANCH_AHEAD["$CURRENT_BRANCH"]=0
|
|
2428
|
+
BRANCH_BEHIND["$CURRENT_BRANCH"]=0
|
|
2429
|
+
fi
|
|
2430
|
+
fi
|
|
2431
|
+
|
|
2432
|
+
# Cache other branches status (only on bash 4+ and verbose mode)
|
|
2433
|
+
if [ "$HAS_ASSOC_ARRAYS" = true ] && [ "$VERBOSE" = true ] && [ -n "$TRACKED_BRANCHES" ]; then
|
|
2434
|
+
while IFS= read -r line; do
|
|
2435
|
+
branch=$(echo "$line" | awk '{print $1}')
|
|
2436
|
+
upstream=$(echo "$line" | awk '{print $2}')
|
|
2437
|
+
|
|
2438
|
+
if [ "$branch" != "$CURRENT_BRANCH" ]; then
|
|
2439
|
+
BRANCH_HAS_UPSTREAM["$branch"]=true
|
|
2440
|
+
BRANCH_AHEAD["$branch"]=$(git rev-list --count "$upstream..$branch" 2>/dev/null || echo "0")
|
|
2441
|
+
BRANCH_BEHIND["$branch"]=$(git rev-list --count "$branch..$upstream" 2>/dev/null || echo "0")
|
|
2442
|
+
fi
|
|
2443
|
+
done <<< "$TRACKED_BRANCHES"
|
|
2444
|
+
fi
|
|
2445
|
+
|
|
2446
|
+
# === SUMMARY SECTION (using cached values) ===
|
|
2447
|
+
# Note: Keeping set +u active for array access throughout status reporting
|
|
2448
|
+
|
|
2449
|
+
echo " Summary"
|
|
2450
|
+
printf ' %0.s' $(seq 1 40); echo ""
|
|
2451
|
+
|
|
2452
|
+
# Count synced vs warning branches using cached values
|
|
2453
|
+
SYNCED_COUNT=0
|
|
2454
|
+
WARNING_COUNT=0
|
|
2455
|
+
|
|
2456
|
+
# Check current branch (use simple variables - works on all bash versions)
|
|
2457
|
+
if [ "$CURRENT_HAS_UPSTREAM" = true ]; then
|
|
2458
|
+
if [ "$CURRENT_AHEAD" -eq 0 ] && [ "$CURRENT_BEHIND" -eq 0 ]; then
|
|
2459
|
+
SYNCED_COUNT=$((SYNCED_COUNT + 1))
|
|
2460
|
+
else
|
|
2461
|
+
WARNING_COUNT=$((WARNING_COUNT + 1))
|
|
2462
|
+
HAS_WARNINGS=true
|
|
2463
|
+
fi
|
|
2464
|
+
fi
|
|
2465
|
+
|
|
2466
|
+
# Check other branches using cached values (bash 4+ only, verbose only)
|
|
2467
|
+
if [ "$HAS_ASSOC_ARRAYS" = true ] && [ "$VERBOSE" = true ] && [ -n "$TRACKED_BRANCHES" ]; then
|
|
2468
|
+
while IFS= read -r line; do
|
|
2469
|
+
branch=$(echo "$line" | awk '{print $1}')
|
|
2470
|
+
|
|
2471
|
+
if [ "$branch" != "$CURRENT_BRANCH" ] && [ "${BRANCH_HAS_UPSTREAM["$branch"]}" = true ]; then
|
|
2472
|
+
AHEAD="${BRANCH_AHEAD["$branch"]}"
|
|
2473
|
+
BEHIND="${BRANCH_BEHIND["$branch"]}"
|
|
2474
|
+
|
|
2475
|
+
if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ]; then
|
|
2476
|
+
SYNCED_COUNT=$((SYNCED_COUNT + 1))
|
|
2477
|
+
else
|
|
2478
|
+
WARNING_COUNT=$((WARNING_COUNT + 1))
|
|
2479
|
+
HAS_WARNINGS=true
|
|
2480
|
+
fi
|
|
2481
|
+
fi
|
|
2482
|
+
done <<< "$TRACKED_BRANCHES"
|
|
2483
|
+
fi
|
|
2484
|
+
|
|
2485
|
+
# Display summary counts
|
|
2486
|
+
if [ "$SYNCED_COUNT" -gt 0 ]; then
|
|
2487
|
+
log_ok "Synced" "$SYNCED_COUNT branch(es)"
|
|
2488
|
+
fi
|
|
2489
|
+
if [ "$WARNING_COUNT" -gt 0 ]; then
|
|
2490
|
+
log_warn "Warnings" "$WARNING_COUNT branch(es)"
|
|
2491
|
+
fi
|
|
2492
|
+
|
|
2493
|
+
echo ""
|
|
2494
|
+
echo " Details"
|
|
2495
|
+
printf ' %0.s' $(seq 1 40); echo ""
|
|
2496
|
+
|
|
2497
|
+
# Current branch status (use simple variables - works on all bash versions)
|
|
2498
|
+
if [ "$DETACHED_HEAD" = true ]; then
|
|
2499
|
+
log_dim "detached HEAD" "$DETACHED_HEAD_SHA (no branch)"
|
|
2500
|
+
elif [ "$CURRENT_HAS_UPSTREAM" = true ]; then
|
|
2501
|
+
if [ "$CURRENT_AHEAD" -eq 0 ] && [ "$CURRENT_BEHIND" -eq 0 ]; then
|
|
2502
|
+
log_ok "$CURRENT_BRANCH" "synced"
|
|
2503
|
+
elif [ "$CURRENT_AHEAD" -gt 0 ] && [ "$CURRENT_BEHIND" -eq 0 ]; then
|
|
2504
|
+
log_warn "$CURRENT_BRANCH" "$CURRENT_AHEAD ahead (needs push)"
|
|
2505
|
+
elif [ "$CURRENT_AHEAD" -eq 0 ] && [ "$CURRENT_BEHIND" -gt 0 ]; then
|
|
2506
|
+
log_warn "$CURRENT_BRANCH" "$CURRENT_BEHIND behind (needs pull)"
|
|
2507
|
+
else
|
|
2508
|
+
log_warn "$CURRENT_BRANCH" "$CURRENT_AHEAD ahead, $CURRENT_BEHIND behind (diverged)"
|
|
2509
|
+
fi
|
|
2510
|
+
else
|
|
2511
|
+
log_dim "$CURRENT_BRANCH" "no upstream tracking"
|
|
2512
|
+
fi
|
|
2513
|
+
|
|
2514
|
+
# Other tracked branches + untracked: only show in verbose mode (default: fast)
|
|
2515
|
+
if [ "$VERBOSE" = true ]; then
|
|
2516
|
+
# Other tracked branches (using cached values - bash 4+ only)
|
|
2517
|
+
if [ -n "$TRACKED_BRANCHES" ]; then
|
|
2518
|
+
if [ "$HAS_ASSOC_ARRAYS" = true ]; then
|
|
2519
|
+
echo "$TRACKED_BRANCHES" | while read -r branch upstream; do
|
|
2520
|
+
if [ "$branch" != "$CURRENT_BRANCH" ]; then
|
|
2521
|
+
AHEAD="${BRANCH_AHEAD["$branch"]}"
|
|
2522
|
+
BEHIND="${BRANCH_BEHIND["$branch"]}"
|
|
2523
|
+
|
|
2524
|
+
if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ]; then
|
|
2525
|
+
log_ok "$branch" "synced"
|
|
2526
|
+
elif [ "$AHEAD" -gt 0 ] && [ "$BEHIND" -eq 0 ]; then
|
|
2527
|
+
log_warn "$branch" "$AHEAD ahead (needs push)"
|
|
2528
|
+
elif [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -gt 0 ]; then
|
|
2529
|
+
log_warn "$branch" "$BEHIND behind (needs pull)"
|
|
2530
|
+
else
|
|
2531
|
+
log_warn "$branch" "+$AHEAD/-$BEHIND (diverged)"
|
|
2532
|
+
fi
|
|
2533
|
+
fi
|
|
2534
|
+
done
|
|
2535
|
+
else
|
|
2536
|
+
# Bash 3.2 fallback: fetch status inline (slower but works)
|
|
2537
|
+
echo "$TRACKED_BRANCHES" | while read -r branch upstream; do
|
|
2538
|
+
if [ "$branch" != "$CURRENT_BRANCH" ]; then
|
|
2539
|
+
AHEAD=$(git rev-list --count "$upstream..$branch" 2>/dev/null || echo "0")
|
|
2540
|
+
BEHIND=$(git rev-list --count "$branch..$upstream" 2>/dev/null || echo "0")
|
|
2541
|
+
|
|
2542
|
+
if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ]; then
|
|
2543
|
+
log_ok "$branch" "synced"
|
|
2544
|
+
elif [ "$AHEAD" -gt 0 ] && [ "$BEHIND" -eq 0 ]; then
|
|
2545
|
+
log_warn "$branch" "$AHEAD ahead (needs push)"
|
|
2546
|
+
elif [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -gt 0 ]; then
|
|
2547
|
+
log_warn "$branch" "$BEHIND behind (needs pull)"
|
|
2548
|
+
else
|
|
2549
|
+
log_warn "$branch" "+$AHEAD/-$BEHIND (diverged)"
|
|
2550
|
+
fi
|
|
2551
|
+
fi
|
|
2552
|
+
done
|
|
2553
|
+
fi
|
|
2554
|
+
fi
|
|
2555
|
+
|
|
2556
|
+
# Untracked local branches (no per-branch subprocess; single git call)
|
|
2557
|
+
UNTRACKED=$(git for-each-ref --format='%(if)%(upstream)%(then)%(else)%(refname:short)%(end)' refs/heads/ | grep -v '^$' || true)
|
|
2558
|
+
if [ -n "$UNTRACKED" ]; then
|
|
2559
|
+
echo ""
|
|
2560
|
+
printf " ${DIM}%-16s${NC}\n" "Untracked:"
|
|
2561
|
+
echo "$UNTRACKED" | while read -r branch; do
|
|
2562
|
+
log_dim "$branch" "local only"
|
|
2563
|
+
done
|
|
2564
|
+
fi
|
|
2565
|
+
fi # end verbose branch/untracked detail
|
|
2566
|
+
|
|
2567
|
+
# Remote-only branches + tip: verbose only
|
|
2568
|
+
if [ "$VERBOSE" = true ]; then
|
|
2569
|
+
REMOTE_ONLY=$(git branch -r --format='%(refname:short)' | sed 's/origin\///' | grep -v "HEAD" | while read -r rbranch; do
|
|
2570
|
+
if ! git show-ref --verify --quiet "refs/heads/$rbranch"; then
|
|
2571
|
+
echo "$rbranch"
|
|
2572
|
+
fi
|
|
2573
|
+
done)
|
|
2574
|
+
if [ -n "$REMOTE_ONLY" ]; then
|
|
2575
|
+
echo ""
|
|
2576
|
+
printf " ${DIM}%-16s${NC}\n" "Remote-only:"
|
|
2577
|
+
echo "$REMOTE_ONLY" | while read -r rbranch; do
|
|
2578
|
+
log_dim "$rbranch" "not checked out locally"
|
|
2579
|
+
done
|
|
2580
|
+
fi
|
|
2581
|
+
fi
|
|
2582
|
+
|
|
2583
|
+
# Restore strict unbound-variable checking (disabled at status cache section)
|
|
2584
|
+
set -u
|
|
2585
|
+
|
|
2586
|
+
# === CLEANUP BACKUP BRANCH (only if created and successful) ===
|
|
2587
|
+
if [ "$DRY_RUN" = false ] && [ "$BACKUP_BRANCH_CREATED" = true ]; then
|
|
2588
|
+
if [ "$PUSH_SUCCESS" = true ] || [ "$SYNC_MODE" = true ]; then
|
|
2589
|
+
if git branch -D "$BACKUP_BRANCH" >/dev/null 2>&1; then
|
|
2590
|
+
log_dim "Cleanup" "backup branch removed"
|
|
2591
|
+
fi
|
|
2592
|
+
fi
|
|
2593
|
+
fi
|
|
2594
|
+
|
|
2595
|
+
# === PRUNE STALE BACKUP BRANCHES (orphaned by prior failed/interrupted runs) ===
|
|
2596
|
+
if [ "$DRY_RUN" = false ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
|
|
2597
|
+
if [ "$PUSH_SUCCESS" = true ] || [ "$SYNC_MODE" = true ]; then
|
|
2598
|
+
prune_stale_backup_branches || true
|
|
2599
|
+
fi
|
|
2600
|
+
fi
|
|
2601
|
+
|
|
2602
|
+
# === POST-SYNC HOOKS (project-local) ===
|
|
2603
|
+
if [ "$DRY_RUN" = false ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
|
|
2604
|
+
run_post_sync_hooks
|
|
2605
|
+
fi
|
|
2606
|
+
|
|
2607
|
+
# === AUTO-CHECKOUT MAIN (optional) ===
|
|
2608
|
+
maybe_checkout_main_after_sync() {
|
|
2609
|
+
local checkout_default
|
|
2610
|
+
checkout_default=$(get_sync_checkout_main_default)
|
|
2611
|
+
if ! is_truthy "$checkout_default"; then
|
|
2612
|
+
return 0
|
|
2613
|
+
fi
|
|
2614
|
+
|
|
2615
|
+
# Only after a real sync run with a successful push or sync-mode pull.
|
|
2616
|
+
if [ "$PUSH_SUCCESS" != true ] && [ "$SYNC_MODE" != true ]; then
|
|
2617
|
+
return 0
|
|
2618
|
+
fi
|
|
2619
|
+
|
|
2620
|
+
local branch
|
|
2621
|
+
branch=$(git branch --show-current 2>/dev/null || echo "")
|
|
2622
|
+
if [ "$branch" = "$MAIN_BRANCH" ]; then
|
|
2623
|
+
return 0
|
|
2624
|
+
fi
|
|
2625
|
+
|
|
2626
|
+
if has_tracked_changes; then
|
|
2627
|
+
log_warn "Checkout" "skipped — working tree has changes"
|
|
2628
|
+
return 0
|
|
2629
|
+
fi
|
|
2630
|
+
|
|
2631
|
+
if git checkout "$MAIN_BRANCH" >/dev/null 2>&1; then
|
|
2632
|
+
log_dim "Checkout" "$MAIN_BRANCH"
|
|
2633
|
+
if [ -n "${CREATED_PR_URL:-}" ]; then
|
|
2634
|
+
log_dim "Note" "changes on PR branch, not $MAIN_BRANCH"
|
|
2635
|
+
log_dim "PR" "$CREATED_PR_URL"
|
|
2636
|
+
fi
|
|
2637
|
+
else
|
|
2638
|
+
log_warn "Checkout" "failed: $MAIN_BRANCH"
|
|
2639
|
+
fi
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
if [ "$DRY_RUN" = false ] && [ "$STATUS_ONLY" = false ]; then
|
|
2643
|
+
maybe_checkout_main_after_sync
|
|
2644
|
+
fi
|
|
2645
|
+
|
|
2646
|
+
# === CLEANUP MERGED ROUTE-MAIN BRANCHES ===
|
|
2647
|
+
# After successful sync, clean up local wip/route-main/* branches that have been merged.
|
|
2648
|
+
if [ "$DRY_RUN" = false ]; then
|
|
2649
|
+
if [ "$PUSH_SUCCESS" = true ] || [ "$SYNC_MODE" = true ]; then
|
|
2650
|
+
# Prefer origin's tree (more up-to-date after fetch), fall back to local
|
|
2651
|
+
main_tree=$(git rev-parse "origin/$MAIN_BRANCH^{tree}" 2>/dev/null || git rev-parse "$MAIN_BRANCH^{tree}" 2>/dev/null || true)
|
|
2652
|
+
if [ -n "$main_tree" ]; then
|
|
2653
|
+
current_branch=$(git branch --show-current 2>/dev/null || true)
|
|
2654
|
+
tmpfile=$(mktemp 2>/dev/null) || true
|
|
2655
|
+
if [ -z "${tmpfile:-}" ]; then
|
|
2656
|
+
log_warn "Cleanup" "mktemp failed, skipping"
|
|
2657
|
+
else
|
|
2658
|
+
trap 'rm -f "$tmpfile"' EXIT
|
|
2659
|
+
git branch --list 'wip/route-main/*' 2>/dev/null | sed 's/^[* ]*//' | while IFS= read -r branch; do
|
|
2660
|
+
[ -z "$branch" ] && continue
|
|
2661
|
+
[ "$branch" = "$current_branch" ] && continue # Can't delete current branch
|
|
2662
|
+
branch_tree=$(git rev-parse "$branch^{tree}" 2>/dev/null || true)
|
|
2663
|
+
if [ -n "$branch_tree" ] && [ "$main_tree" = "$branch_tree" ]; then
|
|
2664
|
+
if delete_local_route_branch "$branch"; then
|
|
2665
|
+
echo "$branch" >> "$tmpfile"
|
|
2666
|
+
fi
|
|
2667
|
+
fi
|
|
2668
|
+
done
|
|
2669
|
+
if [ -s "$tmpfile" ]; then
|
|
2670
|
+
cleaned_count=$(wc -l < "$tmpfile" | tr -d ' ')
|
|
2671
|
+
log_dim "Cleanup" "$cleaned_count route-main branch(es) removed"
|
|
2672
|
+
fi
|
|
2673
|
+
fi
|
|
2674
|
+
fi
|
|
2675
|
+
fi
|
|
2676
|
+
fi
|
|
2677
|
+
|
|
2678
|
+
# === ORPHANED AUTO-STASH CHECK ===
|
|
2679
|
+
# If this run stashed changes, verify the stash was actually consumed.
|
|
2680
|
+
# Safety net: catches any path that leaves this run's auto-stash unconsumed.
|
|
2681
|
+
if [ "$DRY_RUN" = false ] && [ "${STASHED:-false}" = true ]; then
|
|
2682
|
+
orphan_stash=$(git stash list --grep='cm-auto-stash-' 2>/dev/null | head -1 || true)
|
|
2683
|
+
if [ -n "$orphan_stash" ]; then
|
|
2684
|
+
stash_ref=$(echo "$orphan_stash" | cut -d: -f1)
|
|
2685
|
+
log_err "Stash" "orphaned auto-stash detected: $stash_ref"
|
|
2686
|
+
echo " Your uncommitted changes were stashed but could not be restored."
|
|
2687
|
+
echo " Inspect: git stash show -p $stash_ref"
|
|
2688
|
+
echo " Restore: git stash pop $stash_ref"
|
|
2689
|
+
echo " Discard: git stash drop $stash_ref"
|
|
2690
|
+
HAS_WARNINGS=true
|
|
2691
|
+
fi
|
|
2692
|
+
fi
|
|
2693
|
+
|
|
2694
|
+
# === COMPLETION ===
|
|
2695
|
+
echo ""
|
|
2696
|
+
if [ "$DRY_RUN" = true ]; then
|
|
2697
|
+
log_dim "Done" "dry run complete — no changes made"
|
|
2698
|
+
elif [ "$SYNC_MODE" = true ]; then
|
|
2699
|
+
if [ "$HAS_WARNINGS" = true ]; then
|
|
2700
|
+
log_warn "Done" "synced (warnings above)"
|
|
2701
|
+
else
|
|
2702
|
+
log_ok "Done" "synced"
|
|
2703
|
+
fi
|
|
2704
|
+
else
|
|
2705
|
+
if [ "$PUSH_SUCCESS" = true ]; then
|
|
2706
|
+
if [ "$HAS_WARNINGS" = true ]; then
|
|
2707
|
+
log_warn "Done" "committed and pushed (warnings above)"
|
|
2708
|
+
else
|
|
2709
|
+
log_ok "Done" "committed and pushed"
|
|
2710
|
+
fi
|
|
2711
|
+
else
|
|
2712
|
+
log_warn "Done" "partial success (see above)"
|
|
2713
|
+
fi
|
|
2714
|
+
fi
|
|
2715
|
+
echo ""
|
|
2716
|
+
|
|
2717
|
+
# Explicit exit code: 0 when journey completed; 1 only on failure (warnings do not fail)
|
|
2718
|
+
if [ "$HAS_FAILURE" = true ]; then
|
|
2719
|
+
exit 1
|
|
2720
|
+
else
|
|
2721
|
+
exit 0
|
|
2722
|
+
fi
|